merge fixup

This commit is contained in:
Sandy Walsh 2011-09-02 12:13:22 -07:00
commit 9deb35f36e
19 changed files with 331 additions and 214 deletions

5
.gitignore vendored
View File

@ -4,3 +4,8 @@
cover
*.pyc
.idea
*.swp
*~
build
dist
python_novaclient.egg-info

View File

@ -67,12 +67,13 @@ class Manager(object):
if obj_class is None:
obj_class = self.resource_class
data = body[response_key]
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
# unlike other services which just return the list...
if type(data) is dict:
data = data['values']
return [obj_class(self, res) for res in data if res]
return [obj_class(self, res, loaded=True) for res in data if res]
def _get(self, url, response_key):
resp, body = self.api.client.get(url)
@ -203,19 +204,28 @@ class Resource(object):
"""
A resource represents a particular instance of an object (server, flavor,
etc). This is pretty much just a bag for attributes.
:param manager: Manager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
def __init__(self, manager, info):
def __init__(self, manager, info, loaded=False):
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
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__:
#NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)
raise AttributeError(k)
else:
return self.__dict__[k]
@ -227,8 +237,11 @@ class Resource(object):
return "<%s %s>" % (self.__class__.__name__, info)
def get(self):
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
@ -239,3 +252,9 @@ class Resource(object):
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info
def is_loaded(self):
return self._loaded
def set_loaded(self, val):
self._loaded = val

View File

@ -66,6 +66,8 @@ class HTTPClient(httplib2.Http):
string_parts.append(header)
_logger.debug("REQ: %s\n" % "".join(string_parts))
if 'body' in kwargs:
_logger.debug("REQ BODY: %s\n" % (kwargs['body']))
_logger.debug("RESP:%s %s\n", resp, body)
def request(self, *args, **kwargs):

View File

@ -163,6 +163,16 @@ class OpenStackComputeShell(object):
raise exc.CommandError("You must provide an API key, either"
"via --apikey or via"
"env[NOVA_API_KEY]")
if options.version and options.version != '1.0':
if not projectid:
raise exc.CommandError("You must provide an projectid, either"
"via --projectid or via"
"env[NOVA_PROJECT_ID")
if not url:
raise exc.CommandError("You must provide a auth url, either"
"via --url or via"
"env[NOVA_URL")
self.cs = self.get_api_class(options.version) \
(user, apikey, projectid, url,

View File

@ -22,9 +22,9 @@ from novaclient.v1_0 import base as local_base
class Weighting(base.Resource):
def __init__(self, manager, info):
def __init__(self, manager, info, loaded=False):
self.name = "n/a"
super(Weighting, self).__init__(manager, info)
super(Weighting, self).__init__(manager, info, loaded)
def __repr__(self):
return "<Weighting: %s>" % self.name
@ -35,11 +35,11 @@ class Weighting(base.Resource):
class Zone(base.Resource):
def __init__(self, manager, info):
def __init__(self, manager, info, loaded=False):
self.name = "n/a"
self.is_active = "n/a"
self.capabilities = "n/a"
super(Zone, self).__init__(manager, info)
super(Zone, self).__init__(manager, info, loaded)
def __repr__(self):
return "<Zone: %s>" % self.api_url

View File

@ -15,117 +15,11 @@
# 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 base
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:
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,
meta=None, files=None, zone_blob=None,
@ -155,8 +49,8 @@ class BootingManagerWithFind(ManagerWithFind):
"""
body = {"server": {
"name": name,
"imageRef": getid(image),
"flavorRef": getid(flavor),
"imageRef": base.getid(image),
"flavorRef": base.getid(flavor),
}}
if meta:
body["server"]["metadata"] = meta
@ -194,43 +88,3 @@ class BootingManagerWithFind(ManagerWithFind):
return self._create(resource_url, body, response_key,
return_raw=return_raw)
class Resource(object):
"""
A resource represents a particular instance of an object (server, flavor,
etc). This is pretty much just a bag for attributes.
"""
def __init__(self, manager, info):
self.manager = manager
self._info = info
self._add_details(info)
def _add_details(self, info):
for (k, v) in info.iteritems():
setattr(self, k, v)
def __getattr__(self, k):
self.get()
if k not in self.__dict__:
raise AttributeError(k)
else:
return self.__dict__[k]
def __repr__(self):
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
k != 'manager')
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
def get(self):
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
if hasattr(self, 'id') and hasattr(other, 'id'):
return self.id == other.id
return self._info == other._info

View File

@ -13,7 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from novaclient.v1_1 import base
from novaclient import base
class FloatingIP(base.Resource):

View File

@ -56,3 +56,24 @@ class ImageManager(base.ManagerWithFind):
:param image: The :class:`Image` (or its ID) to delete.
"""
self._delete("/images/%s" % base.getid(image))
def set_meta(self, image, metadata):
"""
Set an images metadata
:param image: The :class:`Image` to add metadata to
:param metadata: A dict of metadata to add to the image
"""
body = {'metadata': metadata}
return self._create("/images/%s/metadata" % base.getid(image), body,
"metadata")
def delete_meta(self, image, keys):
"""
Delete metadata from an image
:param image: The :class:`Image` to add metadata to
:param keys: A list of metadata keys to delete from the image
"""
for k in keys:
self._delete("/images/%s/metadata/%s" % (base.getid(image), k))

View File

@ -461,6 +461,25 @@ class ServerManager(local_base.BootingManagerWithFind):
self._action('createImage', server,
{'name': image_name, 'metadata': metadata or {}})
def set_meta(self, server, metadata):
"""
Set a servers metadata
:param server: The :class:`Server` to add metadata to
:param metadata: A dict of metadata to add to the server
"""
body = {'metadata': metadata}
return self._create("/servers/%s/metadata" % base.getid(server),
body, "metadata")
def delete_meta(self, server, keys):
"""
Delete metadata from an server
:param server: The :class:`Server` to add metadata to
:param keys: A list of metadata keys to delete from the server
"""
for k in keys:
self._delete("/servers/%s/metadata/%s" % (base.getid(server), k))
def _action(self, action, server, info=None):
"""
Perform a server "action" -- reboot/rebuild/resize/etc.

View File

@ -17,7 +17,6 @@
import getpass
import os
import uuid
from novaclient import exceptions
from novaclient import utils
@ -44,9 +43,13 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
raise exceptions.CommandError("min_instances nor max_instances should"
"be 0")
flavor = args.flavor or cs.flavors.find(ram=256)
image = args.image or cs.images.find(name="Ubuntu 10.04 LTS "\
"(lucid)")
if not args.image:
raise exceptions.CommandError("you need to specify a Image ID ")
if not args.flavor:
raise exceptions.CommandError("you need to specify a Flavor ID ")
flavor = args.flavor
image = args.image
metadata = dict(v.split('=') for v in args.meta)
@ -86,13 +89,11 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
@utils.arg('--flavor',
default=None,
metavar='<flavor>',
help="Flavor ID (see 'nova flavors'). "\
"Defaults to 256MB RAM instance.")
help="Flavor ID (see 'nova flavor-list').")
@utils.arg('--image',
default=None,
metavar='<image>',
help="Image ID (see 'nova images'). "\
"Defaults to Ubuntu 10.04 LTS.")
help="Image ID (see 'nova image-list'). ")
@utils.arg('--meta',
metavar="<key=value>",
action='append',
@ -144,13 +145,11 @@ def do_boot(cs, args):
@utils.arg('--flavor',
default=None,
metavar='<flavor>',
help="Flavor ID (see 'nova flavors'). "\
"Defaults to 256MB RAM instance.")
help="Flavor ID (see 'nova flavor-list')")
@utils.arg('--image',
default=None,
metavar='<image>',
help="Image ID (see 'nova images'). "\
"Defaults to Ubuntu 10.04 LTS.")
help="Image ID (see 'nova image-list').")
@utils.arg('--meta',
metavar="<key=value>",
action='append',
@ -238,6 +237,52 @@ def do_image_list(cs, args):
"""Print a list of available images to boot from."""
utils.print_list(cs.images.list(), ['ID', 'Name', 'Status'])
@utils.arg('image',
metavar='<image>',
help="Name or ID of image")
@utils.arg('action',
metavar='<action>',
choices=['set', 'delete'],
help="Actions: 'set' or 'delete'")
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
action='append',
default=[],
help='Metadata to add/update or delete (only key is necessary on delete)')
def do_image_meta(cs, args):
"""Set or Delete metadata on an image."""
image = _find_image(cs, args.image)
metadata = {}
for metadatum in args.metadata[0]:
# Can only pass the key in on 'delete'
# So this doesn't have to have '='
if metadatum.find('=') > -1:
(key, value) = metadatum.split('=',1)
else:
key = metadatum
value = None
metadata[key] = value
if args.action == 'set':
cs.images.set_meta(image, metadata)
elif args.action == 'delete':
cs.images.delete_meta(image, metadata.keys())
def _print_image(image):
links = image.links
info = image._info.copy()
info.pop('links')
utils.print_dict(info)
@utils.arg('image',
metavar='<image>',
help="Name or ID of image")
def do_image_show(cs, args):
"""Show details about the given image."""
image = _find_image(cs, args.image)
_print_image(image)
@utils.arg('image', metavar='<image>', help='Name or ID of image.')
def do_image_delete(cs, args):
@ -478,8 +523,49 @@ def do_image_create(cs, args):
server = _find_server(cs, args.server)
cs.servers.create_image(server, args.name)
@utils.arg('server',
metavar='<server>',
help="Name or ID of server")
@utils.arg('action',
metavar='<action>',
choices=['set', 'delete'],
help="Actions: 'set' or 'delete'")
@utils.arg('metadata',
metavar='<key=value>',
nargs='+',
action='append',
default=[],
help='Metadata to set or delete (only key is necessary on delete)')
def do_meta(cs, args):
"""Set or Delete metadata on a server."""
server = _find_server(cs, args.server)
metadata = {}
for metadatum in args.metadata[0]:
# Can only pass the key in on 'delete'
# So this doesn't have to have '='
if metadatum.find('=') > -1:
(key, value) = metadatum.split('=',1)
else:
key = metadatum
value = None
metadata[key] = value
if args.action == 'set':
cs.servers.set_meta(server, metadata)
elif args.action == 'delete':
cs.servers.delete_meta(server, metadata.keys())
def _print_server(cs, server):
# By default when searching via name we will do a
# findall(name=blah) and due a REST /details which is not the same
# as a .get() and doesn't get the information about flavors and
# images. This fix it as we redo the call with the id which does a
# .get() to get all informations.
if not 'flavor' in server._info:
server = _find_server(cs, server.id)
networks = server.networks
info = server._info.copy()
for network_label, address_list in networks.items():

View File

@ -17,13 +17,14 @@
Zone interface.
"""
from novaclient.v1_1 import base
from novaclient import base
from novaclient.v1_1 import base as local_base
class Weighting(base.Resource):
def __init__(self, manager, info):
def __init__(self, manager, info, loaded=False):
self.name = "n/a"
super(Weighting, self).__init__(manager, info)
super(Weighting, self).__init__(manager, info, loaded)
def __repr__(self):
return "<Weighting: %s>" % self.name
@ -34,11 +35,11 @@ class Weighting(base.Resource):
class Zone(base.Resource):
def __init__(self, manager, info):
def __init__(self, manager, info, loaded=False):
self.name = "n/a"
self.is_active = "n/a"
self.capabilities = "n/a"
super(Zone, self).__init__(manager, info)
super(Zone, self).__init__(manager, info, loaded)
def __repr__(self):
return "<Zone: %s>" % self.api_url
@ -64,7 +65,7 @@ class Zone(base.Resource):
weight_offset, weight_scale)
class ZoneManager(base.BootingManagerWithFind):
class ZoneManager(local_base.BootingManagerWithFind):
resource_class = Zone
def info(self):

View File

@ -19,7 +19,7 @@ setup(
license = 'Apache',
author = 'Rackspace, based on work by Jacob Kaplan-Moss',
author_email = 'github@racklabs.com',
packages = find_packages(exclude=['tests']),
packages = find_packages(exclude=['tests', 'tests.*']),
classifiers = [
'Development Status :: 5 - Production/Stable',
'Environment :: Console',

View File

@ -23,12 +23,12 @@ def assert_has_keys(dict, required=[], optional=[]):
class FakeClient(object):
def assert_called(self, method, url, body=None):
def assert_called(self, method, url, body=None, pos=-1):
"""
Assert than an API method was just called.
"""
expected = (method, url)
called = self.client.callstack[-1][0:2]
called = self.client.callstack[pos][0:2]
assert self.client.callstack, \
"Expected %s %s but no calls were made." % expected
@ -37,11 +37,7 @@ class FakeClient(object):
(expected + called)
if body is not None:
print "CALL", self.client.callstack[-1][2]
print "BODY", body
assert self.client.callstack[-1][2] == body
self.client.callstack = []
assert self.client.callstack[pos][2] == body
def assert_called_anytime(self, method, url, body=None):
"""
@ -72,5 +68,8 @@ class FakeClient(object):
self.client.callstack = []
def clear_callstack(self):
self.client.callstack = []
def authenticate(self):
pass

View File

@ -30,7 +30,6 @@ class BaseTest(utils.TestCase):
# 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

View File

@ -16,6 +16,7 @@ class ShellTest(utils.TestCase):
'NOVA_USERNAME': 'username',
'NOVA_API_KEY': 'password',
'NOVA_PROJECT_ID': 'project_id',
'NOVA_URL': 'http://no.where',
}
_old_env, os.environ = os.environ, fake_env.copy()

View File

@ -213,6 +213,18 @@ class FakeHTTPClient(base_client.HTTPClient):
def delete_servers_1234(self, **kw):
return (202, None)
def delete_servers_1234_metadata_test_key(self, **kw):
return (204, None)
def delete_servers_1234_metadata_key1(self, **kw):
return (204, None)
def delete_servers_1234_metadata_key2(self, **kw):
return (204, None)
def post_servers_1234_metadata(self, **kw):
return (204, {'metadata': { 'test_key': 'test_value'}})
#
# Server Addresses
#
@ -338,7 +350,11 @@ class FakeHTTPClient(base_client.HTTPClient):
'name': 'CentOS 5.2',
"updated": "2010-10-10T12:00:00Z",
"created": "2010-08-10T12:00:00Z",
"status": "ACTIVE"
"status": "ACTIVE",
"metadata": {
"test_key": "test_value",
},
"links": {},
},
{
"id": 743,
@ -347,7 +363,8 @@ class FakeHTTPClient(base_client.HTTPClient):
"updated": "2010-10-10T12:00:00Z",
"created": "2010-08-10T12:00:00Z",
"status": "SAVING",
"progress": 80
"progress": 80,
"links": {},
}
]})
@ -362,9 +379,19 @@ class FakeHTTPClient(base_client.HTTPClient):
fakes.assert_has_keys(body['image'], required=['serverId', 'name'])
return (202, self.get_images_1()[1])
def post_images_1_metadata(self, body, **kw):
assert body.keys() == ['metadata']
fakes.assert_has_keys(body['metadata'],
required=['test_key'])
return (200,
{'metadata': self.get_images_1()[1]['image']['metadata']})
def delete_images_1(self, **kw):
return (204, None)
def delete_images_1_metadata_test_key(self, **kw):
return (204, None)
#
# Zones
#

View File

@ -29,6 +29,15 @@ class ImagesTest(utils.TestCase):
cs.images.delete(1)
cs.assert_called('DELETE', '/images/1')
def test_delete_meta(self):
cs.images.delete_meta(1, {'test_key': 'test_value'})
cs.assert_called('DELETE', '/images/1/metadata/test_key')
def test_set_meta(self):
cs.images.set_meta(1, {'test_key': 'test_value'})
cs.assert_called('POST', '/images/1/metadata',
{"metadata": {'test_key': 'test_value'}})
def test_find(self):
i = cs.images.find(name="CentOS 5.2")
self.assertEqual(i.id, 1)

View File

@ -66,6 +66,15 @@ class ServersTest(utils.TestCase):
cs.servers.delete(s)
cs.assert_called('DELETE', '/servers/1234')
def test_delete_server_meta(self):
s = cs.servers.delete_meta(1234, ['test_key'])
cs.assert_called('DELETE', '/servers/1234/metadata/test_key')
def test_set_server_meta(self):
s = cs.servers.set_meta(1234, {'test_key': 'test_value'})
reval = cs.assert_called('POST', '/servers/1234/metadata',
{'metadata': { 'test_key': 'test_value' }})
def test_find(self):
s = cs.servers.find(name='sample-server')
cs.assert_called('GET', '/servers/detail')

View File

@ -1,5 +1,7 @@
import os
import mock
import sys
import tempfile
from novaclient.shell import OpenStackComputeShell
from novaclient import exceptions
@ -18,6 +20,7 @@ class ShellTest(utils.TestCase):
'NOVA_API_KEY': 'password',
'NOVA_PROJECT_ID': 'project_id',
'NOVA_VERSION': '1.1',
'NOVA_URL': 'http://no.where',
}
self.shell = OpenStackComputeShell()
@ -25,64 +28,70 @@ class ShellTest(utils.TestCase):
def tearDown(self):
os.environ = self.old_environment
# For some method like test_image_meta_bad_action we are
# testing a SystemExit to be thrown and object self.shell has
# no time to get instantatiated which is OK in this case, so
# we make sure the method is there before launching it.
if hasattr(self.shell, 'cs'):
self.shell.cs.clear_callstack()
def run_command(self, cmd):
self.shell.main(cmd.split())
def assert_called(self, method, url, body=None):
return self.shell.cs.assert_called(method, url, body)
def assert_called(self, method, url, body=None, **kwargs):
return self.shell.cs.assert_called(method, url, body, **kwargs)
def assert_called_anytime(self, method, url, body=None):
return self.shell.cs.assert_called_anytime(method, url, body)
def test_boot(self):
self.run_command('boot --image 1 some-server')
self.run_command('boot --flavor 1 --image 1 some-server')
self.assert_called_anytime(
'POST', '/servers',
{'server': {
'flavorRef': 1,
'flavorRef': '1',
'name': 'some-server',
'imageRef': '1',
'min_count': 1,
'max_count': 1,
}}
}},
)
self.run_command('boot --image 1 --meta foo=bar'
self.run_command('boot --image 1 --flavor 1 --meta foo=bar'
' --meta spam=eggs some-server ')
self.assert_called_anytime(
'POST', '/servers',
{'server': {
'flavorRef': 1,
'flavorRef': '1',
'name': 'some-server',
'imageRef': '1',
'metadata': {'foo': 'bar', 'spam': 'eggs'},
'min_count': 1,
'max_count': 1,
}}
}},
)
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 ' \
cmd = 'boot some-server --flavor 1 --image 1 ' \
'--file /tmp/foo=%s --file /tmp/bar=%s'
self.run_command(cmd % (testfile, testfile))
self.assert_called_anytime(
'POST', '/servers',
{'server': {
'flavorRef': 1,
'flavorRef': '1',
'name': 'some-server',
'imageRef': '1',
'min_count': 1,
'max_count': 1,
'personality': [
{'path': '/tmp/bar', 'contents': expected_file_data},
{'path': '/tmp/foo', 'contents': expected_file_data}
]}
}
{'path': '/tmp/foo', 'contents': expected_file_data},
]},
},
)
def test_boot_invalid_file(self):
@ -100,11 +109,11 @@ class ShellTest(utils.TestCase):
@mock.patch('os.path.exists', mock_exists)
@mock.patch('__builtin__.open', mock_open)
def test_shell_call():
self.run_command('boot some-server --image 1 --key')
self.run_command('boot some-server --flavor 1 --image 1 --key')
self.assert_called_anytime(
'POST', '/servers',
{'server': {
'flavorRef': 1,
'flavorRef': '1',
'name': 'some-server',
'imageRef': '1',
'min_count': 1,
@ -112,8 +121,8 @@ class ShellTest(utils.TestCase):
'personality': [{
'path': '/root/.ssh/authorized_keys2',
'contents': ('SSHKEY').encode('base64')},
]}
}
]},
},
)
test_shell_call()
@ -124,18 +133,19 @@ class ShellTest(utils.TestCase):
@mock.patch('os.path.exists', mock_exists)
def test_shell_call():
self.assertRaises(exceptions.CommandError, self.run_command,
'boot some-server --image 1 --key')
'boot some-server --flavor 1 --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')
self.run_command('boot some-server --image 1 --key %s' % testfile)
cmd = 'boot some-server --flavor 1 --image 1 --key %s'
self.run_command(cmd % testfile)
self.assert_called_anytime(
'POST', '/servers',
{'server': {
'flavorRef': 1,
'flavorRef': '1',
'name': 'some-server',
'imageRef': '1',
'min_count': 1,
@ -143,20 +153,47 @@ class ShellTest(utils.TestCase):
'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')
cmd = 'boot some-server --flavor 1 --image 1 --key %s'
self.assertRaises(exceptions.CommandError, self.run_command,
'boot some-server --image 1 --key %s' % invalid_file)
cmd % invalid_file)
def test_flavor_list(self):
self.run_command('flavor-list')
self.assert_called_anytime('GET', '/flavors/detail')
def test_image_show(self):
self.run_command('image-show 1')
self.assert_called('GET', '/images/1')
def test_image_meta_set(self):
self.run_command('image-meta 1 set test_key=test_value')
self.assert_called('POST', '/images/1/metadata',
{'metadata': {'test_key': 'test_value'}})
def test_image_meta_del(self):
self.run_command('image-meta 1 delete test_key=test_value')
self.assert_called('DELETE', '/images/1/metadata/test_key')
def test_image_meta_bad_action(self):
tmp = tempfile.TemporaryFile()
# Suppress stdout and stderr
(stdout, stderr) = (sys.stdout, sys.stderr)
(sys.stdout, sys.stderr) = (tmp, tmp)
self.assertRaises(SystemExit, self.run_command,
'image-meta 1 BAD_ACTION test_key=test_value')
# Put stdout and stderr back
sys.stdout, sys.stderr = (stdout, stderr)
def test_image_list(self):
self.run_command('image-list')
self.assert_called('GET', '/images/detail')
@ -165,7 +202,7 @@ class ShellTest(utils.TestCase):
self.run_command('image-create sample-server mysnapshot')
self.assert_called(
'POST', '/servers/1234/action',
{'createImage': {'name': 'mysnapshot', 'metadata': {}}}
{'createImage': {'name': 'mysnapshot', 'metadata': {}}},
)
def test_image_delete(self):
@ -197,7 +234,6 @@ class ShellTest(utils.TestCase):
# {'rebuild': {'imageRef': 1, 'adminPass': 'asdf'}})
self.assert_called('GET', '/images/2')
def test_rename(self):
self.run_command('rename sample-server newname')
self.assert_called('PUT', '/servers/1234',
@ -226,12 +262,32 @@ class ShellTest(utils.TestCase):
def test_show(self):
self.run_command('show 1234')
# XXX need a way to test multiple calls
# assert_called('GET', '/servers/1234')
self.assert_called('GET', '/servers/1234', pos=-3)
self.assert_called('GET', '/flavors/1', pos=-2)
self.assert_called('GET', '/images/2')
def test_show_bad_id(self):
self.assertRaises(exceptions.CommandError,
self.run_command, 'show xxx')
def test_delete(self):
self.run_command('delete 1234')
self.assert_called('DELETE', '/servers/1234')
self.run_command('delete sample-server')
self.assert_called('DELETE', '/servers/1234')
def test_set_meta_set(self):
self.run_command('meta 1234 set key1=val1 key2=val2')
self.assert_called('POST', '/servers/1234/metadata',
{'metadata': {'key1': 'val1', 'key2': 'val2'}})
def test_set_meta_delete_dict(self):
self.run_command('meta 1234 delete key1=val1 key2=val2')
self.assert_called('DELETE', '/servers/1234/metadata/key1')
self.assert_called('DELETE', '/servers/1234/metadata/key2', pos=-2)
def test_set_meta_delete_keys(self):
self.run_command('meta 1234 delete key1 key2')
self.assert_called('DELETE', '/servers/1234/metadata/key1')
self.assert_called('DELETE', '/servers/1234/metadata/key2', pos=-2)