Merge "Remove project purge image commands"
This commit is contained in:
commit
9e2316fd06
@ -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
|
@ -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
|
||||
|
@ -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)
|
@ -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)
|
@ -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.
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user