merge fixup
This commit is contained in:
commit
9deb35f36e
5
.gitignore
vendored
5
.gitignore
vendored
@ -4,3 +4,8 @@
|
||||
cover
|
||||
*.pyc
|
||||
.idea
|
||||
*.swp
|
||||
*~
|
||||
build
|
||||
dist
|
||||
python_novaclient.egg-info
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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))
|
||||
|
@ -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.
|
||||
|
@ -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():
|
||||
|
@ -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):
|
||||
|
2
setup.py
2
setup.py
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user