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 8f10a7745..000000000 --- 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 d789eceb5..8e1c9a6c0 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 11cf0076d..000000000 --- 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='', - 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 850adb5bb..000000000 --- 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 000000000..049ed26b2 --- /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 6989489fc..de1350756 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