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:
Steve Martinelli 2016-09-08 15:13:38 -07:00
parent eb793dc8c6
commit 227d4c64ef
6 changed files with 531 additions and 0 deletions

View 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.

View File

@ -251,6 +251,7 @@ Those actions with an opposite action are noted in parens if applicable.
live server migration if possible live server migration if possible
* ``pause`` (``unpause``) - stop one or more servers and leave them in memory * ``pause`` (``unpause``) - stop one or more servers and leave them in memory
* ``query`` - Query resources by Elasticsearch query string or json format DSL. * ``query`` - Query resources by Elasticsearch query string or json format DSL.
* ``purge`` - clean resources associated with a specific project
* ``reboot`` - forcibly reboot a server * ``reboot`` - forcibly reboot a server
* ``rebuild`` - rebuild a server using (most of) the same arguments as in the original create * ``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 * ``remove`` (``add``) - remove an object from a group of objects

View 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)

View 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)

View 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>`_]

View File

@ -47,6 +47,7 @@ openstack.common =
extension_list = openstackclient.common.extension:ListExtension extension_list = openstackclient.common.extension:ListExtension
extension_show = openstackclient.common.extension:ShowExtension extension_show = openstackclient.common.extension:ShowExtension
limits_show = openstackclient.common.limits:ShowLimits limits_show = openstackclient.common.limits:ShowLimits
project_purge = openstackclient.common.project_purge:ProjectPurge
quota_list = openstackclient.common.quota:ListQuota quota_list = openstackclient.common.quota:ListQuota
quota_set = openstackclient.common.quota:SetQuota quota_set = openstackclient.common.quota:SetQuota
quota_show = openstackclient.common.quota:ShowQuota quota_show = openstackclient.common.quota:ShowQuota