Add project purge command to osc
See the initial implementation: https://github.com/openstack/ospurge/blob/master/ospurge/client.py Partial-Bug: 1584596 Change-Id: I3aa86af7c85e7ca3b7f04b43e8e07125f7d956d1
This commit is contained in:
		
							
								
								
									
										42
									
								
								doc/source/command-objects/project-purge.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								doc/source/command-objects/project-purge.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
=============
 | 
			
		||||
project purge
 | 
			
		||||
=============
 | 
			
		||||
 | 
			
		||||
Clean resources associated with a specific project.
 | 
			
		||||
 | 
			
		||||
Block Storage v1, v2; Compute v2; Image v1, v2
 | 
			
		||||
 | 
			
		||||
project purge
 | 
			
		||||
-------------
 | 
			
		||||
 | 
			
		||||
Clean resources associated with a project
 | 
			
		||||
 | 
			
		||||
.. program:: project purge
 | 
			
		||||
.. code:: bash
 | 
			
		||||
 | 
			
		||||
    openstack project purge
 | 
			
		||||
        [--dry-run]
 | 
			
		||||
        [--keep-project]
 | 
			
		||||
        [--auth-project | --project <project>]
 | 
			
		||||
        [--project-domain <project-domain>]
 | 
			
		||||
 | 
			
		||||
.. option:: --dry-run
 | 
			
		||||
 | 
			
		||||
    List a project's resources
 | 
			
		||||
 | 
			
		||||
.. option:: --keep-project
 | 
			
		||||
 | 
			
		||||
    Clean project resources, but don't delete the project.
 | 
			
		||||
 | 
			
		||||
.. option:: --auth-project
 | 
			
		||||
 | 
			
		||||
    Delete resources of the project used to authenticate
 | 
			
		||||
 | 
			
		||||
.. option:: --project <project>
 | 
			
		||||
 | 
			
		||||
    Project to clean (name or ID)
 | 
			
		||||
 | 
			
		||||
.. option:: --project-domain <project-domain>
 | 
			
		||||
 | 
			
		||||
    Domain the project belongs to (name or ID). This can be
 | 
			
		||||
    used in case collisions between project names exist.
 | 
			
		||||
@@ -251,6 +251,7 @@ 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
 | 
			
		||||
* ``reboot`` - forcibly reboot a server
 | 
			
		||||
* ``rebuild`` - rebuild a server using (most of) the same arguments as in the original create
 | 
			
		||||
* ``remove`` (``add``) - remove an object from a group of objects
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										168
									
								
								openstackclient/common/project_purge.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								openstackclient/common/project_purge.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,168 @@
 | 
			
		||||
#   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}
 | 
			
		||||
            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
 | 
			
		||||
            data = image_client.images.list(owner=project_id)
 | 
			
		||||
            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}
 | 
			
		||||
        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)
 | 
			
		||||
							
								
								
									
										314
									
								
								openstackclient/tests/unit/common/test_project_purge.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										314
									
								
								openstackclient/tests/unit/common/test_project_purge.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,314 @@
 | 
			
		||||
#   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 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(TestProjectPurgeInit, self).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.FakeServer.create_one_server()
 | 
			
		||||
    image = image_fakes.FakeImage.create_one_image()
 | 
			
		||||
    volume = volume_fakes.FakeVolume.create_one_volume()
 | 
			
		||||
    backup = volume_fakes.FakeBackup.create_one_backup()
 | 
			
		||||
    snapshot = volume_fakes.FakeSnapshot.create_one_snapshot()
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        super(TestProjectPurge, self).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})
 | 
			
		||||
        self.images_mock.list.assert_called_once_with(
 | 
			
		||||
            owner=self.project.id)
 | 
			
		||||
        volume_search_opts = {'project_id': self.project.id}
 | 
			
		||||
        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})
 | 
			
		||||
        self.images_mock.list.assert_called_once_with(
 | 
			
		||||
            owner=self.project.id)
 | 
			
		||||
        volume_search_opts = {'project_id': self.project.id}
 | 
			
		||||
        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})
 | 
			
		||||
        self.images_mock.list.assert_called_once_with(
 | 
			
		||||
            owner=self.project.id)
 | 
			
		||||
        volume_search_opts = {'project_id': self.project.id}
 | 
			
		||||
        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})
 | 
			
		||||
        self.images_mock.list.assert_called_once_with(
 | 
			
		||||
            owner=self.project.id)
 | 
			
		||||
        volume_search_opts = {'project_id': self.project.id}
 | 
			
		||||
        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})
 | 
			
		||||
        self.images_mock.list.assert_called_once_with(
 | 
			
		||||
            owner=self.project.id)
 | 
			
		||||
        volume_search_opts = {'project_id': self.project.id}
 | 
			
		||||
        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})
 | 
			
		||||
        self.images_mock.list.assert_called_once_with(
 | 
			
		||||
            owner=self.project.id)
 | 
			
		||||
        volume_search_opts = {'project_id': self.project.id}
 | 
			
		||||
        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)
 | 
			
		||||
							
								
								
									
										5
									
								
								releasenotes/notes/bug-1584596-5b3109487b451bec.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								releasenotes/notes/bug-1584596-5b3109487b451bec.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
---
 | 
			
		||||
fixes:
 | 
			
		||||
  - |
 | 
			
		||||
    Add command ``openstack project purge`` to clean a project's resources.
 | 
			
		||||
    [Bug `1584596 <https://bugs.launchpad.net/bugs/1584596>`_]
 | 
			
		||||
@@ -47,6 +47,7 @@ openstack.common =
 | 
			
		||||
    extension_list = openstackclient.common.extension:ListExtension
 | 
			
		||||
    extension_show = openstackclient.common.extension:ShowExtension
 | 
			
		||||
    limits_show = openstackclient.common.limits:ShowLimits
 | 
			
		||||
    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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user