summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--novaclient/base.py25
-rw-r--r--novaclient/client.py2
-rw-r--r--novaclient/shell.py10
-rw-r--r--novaclient/v1_0/zones.py8
-rw-r--r--novaclient/v1_1/base.py154
-rw-r--r--novaclient/v1_1/floating_ips.py2
-rw-r--r--novaclient/v1_1/images.py21
-rw-r--r--novaclient/v1_1/servers.py19
-rw-r--r--novaclient/v1_1/shell.py110
-rw-r--r--novaclient/v1_1/zones.py13
-rw-r--r--setup.py2
-rw-r--r--tests/fakes.py13
-rw-r--r--tests/test_base.py1
-rw-r--r--tests/test_shell.py1
-rw-r--r--tests/v1_1/fakes.py31
-rw-r--r--tests/v1_1/test_images.py9
-rw-r--r--tests/v1_1/test_servers.py9
-rw-r--r--tests/v1_1/test_shell.py110
19 files changed, 331 insertions, 214 deletions
diff --git a/.gitignore b/.gitignore
index b7451250..33dc9486 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,8 @@
cover
*.pyc
.idea
+*.swp
+*~
+build
+dist
+python_novaclient.egg-info
diff --git a/novaclient/base.py b/novaclient/base.py
index e18e99db..b2c9a84d 100644
--- a/novaclient/base.py
+++ b/novaclient/base.py
@@ -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
diff --git a/novaclient/client.py b/novaclient/client.py
index c104052a..88564223 100644
--- a/novaclient/client.py
+++ b/novaclient/client.py
@@ -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):
diff --git a/novaclient/shell.py b/novaclient/shell.py
index 1f59e095..f39fa190 100644
--- a/novaclient/shell.py
+++ b/novaclient/shell.py
@@ -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,
diff --git a/novaclient/v1_0/zones.py b/novaclient/v1_0/zones.py
index f314ea0d..a36d1659 100644
--- a/novaclient/v1_0/zones.py
+++ b/novaclient/v1_0/zones.py
@@ -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
diff --git a/novaclient/v1_1/base.py b/novaclient/v1_1/base.py
index 29cbd31e..7c5d284e 100644
--- a/novaclient/v1_1/base.py
+++ b/novaclient/v1_1/base.py
@@ -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
diff --git a/novaclient/v1_1/floating_ips.py b/novaclient/v1_1/floating_ips.py
index 9fd61a11..13503e82 100644
--- a/novaclient/v1_1/floating_ips.py
+++ b/novaclient/v1_1/floating_ips.py
@@ -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):
diff --git a/novaclient/v1_1/images.py b/novaclient/v1_1/images.py
index e25c237e..37b61cb5 100644
--- a/novaclient/v1_1/images.py
+++ b/novaclient/v1_1/images.py
@@ -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))
diff --git a/novaclient/v1_1/servers.py b/novaclient/v1_1/servers.py
index 00ed5f53..ba7dc443 100644
--- a/novaclient/v1_1/servers.py
+++ b/novaclient/v1_1/servers.py
@@ -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.
diff --git a/novaclient/v1_1/shell.py b/novaclient/v1_1/shell.py
index 5efd6a8c..6b83292e 100644
--- a/novaclient/v1_1/shell.py
+++ b/novaclient/v1_1/shell.py
@@ -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():
diff --git a/novaclient/v1_1/zones.py b/novaclient/v1_1/zones.py
index f4a9c907..c8308d48 100644
--- a/novaclient/v1_1/zones.py
+++ b/novaclient/v1_1/zones.py
@@ -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):
diff --git a/setup.py b/setup.py
index 9c56ba47..08db0c75 100644
--- a/setup.py
+++ b/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',
diff --git a/tests/fakes.py b/tests/fakes.py
index f09b2a52..e1b07324 100644
--- a/tests/fakes.py
+++ b/tests/fakes.py
@@ -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
diff --git a/tests/test_base.py b/tests/test_base.py
index c53c3aa8..c5e3fc79 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -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
diff --git a/tests/test_shell.py b/tests/test_shell.py
index c24b4a4e..38f8ef3a 100644
--- a/tests/test_shell.py
+++ b/tests/test_shell.py
@@ -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()
diff --git a/tests/v1_1/fakes.py b/tests/v1_1/fakes.py
index 1f711778..a1554fe0 100644
--- a/tests/v1_1/fakes.py
+++ b/tests/v1_1/fakes.py
@@ -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
#
diff --git a/tests/v1_1/test_images.py b/tests/v1_1/test_images.py
index bf61a338..07609af7 100644
--- a/tests/v1_1/test_images.py
+++ b/tests/v1_1/test_images.py
@@ -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)
diff --git a/tests/v1_1/test_servers.py b/tests/v1_1/test_servers.py
index 9f69754d..21c94758 100644
--- a/tests/v1_1/test_servers.py
+++ b/tests/v1_1/test_servers.py
@@ -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')
diff --git a/tests/v1_1/test_shell.py b/tests/v1_1/test_shell.py
index a0a4dbe3..e6d647b2 100644
--- a/tests/v1_1/test_shell.py
+++ b/tests/v1_1/test_shell.py
@@ -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)