diff --git a/glanceclient/v2/client.py b/glanceclient/v2/client.py index 33f4ae5..741a59e 100644 --- a/glanceclient/v2/client.py +++ b/glanceclient/v2/client.py @@ -17,6 +17,7 @@ import warlock from glanceclient.common import http from glanceclient.v2 import images +from glanceclient.v2 import image_members from glanceclient.v2 import schemas @@ -35,7 +36,13 @@ class Client(object): self.schemas = schemas.Controller(self.http_client) self.images = images.Controller(self.http_client, self._get_image_model()) + self.image_members = image_members.Controller(self.http_client, + self._get_member_model()) def _get_image_model(self): schema = self.schemas.get('image') return warlock.model_factory(schema.raw()) + + def _get_member_model(self): + schema = self.schemas.get('member') + return warlock.model_factory(schema.raw()) diff --git a/glanceclient/v2/image_members.py b/glanceclient/v2/image_members.py new file mode 100644 index 0000000..a6a2d7e --- /dev/null +++ b/glanceclient/v2/image_members.py @@ -0,0 +1,47 @@ +# Copyright 2013 OpenStack Foundation +# 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 utils + + +class Controller(object): + def __init__(self, http_client, model): + self.http_client = http_client + self.model = model + + def list(self, image_id): + url = '/v2/images/%s/members' % image_id + resp, body = self.http_client.json_request('GET', url) + for member in body['members']: + yield self.model(member) + + def delete(self, image_id, member_id): + self.http_client.json_request('DELETE', + '/v2/images/%s/members/%s' % (image_id, + member_id)) + + def update(self, image_id, member_id, member_status): + url = '/v2/images/%s/members/%s' % (image_id, member_id) + body = {'status': member_status} + resp, updated_member = self.http_client.json_request('PUT', url, + body=body) + return self.model(updated_member) + + def create(self, image_id, member_id): + url = '/v2/images/%s/members' % image_id + body = {'member': member_id} + resp, created_member = self.http_client.json_request('POST', url, + body=body) + return self.model(created_member) diff --git a/glanceclient/v2/shell.py b/glanceclient/v2/shell.py index 93e633a..f9aa01b 100644 --- a/glanceclient/v2/shell.py +++ b/glanceclient/v2/shell.py @@ -49,6 +49,62 @@ def do_image_show(gc, args): utils.print_dict(image) +@utils.arg('--image-id', metavar='', required=True, + help='Image to display members of.') +def do_member_list(gc, args): + """Describe sharing permissions by image""" + + members = gc.image_members.list(args.image_id) + columns = ['Image ID', 'Member ID', 'Status'] + utils.print_list(members, columns) + + +@utils.arg('image_id', metavar='', + help='Image from which to remove member') +@utils.arg('member_id', metavar='', + help='Tenant to remove as member') +def do_member_delete(gc, args): + """Delete image member""" + if not (args.image_id and args.member_id): + utils.exit('Unable to delete member. Specify image_id and member_id') + else: + gc.image_members.delete(args.image_id, args.member_id) + + +@utils.arg('image_id', metavar='', + help='Image from which to update member') +@utils.arg('member_id', metavar='', + help='Tenant to update') +@utils.arg('member_status', metavar='', + help='Updated status of member') +def do_member_update(gc, args): + """Update the status of a member for a given image.""" + if not (args.image_id and args.member_id and args.member_status): + utils.exit('Unable to update member. Specify image_id, member_id and' + ' member_status') + else: + member = gc.image_members.update(args.image_id, args.member_id, + args.member_status) + member = [member] + columns = ['Image ID', 'Member ID', 'Status'] + utils.print_list(member, columns) + + +@utils.arg('image_id', metavar='', + help='Image on which to create member') +@utils.arg('member_id', metavar='', + help='Tenant to add as member') +def do_member_create(gc, args): + """Create member for a given image.""" + if not (args.image_id and args.member_id): + utils.exit('Unable to create member. Specify image_id and member_id') + else: + member = gc.image_members.create(args.image_id, args.member_id) + member = [member] + columns = ['Image ID', 'Member ID', 'Status'] + utils.print_list(member, columns) + + @utils.arg('model', metavar='', help='Name of model to describe.') def do_explain(gc, args): """Describe a specific model.""" diff --git a/tests/v2/test_members.py b/tests/v2/test_members.py new file mode 100644 index 0000000..43f6530 --- /dev/null +++ b/tests/v2/test_members.py @@ -0,0 +1,112 @@ +# Copyright 2013 OpenStack Foundation +# 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. + +import errno +import testtools + +import warlock + +from glanceclient.v2 import image_members +from tests import utils + + +IMAGE = '3a4560a1-e585-443e-9b39-553b46ec92d1' +MEMBER = '11223344-5566-7788-9911-223344556677' + + +fixtures = { + '/v2/images/{image}/members'.format(image=IMAGE): { + 'GET': ( + {}, + {'members': [ + { + 'image_id': IMAGE, + 'member_id': MEMBER, + }, + ]}, + ), + 'POST': ( + {}, + { + 'image_id': IMAGE, + 'member_id': MEMBER, + 'status': 'pending' + } + ) + }, + '/v2/images/{image}/members/{mem}'.format(image=IMAGE, mem=MEMBER): { + 'DELETE': ( + {}, + None, + ), + 'PUT': ( + {}, + { + 'image_id': IMAGE, + 'member_id': MEMBER, + 'status': 'accepted' + } + ), + }, +} + + +fake_schema = {'name': 'member', 'properties': {'image_id': {}, + 'member_id': {}}} +FakeModel = warlock.model_factory(fake_schema) + + +class TestController(testtools.TestCase): + def setUp(self): + super(TestController, self).setUp() + self.api = utils.FakeAPI(fixtures) + self.controller = image_members.Controller(self.api, FakeModel) + + def test_list_image_members(self): + image_id = IMAGE + #NOTE(iccha): cast to list since the controller returns a generator + image_members = list(self.controller.list(image_id)) + self.assertEqual(image_members[0].image_id, IMAGE) + self.assertEqual(image_members[0].member_id, MEMBER) + + def test_delete_image_member(self): + image_id = IMAGE + member_id = MEMBER + self.controller.delete(image_id, member_id) + expect = [ + ('DELETE', + '/v2/images/{image}/members/{mem}'.format(image=IMAGE, + mem=MEMBER), + {}, + None)] + self.assertEqual(self.api.calls, expect) + + def test_update_image_members(self): + image_id = IMAGE + member_id = MEMBER + status = 'accepted' + image_member = self.controller.update(image_id, member_id, status) + self.assertEqual(image_member.image_id, IMAGE) + self.assertEqual(image_member.member_id, MEMBER) + self.assertEqual(image_member.status, status) + + def test_create_image_members(self): + image_id = IMAGE + member_id = MEMBER + status = 'pending' + image_member = self.controller.create(image_id, member_id) + self.assertEqual(image_member.image_id, IMAGE) + self.assertEqual(image_member.member_id, MEMBER) + self.assertEqual(image_member.status, status)