diff --git a/glanceclient/common/base.py b/glanceclient/common/base.py index f61a1bd..686ed12 100644 --- a/glanceclient/common/base.py +++ b/glanceclient/common/base.py @@ -33,13 +33,6 @@ 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: @@ -86,10 +79,12 @@ class Manager(object): methods = {"PUT": self.api.put, "POST": self.api.post} try: - resp, body = methods[method](url, body=body) + _method = methods[method] except KeyError: - raise exceptions.ClientException("Invalid update method: %s" - % method) + msg = "Invalid update method: %s" % method + raise exceptions.ClientException(msg) + + resp, body = _method(url, body=body) # PUT requests may not return a body if body: return self.resource_class(self, body[response_key]) diff --git a/glanceclient/v1/client.py b/glanceclient/v1/client.py index bcb5bad..6671c7a 100644 --- a/glanceclient/v1/client.py +++ b/glanceclient/v1/client.py @@ -17,6 +17,7 @@ import logging from glanceclient.common import http from glanceclient.v1 import images +from glanceclient.v1 import image_members logger = logging.getLogger(__name__) @@ -36,3 +37,4 @@ class Client(http.HTTPClient): """ Initialize a new client for the Images v1 API. """ super(Client, self).__init__(endpoint, token=token, timeout=timeout) self.images = images.ImageManager(self) + self.image_members = image_members.ImageMemberManager(self) diff --git a/glanceclient/v1/image_members.py b/glanceclient/v1/image_members.py new file mode 100644 index 0000000..459d6f7 --- /dev/null +++ b/glanceclient/v1/image_members.py @@ -0,0 +1,64 @@ +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from glanceclient.common import base + + +class ImageMember(base.Resource): + def __repr__(self): + return "" % self._info + + +class ImageMemberManager(base.Manager): + resource_class = ImageMember + + def get(self, image, member_id): + url = '/v1/images/%s' % (base.getid(image), member_id) + return self._get(url, 'member') + + def list(self, image): + url = '/v1/images/%s/members' % base.getid(image) + return self._list(url, 'members') + + def delete(self, image, member): + image_id = base.getid(image) + try: + member_id = base.getid(member) + except AttributeError: + member_id = member + + self._delete("/v1/images/%s/members/%s" % (image_id, member_id)) + + def create(self, image, member_id, can_share=False): + """Create an image""" + url = '/v1/images/%s/members/%s' % (base.getid(image), member_id) + body = {'member': {'can_share': can_share}} + self._update(url, body=body) + + def replace(self, image, members): + memberships = [] + for member in members: + try: + obj = { + 'member_id': member.member_id, + 'can_share': member.can_share, + } + except AttributeError: + obj = {'member_id': member['member_id']} + if 'can_share' in member: + obj['can_share'] = member['can_share'] + memberships.append(obj) + url = '/v1/images/%s/members' % base.getid(image) + self.api.put(url, {}, {'memberships': memberships}) diff --git a/glanceclient/v1/images.py b/glanceclient/v1/images.py index 97e2ce4..44b0aaf 100644 --- a/glanceclient/v1/images.py +++ b/glanceclient/v1/images.py @@ -22,6 +22,9 @@ class Image(base.Resource): def __repr__(self): return "" % self._info + def update(self, **fields): + self.manager.update(self, **fields) + def delete(self): return self.manager.delete(self) @@ -49,13 +52,13 @@ class ImageManager(base.Manager): headers['x-image-meta-%s' % key] = value return headers - def get(self, image): + def get(self, image_id): """Get the metadata for a specific image. :param image: image object or id to look up :rtype: :class:`Image` """ - resp, body = self.api.head('/v1/images/%s' % base.getid(image)) + resp, body = self.api.head('/v1/images/%s' % image_id) meta = self._image_meta_from_headers(resp) return Image(self, meta) @@ -97,3 +100,14 @@ class ImageManager(base.Manager): resp, body = self.api.put(url, headers=send_meta) recv_meta = self._image_meta_from_headers(resp) return Image(self, recv_meta) + + def delete_member(self, image, image_member): + """Remove a member from an image""" + image_id = base.getid(image) + try: + member_id = image_member.member_id + except AttributeError: + member_id = image_member + + url = '/v1/images/%s/members/%s' % (image_id, member_id) + resp, body = self.api.delete(url) diff --git a/glanceclient/v1/shell.py b/glanceclient/v1/shell.py old mode 100755 new mode 100644 diff --git a/tests/v1/test_image_members.py b/tests/v1/test_image_members.py new file mode 100644 index 0000000..f7d0659 --- /dev/null +++ b/tests/v1/test_image_members.py @@ -0,0 +1,60 @@ + +import unittest + +from tests.v1 import utils + +import glanceclient.v1.images +import glanceclient.v1.image_members + + +class ImageMemberManagerTest(unittest.TestCase): + + def setUp(self): + self.api = utils.FakeAPI() + self.mgr = glanceclient.v1.image_members.ImageMemberManager(self.api) + self.image = glanceclient.v1.images.Image(self.api, {'id': '1'}, True) + + def test_list(self): + members = self.mgr.list(self.image) + expect = [('GET', '/v1/images/1/members', {}, None)] + self.assertEqual(self.api.calls, expect) + self.assertEqual(len(members), 1) + self.assertEqual(members[0].member_id, '1') + self.assertEqual(members[0].can_share, False) + + def test_delete(self): + self.mgr.delete(self.image, '1') + expect = [('DELETE', '/v1/images/1/members/1', {}, None)] + self.assertEqual(self.api.calls, expect) + + def test_create(self): + self.mgr.create(self.image, '1', can_share=True) + expect_body = {'member': {'can_share': True}} + expect = [('PUT', '/v1/images/1/members/1', {}, expect_body)] + self.assertEqual(self.api.calls, expect) + + def test_replace(self): + body = [ + {'member_id': '2', 'can_share': False}, + {'member_id': '3'}, + ] + self.mgr.replace(self.image, body) + expect = [('PUT', '/v1/images/1/members', {}, {'memberships': body})] + self.assertEqual(self.api.calls, expect) + + def test_replace_objects(self): + body = [ + glanceclient.v1.image_members.ImageMember( + self.mgr, {'member_id': '2', 'can_share': False}), + glanceclient.v1.image_members.ImageMember( + self.mgr, {'member_id': '3', 'can_share': True}), + ] + self.mgr.replace(self.image, body) + expect_body = { + 'memberships': [ + {'member_id': '2', 'can_share': False}, + {'member_id': '3', 'can_share': True}, + ], + } + expect = [('PUT', '/v1/images/1/members', {}, expect_body)] + self.assertEqual(self.api.calls, expect) diff --git a/tests/v1/test_images.py b/tests/v1/test_images.py index f8d904f..8ade8ab 100644 --- a/tests/v1/test_images.py +++ b/tests/v1/test_images.py @@ -57,3 +57,27 @@ class ImageManagerTest(unittest.TestCase): self.assertEqual(self.api.calls, expect) self.assertEqual(image.id, '1') self.assertEqual(image.name, 'image-2') + + +class ImageTest(unittest.TestCase): + def setUp(self): + self.api = utils.FakeAPI() + self.mgr = glanceclient.v1.images.ImageManager(self.api) + + def test_delete(self): + image = self.mgr.get('1') + image.delete() + expect = [ + ('HEAD', '/v1/images/1', {}, None), + ('DELETE', '/v1/images/1', {}, None), + ] + self.assertEqual(self.api.calls, expect) + + def test_update(self): + image = self.mgr.get('1') + image.update(name='image-5') + expect = [ + ('HEAD', '/v1/images/1', {}, None), + ('PUT', '/v1/images/1', {'x-image-meta-name': 'image-5'}, None), + ] + self.assertEqual(self.api.calls, expect) diff --git a/tests/v1/utils.py b/tests/v1/utils.py index 42acd27..6719c35 100644 --- a/tests/v1/utils.py +++ b/tests/v1/utils.py @@ -1,4 +1,17 @@ - +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. fixtures = { '/v1/images': { @@ -39,9 +52,23 @@ fixtures = { }, None), 'DELETE': ({}, None), - } + }, + '/v1/images/1/members': { + 'GET': ( + {}, + {'members': [ + {'member_id': '1', 'can_share': False}, + ]}, + ), + 'PUT': ({}, None), + }, + '/v1/images/1/members/1': { + 'PUT': ({}, None), + 'DELETE': ({}, None), + }, } + class FakeAPI(object): def __init__(self):