From f055fe67c11fff020ae959b1672844aaff382491 Mon Sep 17 00:00:00 2001
From: Jordan Pittier <jordan.pittier@scality.com>
Date: Thu, 15 Dec 2016 22:20:30 +0100
Subject: [PATCH] Add support for Glance 'update image members' feature

This patch adds 3 new options to the "image set" command: --accept,
--reject and --pending. This updates the membership status for
an image.

Closes-Bug: 1620481
Change-Id: I13b8c067aad68ece9ff636fbdd83bcb3663c91b2
---
 doc/source/command-objects/image.rst          | 31 ++++++++++++++++
 openstackclient/image/v2/image.py             | 36 ++++++++++++++++++-
 .../tests/functional/image/v2/test_image.py   | 22 ++++++++++++
 .../tests/unit/image/v2/test_image.py         | 33 +++++++++++++++++
 ...ate-image-membership-68221f226ca3b6e0.yaml |  4 +++
 5 files changed, 125 insertions(+), 1 deletion(-)
 create mode 100644 releasenotes/notes/image-set-to-update-image-membership-68221f226ca3b6e0.yaml

diff --git a/doc/source/command-objects/image.rst b/doc/source/command-objects/image.rst
index 7ebcb54ad2..999842af21 100644
--- a/doc/source/command-objects/image.rst
+++ b/doc/source/command-objects/image.rst
@@ -325,6 +325,7 @@ Set image properties
         [--ramdisk-id <ramdisk-id>]
         [--activate|--deactivate]
         [--project <project> [--project-domain <project-domain>]]
+        [--accept | --reject | --pending]
         <image>
 
 .. option:: --name <name>
@@ -490,6 +491,36 @@ Set image properties
 
     .. versionadded:: 2
 
+.. option:: --accept
+
+    Accept the image membership.
+
+    If `--project` is passed, this will update the membership status for the
+    given project, otherwise `--project` will default to the project the user
+    is authenticated to.
+
+    .. versionadded:: 2
+
+.. option:: --reject
+
+    Reject the image membership.
+
+    If `--project` is passed, this will update the membership status for the
+    given project, otherwise `--project` will default to the project the user
+    is authenticated to.
+
+    .. versionadded:: 2
+
+.. option:: --pending
+
+    Reset the image membership to 'pending'.
+
+    If `--project` is passed, this will update the membership status for the
+    given project, otherwise `--project` will default to the project the user
+    is authenticated to.
+
+    .. versionadded:: 2
+
 .. _image_set-image:
 .. describe:: <image>
 
diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py
index 55eb7eb104..418cc3973f 100644
--- a/openstackclient/image/v2/image.py
+++ b/openstackclient/image/v2/image.py
@@ -777,6 +777,23 @@ class SetImage(command.Command):
                 dest=deadopt.replace('-', '_'),
                 help=argparse.SUPPRESS,
             )
+
+        membership_group = parser.add_mutually_exclusive_group()
+        membership_group.add_argument(
+            "--accept",
+            action="store_true",
+            help=_("Accept the image membership"),
+        )
+        membership_group.add_argument(
+            "--reject",
+            action="store_true",
+            help=_("Reject the image membership"),
+        )
+        membership_group.add_argument(
+            "--pending",
+            action="store_true",
+            help=_("Reset the image membership to 'pending'"),
+        )
         return parser
 
     def take_action(self, parsed_args):
@@ -828,12 +845,14 @@ class SetImage(command.Command):
             project_arg = parsed_args.owner
             LOG.warning(_('The --owner option is deprecated, '
                           'please use --project instead.'))
+        project_id = None
         if project_arg:
-            kwargs['owner'] = common.find_project(
+            project_id = common.find_project(
                 identity_client,
                 project_arg,
                 parsed_args.project_domain,
             ).id
+            kwargs['owner'] = project_id
 
         image = utils.find_resource(
             image_client.images, parsed_args.image)
@@ -846,6 +865,21 @@ class SetImage(command.Command):
             image_client.images.reactivate(image.id)
             activation_status = "activated"
 
+        membership_group_args = ('accept', 'reject', 'pending')
+        membership_status = [status for status in membership_group_args
+                             if getattr(parsed_args, status)]
+        if membership_status:
+            # If a specific project is not passed, assume we want to update
+            # our own membership
+            if not project_id:
+                project_id = self.app.client_manager.auth_ref.project_id
+            # The mutually exclusive group of the arg parser ensure we have at
+            # most one item in the membership_status list.
+            if membership_status[0] != 'pending':
+                membership_status[0] += 'ed'  # Glance expects the past form
+            image_client.image_members.update(
+                image.id, project_id, membership_status[0])
+
         if parsed_args.tags:
             # Tags should be extended, but duplicates removed
             kwargs['tags'] = list(set(image.tags).union(set(parsed_args.tags)))
diff --git a/openstackclient/tests/functional/image/v2/test_image.py b/openstackclient/tests/functional/image/v2/test_image.py
index 3f432b02ee..6faff94a32 100644
--- a/openstackclient/tests/functional/image/v2/test_image.py
+++ b/openstackclient/tests/functional/image/v2/test_image.py
@@ -74,3 +74,25 @@ class ImageTests(base.TestCase):
         self.openstack('image unset --property a --property c ' + self.NAME)
         raw_output = self.openstack('image show ' + self.NAME + opts)
         self.assertEqual(self.NAME + "\n\n", raw_output)
+
+    def test_image_members(self):
+        opts = self.get_opts(['project_id'])
+        my_project_id = self.openstack('token issue' + opts).strip()
+        self.openstack(
+            'image add project {} {}'.format(self.NAME, my_project_id))
+
+        self.openstack(
+            'image set --accept ' + self.NAME)
+        shared_img_list = self.parse_listing(
+            self.openstack('image list --shared', self.get_opts(['name']))
+        )
+        self.assertIn(self.NAME, [img['Name'] for img in shared_img_list])
+
+        self.openstack(
+            'image set --reject ' + self.NAME)
+        shared_img_list = self.parse_listing(
+            self.openstack('image list --shared', self.get_opts(['name']))
+        )
+
+        self.openstack(
+            'image remove project {} {}'.format(self.NAME, my_project_id))
diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py
index a054e513d9..a15131190f 100644
--- a/openstackclient/tests/unit/image/v2/test_image.py
+++ b/openstackclient/tests/unit/image/v2/test_image.py
@@ -845,6 +845,39 @@ class TestImageSet(TestImage):
 
         self.assertIsNone(result)
 
+        self.image_members_mock.update.assert_not_called()
+
+    def test_image_set_membership_option(self):
+        membership = image_fakes.FakeImage.create_one_image_member(
+            attrs={'image_id': image_fakes.image_id,
+                   'member_id': self.project.id}
+        )
+        self.image_members_mock.update.return_value = membership
+
+        for status in ('accept', 'reject', 'pending'):
+            arglist = [
+                '--%s' % status,
+                image_fakes.image_id,
+            ]
+            verifylist = [
+                (status, True),
+                ('image', image_fakes.image_id)
+            ]
+
+            parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+            self.cmd.take_action(parsed_args)
+
+            self.image_members_mock.update.assert_called_once_with(
+                image_fakes.image_id,
+                self.app.client_manager.auth_ref.project_id,
+                status if status == 'pending' else status + 'ed'
+            )
+            self.image_members_mock.update.reset_mock()
+
+        # Assert that the 'update image" route is also called, in addition to
+        # the 'update membership' route.
+        self.images_mock.update.assert_called_with(image_fakes.image_id)
+
     def test_image_set_options(self):
         arglist = [
             '--name', 'new-name',
diff --git a/releasenotes/notes/image-set-to-update-image-membership-68221f226ca3b6e0.yaml b/releasenotes/notes/image-set-to-update-image-membership-68221f226ca3b6e0.yaml
new file mode 100644
index 0000000000..599216c47b
--- /dev/null
+++ b/releasenotes/notes/image-set-to-update-image-membership-68221f226ca3b6e0.yaml
@@ -0,0 +1,4 @@
+---
+features:
+  - Add support to update image membership with the `--accept`,
+    ``--reject`` and ``--pending`` options of the ``image set command``.