diff options
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | novaclient/base.py | 25 | ||||
-rw-r--r-- | novaclient/client.py | 2 | ||||
-rw-r--r-- | novaclient/shell.py | 10 | ||||
-rw-r--r-- | novaclient/v1_0/zones.py | 8 | ||||
-rw-r--r-- | novaclient/v1_1/base.py | 154 | ||||
-rw-r--r-- | novaclient/v1_1/floating_ips.py | 2 | ||||
-rw-r--r-- | novaclient/v1_1/images.py | 21 | ||||
-rw-r--r-- | novaclient/v1_1/servers.py | 19 | ||||
-rw-r--r-- | novaclient/v1_1/shell.py | 110 | ||||
-rw-r--r-- | novaclient/v1_1/zones.py | 13 | ||||
-rw-r--r-- | setup.py | 2 | ||||
-rw-r--r-- | tests/fakes.py | 13 | ||||
-rw-r--r-- | tests/test_base.py | 1 | ||||
-rw-r--r-- | tests/test_shell.py | 1 | ||||
-rw-r--r-- | tests/v1_1/fakes.py | 31 | ||||
-rw-r--r-- | tests/v1_1/test_images.py | 9 | ||||
-rw-r--r-- | tests/v1_1/test_servers.py | 9 | ||||
-rw-r--r-- | tests/v1_1/test_shell.py | 110 |
19 files changed, 331 insertions, 214 deletions
@@ -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): @@ -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) |