From 601d9717c2a5c125912ca27e43259c48e548939b Mon Sep 17 00:00:00 2001
From: Stephen Finucane <stephenfin@redhat.com>
Date: Wed, 7 Jun 2023 10:32:23 +0100
Subject: [PATCH] Remove project purge image commands

This was still using glanceclient-style calls despite us having switched
all image command to SDK way back in 5.2.0 (Ussuri). It therefore hasn't
worked since then, so we remove without deprecation.

Change-Id: Ibea79ea10bb272cba194debfe4bcbaeebf3875b7
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
---
 .../cli/command-objects/project-purge.rst     |  11 -
 doc/source/cli/commands.rst                   |   1 -
 openstackclient/common/project_purge.py       | 181 ---------
 .../tests/unit/common/test_project_purge.py   | 364 ------------------
 ...remove-project-purge-d372374b1a7c4641.yaml |   8 +
 setup.cfg                                     |   1 -
 6 files changed, 8 insertions(+), 558 deletions(-)
 delete mode 100644 doc/source/cli/command-objects/project-purge.rst
 delete mode 100644 openstackclient/common/project_purge.py
 delete mode 100644 openstackclient/tests/unit/common/test_project_purge.py
 create mode 100644 releasenotes/notes/remove-project-purge-d372374b1a7c4641.yaml

diff --git a/doc/source/cli/command-objects/project-purge.rst b/doc/source/cli/command-objects/project-purge.rst
deleted file mode 100644
index 8f10a77452..0000000000
--- a/doc/source/cli/command-objects/project-purge.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-=============
-project purge
-=============
-
-Clean resources associated with a specific project.
-
-Block Storage v1, v2; Compute v2; Image v1, v2
-
-
-.. autoprogram-cliff:: openstack.common
-   :command: project purge
diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst
index d789eceb51..8e1c9a6c0e 100644
--- a/doc/source/cli/commands.rst
+++ b/doc/source/cli/commands.rst
@@ -276,7 +276,6 @@ Those actions with an opposite action are noted in parens if applicable.
   live server migration if possible
 * ``pause`` (``unpause``) - stop one or more servers and leave them in memory
 * ``query`` - Query resources by Elasticsearch query string or json format DSL.
-* ``purge`` - clean resources associated with a specific project
 * ``cleanup`` - flexible clean resources associated with a specific project
 * ``reboot`` - forcibly reboot a server
 * ``rebuild`` - rebuild a server using (most of) the same arguments as in the original create
diff --git a/openstackclient/common/project_purge.py b/openstackclient/common/project_purge.py
deleted file mode 100644
index 11cf0076d2..0000000000
--- a/openstackclient/common/project_purge.py
+++ /dev/null
@@ -1,181 +0,0 @@
-#   Copyright 2012 OpenStack Foundation
-#
-#   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 logging
-
-from osc_lib.command import command
-from osc_lib import utils
-
-from openstackclient.i18n import _
-from openstackclient.identity import common as identity_common
-
-
-LOG = logging.getLogger(__name__)
-
-
-class ProjectPurge(command.Command):
-    _description = _("Clean resources associated with a project")
-
-    def get_parser(self, prog_name):
-        parser = super(ProjectPurge, self).get_parser(prog_name)
-        parser.add_argument(
-            '--dry-run',
-            action='store_true',
-            help=_("List a project's resources"),
-        )
-        parser.add_argument(
-            '--keep-project',
-            action='store_true',
-            help=_("Clean project resources, but don't delete the project"),
-        )
-        project_group = parser.add_mutually_exclusive_group(required=True)
-        project_group.add_argument(
-            '--auth-project',
-            action='store_true',
-            help=_('Delete resources of the project used to authenticate'),
-        )
-        project_group.add_argument(
-            '--project',
-            metavar='<project>',
-            help=_('Project to clean (name or ID)'),
-        )
-        identity_common.add_project_domain_option_to_parser(parser)
-        return parser
-
-    def take_action(self, parsed_args):
-        identity_client = self.app.client_manager.identity
-
-        if parsed_args.auth_project:
-            project_id = self.app.client_manager.auth_ref.project_id
-        elif parsed_args.project:
-            try:
-                project_id = identity_common.find_project(
-                    identity_client,
-                    parsed_args.project,
-                    parsed_args.project_domain,
-                ).id
-            except AttributeError:  # using v2 auth and supplying a domain
-                project_id = utils.find_resource(
-                    identity_client.tenants,
-                    parsed_args.project,
-                ).id
-
-        # delete all non-identity resources
-        self.delete_resources(parsed_args.dry_run, project_id)
-
-        # clean up the project
-        if not parsed_args.keep_project:
-            LOG.warning(_('Deleting project: %s'), project_id)
-            if not parsed_args.dry_run:
-                identity_client.projects.delete(project_id)
-
-    def delete_resources(self, dry_run, project_id):
-        # servers
-        try:
-            compute_client = self.app.client_manager.compute
-            search_opts = {'tenant_id': project_id, 'all_tenants': True}
-            data = compute_client.servers.list(search_opts=search_opts)
-            self.delete_objects(
-                compute_client.servers.delete, data, 'server', dry_run
-            )
-        except Exception:
-            pass
-
-        # images
-        try:
-            image_client = self.app.client_manager.image
-            api_version = int(image_client.version)
-            if api_version == 1:
-                data = image_client.images.list(owner=project_id)
-            elif api_version == 2:
-                kwargs = {'filters': {'owner': project_id}}
-                data = image_client.images.list(**kwargs)
-            else:
-                raise NotImplementedError
-            self.delete_objects(
-                image_client.images.delete, data, 'image', dry_run
-            )
-        except Exception:
-            pass
-
-        # volumes, snapshots, backups
-        volume_client = self.app.client_manager.volume
-        search_opts = {'project_id': project_id, 'all_tenants': True}
-        try:
-            data = volume_client.volume_snapshots.list(search_opts=search_opts)
-            self.delete_objects(
-                self.delete_one_volume_snapshot,
-                data,
-                'volume snapshot',
-                dry_run,
-            )
-        except Exception:
-            pass
-        try:
-            data = volume_client.backups.list(search_opts=search_opts)
-            self.delete_objects(
-                self.delete_one_volume_backup, data, 'volume backup', dry_run
-            )
-        except Exception:
-            pass
-        try:
-            data = volume_client.volumes.list(search_opts=search_opts)
-            self.delete_objects(
-                volume_client.volumes.force_delete, data, 'volume', dry_run
-            )
-        except Exception:
-            pass
-
-    def delete_objects(self, func_delete, data, resource, dry_run):
-        result = 0
-        for i in data:
-            LOG.warning(
-                _('Deleting %(resource)s : %(id)s')
-                % {'resource': resource, 'id': i.id}
-            )
-            if not dry_run:
-                try:
-                    func_delete(i.id)
-                except Exception as e:
-                    result += 1
-                    LOG.error(
-                        _(
-                            "Failed to delete %(resource)s with "
-                            "ID '%(id)s': %(e)s"
-                        )
-                        % {'resource': resource, 'id': i.id, 'e': e}
-                    )
-        if result > 0:
-            total = len(data)
-            msg = _(
-                "%(result)s of %(total)s %(resource)ss failed " "to delete."
-            ) % {'result': result, 'total': total, 'resource': resource}
-            LOG.error(msg)
-
-    def delete_one_volume_snapshot(self, snapshot_id):
-        volume_client = self.app.client_manager.volume
-        try:
-            volume_client.volume_snapshots.delete(snapshot_id)
-        except Exception:
-            # Only volume v2 support deleting by force
-            volume_client.volume_snapshots.delete(snapshot_id, force=True)
-
-    def delete_one_volume_backup(self, backup_id):
-        volume_client = self.app.client_manager.volume
-        try:
-            volume_client.backups.delete(backup_id)
-        except Exception:
-            # Only volume v2 support deleting by force
-            volume_client.backups.delete(backup_id, force=True)
diff --git a/openstackclient/tests/unit/common/test_project_purge.py b/openstackclient/tests/unit/common/test_project_purge.py
deleted file mode 100644
index 850adb5bbc..0000000000
--- a/openstackclient/tests/unit/common/test_project_purge.py
+++ /dev/null
@@ -1,364 +0,0 @@
-#   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 unittest import mock
-
-from osc_lib import exceptions
-
-from openstackclient.common import project_purge
-from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes
-from openstackclient.tests.unit import fakes
-from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
-from openstackclient.tests.unit.image.v2 import fakes as image_fakes
-from openstackclient.tests.unit import utils as tests_utils
-from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes
-
-
-class TestProjectPurgeInit(tests_utils.TestCommand):
-    def setUp(self):
-        super().setUp()
-        compute_client = compute_fakes.FakeComputev2Client(
-            endpoint=fakes.AUTH_URL,
-            token=fakes.AUTH_TOKEN,
-        )
-        self.app.client_manager.compute = compute_client
-        self.servers_mock = compute_client.servers
-        self.servers_mock.reset_mock()
-
-        volume_client = volume_fakes.FakeVolumeClient(
-            endpoint=fakes.AUTH_URL,
-            token=fakes.AUTH_TOKEN,
-        )
-        self.app.client_manager.volume = volume_client
-        self.volumes_mock = volume_client.volumes
-        self.volumes_mock.reset_mock()
-        self.snapshots_mock = volume_client.volume_snapshots
-        self.snapshots_mock.reset_mock()
-        self.backups_mock = volume_client.backups
-        self.backups_mock.reset_mock()
-
-        identity_client = identity_fakes.FakeIdentityv3Client(
-            endpoint=fakes.AUTH_URL,
-            token=fakes.AUTH_TOKEN,
-        )
-        self.app.client_manager.identity = identity_client
-        self.domains_mock = identity_client.domains
-        self.domains_mock.reset_mock()
-        self.projects_mock = identity_client.projects
-        self.projects_mock.reset_mock()
-
-        image_client = image_fakes.FakeImagev2Client(
-            endpoint=fakes.AUTH_URL,
-            token=fakes.AUTH_TOKEN,
-        )
-        self.app.client_manager.image = image_client
-        self.images_mock = image_client.images
-        self.images_mock.reset_mock()
-
-
-class TestProjectPurge(TestProjectPurgeInit):
-    project = identity_fakes.FakeProject.create_one_project()
-    server = compute_fakes.create_one_server()
-    image = image_fakes.create_one_image()
-    volume = volume_fakes.create_one_volume()
-    backup = volume_fakes.create_one_backup()
-    snapshot = volume_fakes.create_one_snapshot()
-
-    def setUp(self):
-        super().setUp()
-        self.projects_mock.get.return_value = self.project
-        self.projects_mock.delete.return_value = None
-        self.images_mock.list.return_value = [self.image]
-        self.images_mock.delete.return_value = None
-        self.servers_mock.list.return_value = [self.server]
-        self.servers_mock.delete.return_value = None
-        self.volumes_mock.list.return_value = [self.volume]
-        self.volumes_mock.delete.return_value = None
-        self.volumes_mock.force_delete.return_value = None
-        self.snapshots_mock.list.return_value = [self.snapshot]
-        self.snapshots_mock.delete.return_value = None
-        self.backups_mock.list.return_value = [self.backup]
-        self.backups_mock.delete.return_value = None
-
-        self.cmd = project_purge.ProjectPurge(self.app, None)
-
-    def test_project_no_options(self):
-        arglist = []
-        verifylist = []
-
-        self.assertRaises(
-            tests_utils.ParserException,
-            self.check_parser,
-            self.cmd,
-            arglist,
-            verifylist,
-        )
-
-    def test_project_purge_with_project(self):
-        arglist = [
-            '--project',
-            self.project.id,
-        ]
-        verifylist = [
-            ('dry_run', False),
-            ('keep_project', False),
-            ('auth_project', False),
-            ('project', self.project.id),
-            ('project_domain', None),
-        ]
-        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-
-        result = self.cmd.take_action(parsed_args)
-        self.projects_mock.get.assert_called_once_with(self.project.id)
-        self.projects_mock.delete.assert_called_once_with(self.project.id)
-        self.servers_mock.list.assert_called_once_with(
-            search_opts={'tenant_id': self.project.id, 'all_tenants': True}
-        )
-        kwargs = {'filters': {'owner': self.project.id}}
-        self.images_mock.list.assert_called_once_with(**kwargs)
-        volume_search_opts = {
-            'project_id': self.project.id,
-            'all_tenants': True,
-        }
-        self.volumes_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.snapshots_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.backups_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.servers_mock.delete.assert_called_once_with(self.server.id)
-        self.images_mock.delete.assert_called_once_with(self.image.id)
-        self.volumes_mock.force_delete.assert_called_once_with(self.volume.id)
-        self.snapshots_mock.delete.assert_called_once_with(self.snapshot.id)
-        self.backups_mock.delete.assert_called_once_with(self.backup.id)
-        self.assertIsNone(result)
-
-    def test_project_purge_with_dry_run(self):
-        arglist = [
-            '--dry-run',
-            '--project',
-            self.project.id,
-        ]
-        verifylist = [
-            ('dry_run', True),
-            ('keep_project', False),
-            ('auth_project', False),
-            ('project', self.project.id),
-            ('project_domain', None),
-        ]
-        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-
-        result = self.cmd.take_action(parsed_args)
-        self.projects_mock.get.assert_called_once_with(self.project.id)
-        self.projects_mock.delete.assert_not_called()
-        self.servers_mock.list.assert_called_once_with(
-            search_opts={'tenant_id': self.project.id, 'all_tenants': True}
-        )
-        kwargs = {'filters': {'owner': self.project.id}}
-        self.images_mock.list.assert_called_once_with(**kwargs)
-        volume_search_opts = {
-            'project_id': self.project.id,
-            'all_tenants': True,
-        }
-        self.volumes_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.snapshots_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.backups_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.servers_mock.delete.assert_not_called()
-        self.images_mock.delete.assert_not_called()
-        self.volumes_mock.force_delete.assert_not_called()
-        self.snapshots_mock.delete.assert_not_called()
-        self.backups_mock.delete.assert_not_called()
-        self.assertIsNone(result)
-
-    def test_project_purge_with_keep_project(self):
-        arglist = [
-            '--keep-project',
-            '--project',
-            self.project.id,
-        ]
-        verifylist = [
-            ('dry_run', False),
-            ('keep_project', True),
-            ('auth_project', False),
-            ('project', self.project.id),
-            ('project_domain', None),
-        ]
-        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-
-        result = self.cmd.take_action(parsed_args)
-        self.projects_mock.get.assert_called_once_with(self.project.id)
-        self.projects_mock.delete.assert_not_called()
-        self.servers_mock.list.assert_called_once_with(
-            search_opts={'tenant_id': self.project.id, 'all_tenants': True}
-        )
-        kwargs = {'filters': {'owner': self.project.id}}
-        self.images_mock.list.assert_called_once_with(**kwargs)
-        volume_search_opts = {
-            'project_id': self.project.id,
-            'all_tenants': True,
-        }
-        self.volumes_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.snapshots_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.backups_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.servers_mock.delete.assert_called_once_with(self.server.id)
-        self.images_mock.delete.assert_called_once_with(self.image.id)
-        self.volumes_mock.force_delete.assert_called_once_with(self.volume.id)
-        self.snapshots_mock.delete.assert_called_once_with(self.snapshot.id)
-        self.backups_mock.delete.assert_called_once_with(self.backup.id)
-        self.assertIsNone(result)
-
-    def test_project_purge_with_auth_project(self):
-        self.app.client_manager.auth_ref = mock.Mock()
-        self.app.client_manager.auth_ref.project_id = self.project.id
-        arglist = [
-            '--auth-project',
-        ]
-        verifylist = [
-            ('dry_run', False),
-            ('keep_project', False),
-            ('auth_project', True),
-            ('project', None),
-            ('project_domain', None),
-        ]
-        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-
-        result = self.cmd.take_action(parsed_args)
-        self.projects_mock.get.assert_not_called()
-        self.projects_mock.delete.assert_called_once_with(self.project.id)
-        self.servers_mock.list.assert_called_once_with(
-            search_opts={'tenant_id': self.project.id, 'all_tenants': True}
-        )
-        kwargs = {'filters': {'owner': self.project.id}}
-        self.images_mock.list.assert_called_once_with(**kwargs)
-        volume_search_opts = {
-            'project_id': self.project.id,
-            'all_tenants': True,
-        }
-        self.volumes_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.snapshots_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.backups_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.servers_mock.delete.assert_called_once_with(self.server.id)
-        self.images_mock.delete.assert_called_once_with(self.image.id)
-        self.volumes_mock.force_delete.assert_called_once_with(self.volume.id)
-        self.snapshots_mock.delete.assert_called_once_with(self.snapshot.id)
-        self.backups_mock.delete.assert_called_once_with(self.backup.id)
-        self.assertIsNone(result)
-
-    @mock.patch.object(project_purge.LOG, 'error')
-    def test_project_purge_with_exception(self, mock_error):
-        self.servers_mock.delete.side_effect = exceptions.CommandError()
-        arglist = [
-            '--project',
-            self.project.id,
-        ]
-        verifylist = [
-            ('dry_run', False),
-            ('keep_project', False),
-            ('auth_project', False),
-            ('project', self.project.id),
-            ('project_domain', None),
-        ]
-        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-
-        result = self.cmd.take_action(parsed_args)
-        self.projects_mock.get.assert_called_once_with(self.project.id)
-        self.projects_mock.delete.assert_called_once_with(self.project.id)
-        self.servers_mock.list.assert_called_once_with(
-            search_opts={'tenant_id': self.project.id, 'all_tenants': True}
-        )
-        kwargs = {'filters': {'owner': self.project.id}}
-        self.images_mock.list.assert_called_once_with(**kwargs)
-        volume_search_opts = {
-            'project_id': self.project.id,
-            'all_tenants': True,
-        }
-        self.volumes_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.snapshots_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.backups_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.servers_mock.delete.assert_called_once_with(self.server.id)
-        self.images_mock.delete.assert_called_once_with(self.image.id)
-        self.volumes_mock.force_delete.assert_called_once_with(self.volume.id)
-        self.snapshots_mock.delete.assert_called_once_with(self.snapshot.id)
-        self.backups_mock.delete.assert_called_once_with(self.backup.id)
-        mock_error.assert_called_with("1 of 1 servers failed to delete.")
-        self.assertIsNone(result)
-
-    def test_project_purge_with_force_delete_backup(self):
-        self.backups_mock.delete.side_effect = [exceptions.CommandError, None]
-        arglist = [
-            '--project',
-            self.project.id,
-        ]
-        verifylist = [
-            ('dry_run', False),
-            ('keep_project', False),
-            ('auth_project', False),
-            ('project', self.project.id),
-            ('project_domain', None),
-        ]
-        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-
-        result = self.cmd.take_action(parsed_args)
-        self.projects_mock.get.assert_called_once_with(self.project.id)
-        self.projects_mock.delete.assert_called_once_with(self.project.id)
-        self.servers_mock.list.assert_called_once_with(
-            search_opts={'tenant_id': self.project.id, 'all_tenants': True}
-        )
-        kwargs = {'filters': {'owner': self.project.id}}
-        self.images_mock.list.assert_called_once_with(**kwargs)
-        volume_search_opts = {
-            'project_id': self.project.id,
-            'all_tenants': True,
-        }
-        self.volumes_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.snapshots_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.backups_mock.list.assert_called_once_with(
-            search_opts=volume_search_opts
-        )
-        self.servers_mock.delete.assert_called_once_with(self.server.id)
-        self.images_mock.delete.assert_called_once_with(self.image.id)
-        self.volumes_mock.force_delete.assert_called_once_with(self.volume.id)
-        self.snapshots_mock.delete.assert_called_once_with(self.snapshot.id)
-        self.assertEqual(2, self.backups_mock.delete.call_count)
-        self.backups_mock.delete.assert_called_with(self.backup.id, force=True)
-        self.assertIsNone(result)
diff --git a/releasenotes/notes/remove-project-purge-d372374b1a7c4641.yaml b/releasenotes/notes/remove-project-purge-d372374b1a7c4641.yaml
new file mode 100644
index 0000000000..049ed26b2e
--- /dev/null
+++ b/releasenotes/notes/remove-project-purge-d372374b1a7c4641.yaml
@@ -0,0 +1,8 @@
+---
+upgrade:
+  - |
+    The ``project purge`` command has been removed. This has been superseded by
+    the ``project cleanup`` command, was not tested, and has not been
+    functional for some time hence its removal without a deprecation period.
+    The replacement is ``project cleanup``, which is more powerful and more
+    flexible.
diff --git a/setup.cfg b/setup.cfg
index 6989489fc1..de13507562 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -45,7 +45,6 @@ openstack.common =
     extension_show = openstackclient.common.extension:ShowExtension
     limits_show = openstackclient.common.limits:ShowLimits
     project_cleanup = openstackclient.common.project_cleanup:ProjectCleanup
-    project_purge = openstackclient.common.project_purge:ProjectPurge
     quota_list = openstackclient.common.quota:ListQuota
     quota_set = openstackclient.common.quota:SetQuota
     quota_show = openstackclient.common.quota:ShowQuota