Skip to content

Commit 7bb4598

Browse files
chengkunyeSteve Martinelli
authored andcommitted
add image member commands for image API
This commit adds the following commands: image project add image project remove Closes-Bug: 1402420 Change-Id: I07954e9fa43a3ad6078dd939ecedf9f038299e7b
1 parent 1af89f7 commit 7bb4598

5 files changed

Lines changed: 306 additions & 0 deletions

File tree

doc/source/command-objects/image.rst

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,57 @@ Display image details
322322
.. describe:: <image>
323323
324324
Image to display (name or ID)
325+
326+
image add project
327+
-----------------
328+
329+
*Only supported for Image v2*
330+
331+
Associate project with image
332+
333+
.. progran:: image add project
334+
.. code:: bash
335+
336+
os image add project
337+
[--project-domain <project-domain>]
338+
<image> <project>
339+
340+
.. option:: --project-domain <project-domain>
341+
342+
Domain the project belongs to (name or ID).
343+
This can be used in case collisions between project names exist.
344+
345+
.. describe:: <image>
346+
347+
Image to share (name or ID).
348+
349+
.. describe:: <project>
350+
351+
Project to associate with image (name or ID)
352+
353+
image remove project
354+
--------------------
355+
356+
*Only supported for Image v2*
357+
358+
Disassociate project with image
359+
360+
.. progran:: image remove project
361+
.. code:: bash
362+
363+
os image remove remove
364+
[--project-domain <project-domain>]
365+
<image> <project>
366+
367+
.. option:: --project-domain <project-domain>
368+
369+
Domain the project belongs to (name or ID).
370+
This can be used in case collisions between project names exist.
371+
372+
.. describe:: <image>
373+
374+
Image to unshare (name or ID).
375+
376+
.. describe:: <project>
377+
378+
Project to disassociate with image (name or ID)

openstackclient/image/v2/image.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,49 @@
2727
from openstackclient.api import utils as api_utils
2828
from openstackclient.common import parseractions
2929
from openstackclient.common import utils
30+
from openstackclient.identity import common
31+
32+
33+
class AddProjectToImage(show.ShowOne):
34+
"""Associate project with image"""
35+
36+
log = logging.getLogger(__name__ + ".AddProjectToImage")
37+
38+
def get_parser(self, prog_name):
39+
parser = super(AddProjectToImage, self).get_parser(prog_name)
40+
parser.add_argument(
41+
"image",
42+
metavar="<image>",
43+
help="Image to share (name or ID)",
44+
)
45+
parser.add_argument(
46+
"project",
47+
metavar="<project>",
48+
help="Project to associate with image (name or ID)",
49+
)
50+
common.add_project_domain_option_to_parser(parser)
51+
return parser
52+
53+
def take_action(self, parsed_args):
54+
self.log.debug("take_action(%s)", parsed_args)
55+
56+
image_client = self.app.client_manager.image
57+
identity_client = self.app.client_manager.identity
58+
59+
project_id = common.find_project(identity_client,
60+
parsed_args.project,
61+
parsed_args.project_domain).id
62+
63+
image_id = utils.find_resource(
64+
image_client.images,
65+
parsed_args.image).id
66+
67+
image_member = image_client.image_members.create(
68+
image_id,
69+
project_id,
70+
)
71+
72+
return zip(*sorted(six.iteritems(image_member._info)))
3073

3174

3275
class DeleteImage(command.Command):
@@ -192,6 +235,43 @@ def take_action(self, parsed_args):
192235
)
193236

194237

238+
class RemoveProjectImage(command.Command):
239+
"""Disassociate project with image"""
240+
241+
log = logging.getLogger(__name__ + ".RemoveProjectImage")
242+
243+
def get_parser(self, prog_name):
244+
parser = super(RemoveProjectImage, self).get_parser(prog_name)
245+
parser.add_argument(
246+
"image",
247+
metavar="<image>",
248+
help="Image to unshare (name or ID)",
249+
)
250+
parser.add_argument(
251+
"project",
252+
metavar="<project>",
253+
help="Project to disassociate with image (name or ID)",
254+
)
255+
common.add_project_domain_option_to_parser(parser)
256+
return parser
257+
258+
def take_action(self, parsed_args):
259+
self.log.debug("take_action(%s)", parsed_args)
260+
261+
image_client = self.app.client_manager.image
262+
identity_client = self.app.client_manager.identity
263+
264+
project_id = common.find_project(identity_client,
265+
parsed_args.project,
266+
parsed_args.project_domain).id
267+
268+
image_id = utils.find_resource(
269+
image_client.images,
270+
parsed_args.image).id
271+
272+
image_client.image_members.delete(image_id, project_id)
273+
274+
195275
class SaveImage(command.Command):
196276
"""Save an image locally"""
197277

openstackclient/tests/image/v2/fakes.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from openstackclient.tests import fakes
1919
from openstackclient.tests import utils
2020

21+
from openstackclient.tests.identity.v3 import fakes as identity_fakes
2122

2223
image_id = '0f41529e-7c12-4de8-be2d-181abb825b3c'
2324
image_name = 'graven'
@@ -36,6 +37,13 @@
3637
IMAGE_columns = tuple(sorted(IMAGE))
3738
IMAGE_data = tuple((IMAGE[x] for x in sorted(IMAGE)))
3839

40+
member_status = 'pending'
41+
MEMBER = {
42+
'member_id': identity_fakes.project_id,
43+
'image_id': image_id,
44+
'status': member_status,
45+
}
46+
3947
# Just enough v2 schema to do some testing
4048
IMAGE_schema = {
4149
"additionalProperties": {
@@ -125,6 +133,8 @@ class FakeImagev2Client(object):
125133
def __init__(self, **kwargs):
126134
self.images = mock.Mock()
127135
self.images.resource_class = fakes.FakeResource(None, {})
136+
self.image_members = mock.Mock()
137+
self.image_members.resource_class = fakes.FakeResource(None, {})
128138
self.auth_token = kwargs['token']
129139
self.management_url = kwargs['endpoint']
130140

@@ -137,3 +147,8 @@ def setUp(self):
137147
endpoint=fakes.AUTH_URL,
138148
token=fakes.AUTH_TOKEN,
139149
)
150+
151+
self.app.client_manager.identity = identity_fakes.FakeIdentityv3Client(
152+
endpoint=fakes.AUTH_URL,
153+
token=fakes.AUTH_TOKEN,
154+
)

openstackclient/tests/image/v2/test_image.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from glanceclient.v2 import schemas
2222
from openstackclient.image.v2 import image
2323
from openstackclient.tests import fakes
24+
from openstackclient.tests.identity.v3 import fakes as identity_fakes
2425
from openstackclient.tests.image.v2 import fakes as image_fakes
2526

2627

@@ -32,6 +33,96 @@ def setUp(self):
3233
# Get a shortcut to the ServerManager Mock
3334
self.images_mock = self.app.client_manager.image.images
3435
self.images_mock.reset_mock()
36+
self.image_members_mock = self.app.client_manager.image.image_members
37+
self.image_members_mock.reset_mock()
38+
self.project_mock = self.app.client_manager.identity.projects
39+
self.project_mock.reset_mock()
40+
self.domain_mock = self.app.client_manager.identity.domains
41+
self.domain_mock.reset_mock()
42+
43+
44+
class TestAddProjectToImage(TestImage):
45+
46+
def setUp(self):
47+
super(TestAddProjectToImage, self).setUp()
48+
49+
# This is the return value for utils.find_resource()
50+
self.images_mock.get.return_value = fakes.FakeResource(
51+
None,
52+
copy.deepcopy(image_fakes.IMAGE),
53+
loaded=True,
54+
)
55+
self.image_members_mock.create.return_value = fakes.FakeResource(
56+
None,
57+
copy.deepcopy(image_fakes.MEMBER),
58+
loaded=True,
59+
)
60+
self.project_mock.get.return_value = fakes.FakeResource(
61+
None,
62+
copy.deepcopy(identity_fakes.PROJECT),
63+
loaded=True,
64+
)
65+
self.domain_mock.get.return_value = fakes.FakeResource(
66+
None,
67+
copy.deepcopy(identity_fakes.DOMAIN),
68+
loaded=True,
69+
)
70+
# Get the command object to test
71+
self.cmd = image.AddProjectToImage(self.app, None)
72+
73+
def test_add_project_to_image_no_option(self):
74+
arglist = [
75+
image_fakes.image_id,
76+
identity_fakes.project_id,
77+
]
78+
verifylist = [
79+
('image', image_fakes.image_id),
80+
('project', identity_fakes.project_id),
81+
]
82+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
83+
84+
# DisplayCommandBase.take_action() returns two tuples
85+
columns, data = self.cmd.take_action(parsed_args)
86+
self.image_members_mock.create.assert_called_with(
87+
image_fakes.image_id,
88+
identity_fakes.project_id
89+
)
90+
collist = ('image_id', 'member_id', 'status')
91+
self.assertEqual(collist, columns)
92+
datalist = (
93+
image_fakes.image_id,
94+
identity_fakes.project_id,
95+
image_fakes.member_status
96+
)
97+
self.assertEqual(datalist, data)
98+
99+
def test_add_project_to_image_with_option(self):
100+
arglist = [
101+
image_fakes.image_id,
102+
identity_fakes.project_id,
103+
'--project-domain', identity_fakes.domain_id,
104+
]
105+
verifylist = [
106+
('image', image_fakes.image_id),
107+
('project', identity_fakes.project_id),
108+
('project_domain', identity_fakes.domain_id),
109+
]
110+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
111+
112+
# DisplayCommandBase.take_action() returns two tuples
113+
columns, data = self.cmd.take_action(parsed_args)
114+
self.image_members_mock.create.assert_called_with(
115+
image_fakes.image_id,
116+
identity_fakes.project_id
117+
)
118+
collist = ('image_id', 'member_id', 'status')
119+
self.assertEqual(collist, columns)
120+
datalist = (
121+
image_fakes.image_id,
122+
identity_fakes.project_id,
123+
image_fakes.member_status
124+
)
125+
self.assertEqual(datalist, data)
35126

36127

37128
class TestImageDelete(TestImage):
@@ -298,6 +389,70 @@ def test_image_list_sort_option(self, si_mock):
298389
self.assertEqual(datalist, tuple(data))
299390

300391

392+
class TestRemoveProjectImage(TestImage):
393+
394+
def setUp(self):
395+
super(TestRemoveProjectImage, self).setUp()
396+
397+
# This is the return value for utils.find_resource()
398+
self.images_mock.get.return_value = fakes.FakeResource(
399+
None,
400+
copy.deepcopy(image_fakes.IMAGE),
401+
loaded=True,
402+
)
403+
self.project_mock.get.return_value = fakes.FakeResource(
404+
None,
405+
copy.deepcopy(identity_fakes.PROJECT),
406+
loaded=True,
407+
)
408+
self.domain_mock.get.return_value = fakes.FakeResource(
409+
None,
410+
copy.deepcopy(identity_fakes.DOMAIN),
411+
loaded=True,
412+
)
413+
self.image_members_mock.delete.return_value = None
414+
# Get the command object to test
415+
self.cmd = image.RemoveProjectImage(self.app, None)
416+
417+
def test_remove_project_image_no_options(self):
418+
arglist = [
419+
image_fakes.image_id,
420+
identity_fakes.project_id,
421+
]
422+
verifylist = [
423+
('image', image_fakes.image_id),
424+
('project', identity_fakes.project_id),
425+
]
426+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
427+
428+
# DisplayCommandBase.take_action() returns two tuples
429+
self.cmd.take_action(parsed_args)
430+
self.image_members_mock.delete.assert_called_with(
431+
image_fakes.image_id,
432+
identity_fakes.project_id,
433+
)
434+
435+
def test_remove_project_image_with_options(self):
436+
arglist = [
437+
image_fakes.image_id,
438+
identity_fakes.project_id,
439+
'--project-domain', identity_fakes.domain_id,
440+
]
441+
verifylist = [
442+
('image', image_fakes.image_id),
443+
('project', identity_fakes.project_id),
444+
('project_domain', identity_fakes.domain_id),
445+
]
446+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
447+
448+
# DisplayCommandBase.take_action() returns two tuples
449+
self.cmd.take_action(parsed_args)
450+
self.image_members_mock.delete.assert_called_with(
451+
image_fakes.image_id,
452+
identity_fakes.project_id,
453+
)
454+
455+
301456
class TestImageShow(TestImage):
302457

303458
def setUp(self):

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,10 @@ openstack.image.v1 =
314314
image_show = openstackclient.image.v1.image:ShowImage
315315

316316
openstack.image.v2 =
317+
image_add_project = openstackclient.image.v2.image:AddProjectToImage
317318
image_delete = openstackclient.image.v2.image:DeleteImage
318319
image_list = openstackclient.image.v2.image:ListImage
320+
image_remove_project = openstackclient.image.v2.image:RemoveProjectImage
319321
image_save = openstackclient.image.v2.image:SaveImage
320322
image_show = openstackclient.image.v2.image:ShowImage
321323
image_set = openstackclient.image.v2.image:SetImage

0 commit comments

Comments
 (0)