Skip to content

Commit 227d4c6

Browse files
committed
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
1 parent eb793dc commit 227d4c6

6 files changed

Lines changed: 531 additions & 0 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
=============
2+
project purge
3+
=============
4+
5+
Clean resources associated with a specific project.
6+
7+
Block Storage v1, v2; Compute v2; Image v1, v2
8+
9+
project purge
10+
-------------
11+
12+
Clean resources associated with a project
13+
14+
.. program:: project purge
15+
.. code:: bash
16+
17+
openstack project purge
18+
[--dry-run]
19+
[--keep-project]
20+
[--auth-project | --project <project>]
21+
[--project-domain <project-domain>]
22+
23+
.. option:: --dry-run
24+
25+
List a project's resources
26+
27+
.. option:: --keep-project
28+
29+
Clean project resources, but don't delete the project.
30+
31+
.. option:: --auth-project
32+
33+
Delete resources of the project used to authenticate
34+
35+
.. option:: --project <project>
36+
37+
Project to clean (name or ID)
38+
39+
.. option:: --project-domain <project-domain>
40+
41+
Domain the project belongs to (name or ID). This can be
42+
used in case collisions between project names exist.

doc/source/commands.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ Those actions with an opposite action are noted in parens if applicable.
251251
live server migration if possible
252252
* ``pause`` (``unpause``) - stop one or more servers and leave them in memory
253253
* ``query`` - Query resources by Elasticsearch query string or json format DSL.
254+
* ``purge`` - clean resources associated with a specific project
254255
* ``reboot`` - forcibly reboot a server
255256
* ``rebuild`` - rebuild a server using (most of) the same arguments as in the original create
256257
* ``remove`` (``add``) - remove an object from a group of objects
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Copyright 2012 OpenStack Foundation
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
#
15+
16+
import logging
17+
18+
from osc_lib.command import command
19+
from osc_lib import utils
20+
21+
from openstackclient.i18n import _
22+
from openstackclient.identity import common as identity_common
23+
24+
25+
LOG = logging.getLogger(__name__)
26+
27+
28+
class ProjectPurge(command.Command):
29+
_description = _("Clean resources associated with a project")
30+
31+
def get_parser(self, prog_name):
32+
parser = super(ProjectPurge, self).get_parser(prog_name)
33+
parser.add_argument(
34+
'--dry-run',
35+
action='store_true',
36+
help=_("List a project's resources"),
37+
)
38+
parser.add_argument(
39+
'--keep-project',
40+
action='store_true',
41+
help=_("Clean project resources, but don't delete the project"),
42+
)
43+
project_group = parser.add_mutually_exclusive_group(required=True)
44+
project_group.add_argument(
45+
'--auth-project',
46+
action='store_true',
47+
help=_('Delete resources of the project used to authenticate'),
48+
)
49+
project_group.add_argument(
50+
'--project',
51+
metavar='<project>',
52+
help=_('Project to clean (name or ID)'),
53+
)
54+
identity_common.add_project_domain_option_to_parser(parser)
55+
return parser
56+
57+
def take_action(self, parsed_args):
58+
identity_client = self.app.client_manager.identity
59+
60+
if parsed_args.auth_project:
61+
project_id = self.app.client_manager.auth_ref.project_id
62+
elif parsed_args.project:
63+
try:
64+
project_id = identity_common.find_project(
65+
identity_client,
66+
parsed_args.project,
67+
parsed_args.project_domain,
68+
).id
69+
except AttributeError: # using v2 auth and supplying a domain
70+
project_id = utils.find_resource(
71+
identity_client.tenants,
72+
parsed_args.project,
73+
).id
74+
75+
# delete all non-identity resources
76+
self.delete_resources(parsed_args.dry_run, project_id)
77+
78+
# clean up the project
79+
if not parsed_args.keep_project:
80+
LOG.warning(_('Deleting project: %s'), project_id)
81+
if not parsed_args.dry_run:
82+
identity_client.projects.delete(project_id)
83+
84+
def delete_resources(self, dry_run, project_id):
85+
# servers
86+
try:
87+
compute_client = self.app.client_manager.compute
88+
search_opts = {'tenant_id': project_id}
89+
data = compute_client.servers.list(search_opts=search_opts)
90+
self.delete_objects(
91+
compute_client.servers.delete, data, 'server', dry_run)
92+
except Exception:
93+
pass
94+
95+
# images
96+
try:
97+
image_client = self.app.client_manager.image
98+
data = image_client.images.list(owner=project_id)
99+
self.delete_objects(
100+
image_client.images.delete, data, 'image', dry_run)
101+
except Exception:
102+
pass
103+
104+
# volumes, snapshots, backups
105+
volume_client = self.app.client_manager.volume
106+
search_opts = {'project_id': project_id}
107+
try:
108+
data = volume_client.volume_snapshots.list(search_opts=search_opts)
109+
self.delete_objects(
110+
self.delete_one_volume_snapshot,
111+
data,
112+
'volume snapshot',
113+
dry_run)
114+
except Exception:
115+
pass
116+
try:
117+
data = volume_client.backups.list(search_opts=search_opts)
118+
self.delete_objects(
119+
self.delete_one_volume_backup,
120+
data,
121+
'volume backup',
122+
dry_run)
123+
except Exception:
124+
pass
125+
try:
126+
data = volume_client.volumes.list(search_opts=search_opts)
127+
self.delete_objects(
128+
volume_client.volumes.force_delete, data, 'volume', dry_run)
129+
except Exception:
130+
pass
131+
132+
def delete_objects(self, func_delete, data, resource, dry_run):
133+
result = 0
134+
for i in data:
135+
LOG.warning(_('Deleting %(resource)s : %(id)s') %
136+
{'resource': resource, 'id': i.id})
137+
if not dry_run:
138+
try:
139+
func_delete(i.id)
140+
except Exception as e:
141+
result += 1
142+
LOG.error(_("Failed to delete %(resource)s with "
143+
"ID '%(id)s': %(e)s")
144+
% {'resource': resource, 'id': i.id, 'e': e})
145+
if result > 0:
146+
total = len(data)
147+
msg = (_("%(result)s of %(total)s %(resource)ss failed "
148+
"to delete.") %
149+
{'result': result,
150+
'total': total,
151+
'resource': resource})
152+
LOG.error(msg)
153+
154+
def delete_one_volume_snapshot(self, snapshot_id):
155+
volume_client = self.app.client_manager.volume
156+
try:
157+
volume_client.volume_snapshots.delete(snapshot_id)
158+
except Exception:
159+
# Only volume v2 support deleting by force
160+
volume_client.volume_snapshots.delete(snapshot_id, force=True)
161+
162+
def delete_one_volume_backup(self, backup_id):
163+
volume_client = self.app.client_manager.volume
164+
try:
165+
volume_client.backups.delete(backup_id)
166+
except Exception:
167+
# Only volume v2 support deleting by force
168+
volume_client.backups.delete(backup_id, force=True)

0 commit comments

Comments
 (0)