Skip to content

Commit 86bca18

Browse files
Zuulopenstack-gerrit
authored andcommitted
Merge "project cleanup"
2 parents 791bed6 + 119d2fa commit 86bca18

6 files changed

Lines changed: 343 additions & 0 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
===============
2+
project cleanup
3+
===============
4+
5+
Clean resources associated with a specific project based on OpenStackSDK
6+
implementation
7+
8+
Block Storage v2, v3; Compute v2; Network v2; DNS v2; Orchestrate v1
9+
10+
11+
.. autoprogram-cliff:: openstack.common
12+
:command: project cleanup

doc/source/cli/commands.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ Those actions with an opposite action are noted in parens if applicable.
270270
* ``pause`` (``unpause``) - stop one or more servers and leave them in memory
271271
* ``query`` - Query resources by Elasticsearch query string or json format DSL.
272272
* ``purge`` - clean resources associated with a specific project
273+
* ``cleanup`` - flexible clean resources associated with a specific project
273274
* ``reboot`` - forcibly reboot a server
274275
* ``rebuild`` - rebuild a server using (most of) the same arguments as in the original create
275276
* ``remove`` (``add``) - remove an object from a group of objects
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Copyright 2020 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 getpass
17+
import logging
18+
import os
19+
import queue
20+
21+
from cliff.formatters import table
22+
from osc_lib.command import command
23+
24+
from openstackclient.i18n import _
25+
from openstackclient.identity import common as identity_common
26+
27+
28+
LOG = logging.getLogger(__name__)
29+
30+
31+
def ask_user_yesno(msg, default=True):
32+
"""Ask user Y/N question
33+
34+
:param str msg: question text
35+
:param bool default: default value
36+
:return bool: User choice
37+
"""
38+
while True:
39+
answer = getpass._raw_input(
40+
'{} [{}]: '.format(msg, 'y/N' if not default else 'Y/n'))
41+
if answer in ('y', 'Y', 'yes'):
42+
return True
43+
elif answer in ('n', 'N', 'no'):
44+
return False
45+
46+
47+
class ProjectCleanup(command.Command):
48+
_description = _("Clean resources associated with a project")
49+
50+
def get_parser(self, prog_name):
51+
parser = super(ProjectCleanup, self).get_parser(prog_name)
52+
parser.add_argument(
53+
'--dry-run',
54+
action='store_true',
55+
help=_("List a project's resources")
56+
)
57+
project_group = parser.add_mutually_exclusive_group(required=True)
58+
project_group.add_argument(
59+
'--auth-project',
60+
action='store_true',
61+
help=_('Delete resources of the project used to authenticate')
62+
)
63+
project_group.add_argument(
64+
'--project',
65+
metavar='<project>',
66+
help=_('Project to clean (name or ID)')
67+
)
68+
parser.add_argument(
69+
'--created-before',
70+
metavar='<YYYY-MM-DDTHH24:MI:SS>',
71+
help=_('Drop resources created before the given time')
72+
)
73+
parser.add_argument(
74+
'--updated-before',
75+
metavar='<YYYY-MM-DDTHH24:MI:SS>',
76+
help=_('Drop resources updated before the given time')
77+
)
78+
identity_common.add_project_domain_option_to_parser(parser)
79+
return parser
80+
81+
def take_action(self, parsed_args):
82+
sdk = self.app.client_manager.sdk_connection
83+
84+
if parsed_args.auth_project:
85+
project_connect = sdk
86+
elif parsed_args.project:
87+
project = sdk.identity.find_project(
88+
name_or_id=parsed_args.project,
89+
ignore_missing=False)
90+
project_connect = sdk.connect_as_project(project)
91+
92+
if project_connect:
93+
status_queue = queue.Queue()
94+
parsed_args.max_width = int(os.environ.get('CLIFF_MAX_TERM_WIDTH',
95+
0))
96+
parsed_args.fit_width = bool(int(os.environ.get('CLIFF_FIT_WIDTH',
97+
0)))
98+
parsed_args.print_empty = False
99+
table_fmt = table.TableFormatter()
100+
101+
self.log.info('Searching resources...')
102+
103+
filters = {}
104+
if parsed_args.created_before:
105+
filters['created_at'] = parsed_args.created_before
106+
107+
if parsed_args.updated_before:
108+
filters['updated_at'] = parsed_args.updated_before
109+
110+
project_connect.project_cleanup(dry_run=True,
111+
status_queue=status_queue,
112+
filters=filters)
113+
114+
data = []
115+
while not status_queue.empty():
116+
resource = status_queue.get_nowait()
117+
data.append(
118+
(type(resource).__name__, resource.id, resource.name))
119+
status_queue.task_done()
120+
status_queue.join()
121+
table_fmt.emit_list(
122+
('Type', 'ID', 'Name'),
123+
data,
124+
self.app.stdout,
125+
parsed_args
126+
)
127+
128+
if parsed_args.dry_run:
129+
return
130+
131+
confirm = ask_user_yesno(
132+
_("These resources will be deleted. Are you sure"),
133+
default=False)
134+
135+
if confirm:
136+
self.log.warning(_('Deleting resources'))
137+
138+
project_connect.project_cleanup(dry_run=False,
139+
status_queue=status_queue,
140+
filters=filters)
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
# not use this file except in compliance with the License. You may obtain
3+
# a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
# License for the specific language governing permissions and limitations
11+
# under the License.
12+
13+
from io import StringIO
14+
from unittest import mock
15+
16+
from openstackclient.common import project_cleanup
17+
from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
18+
from openstackclient.tests.unit import utils as tests_utils
19+
20+
21+
class TestProjectCleanupBase(tests_utils.TestCommand):
22+
23+
def setUp(self):
24+
super(TestProjectCleanupBase, self).setUp()
25+
26+
self.app.client_manager.sdk_connection = mock.Mock()
27+
28+
29+
class TestProjectCleanup(TestProjectCleanupBase):
30+
31+
project = identity_fakes.FakeProject.create_one_project()
32+
33+
def setUp(self):
34+
super(TestProjectCleanup, self).setUp()
35+
self.cmd = project_cleanup.ProjectCleanup(self.app, None)
36+
37+
self.project_cleanup_mock = mock.Mock()
38+
self.sdk_connect_as_project_mock = \
39+
mock.Mock(return_value=self.app.client_manager.sdk_connection)
40+
self.app.client_manager.sdk_connection.project_cleanup = \
41+
self.project_cleanup_mock
42+
self.app.client_manager.sdk_connection.identity.find_project = \
43+
mock.Mock(return_value=self.project)
44+
self.app.client_manager.sdk_connection.connect_as_project = \
45+
self.sdk_connect_as_project_mock
46+
47+
def test_project_no_options(self):
48+
arglist = []
49+
verifylist = []
50+
51+
self.assertRaises(tests_utils.ParserException, self.check_parser,
52+
self.cmd, arglist, verifylist)
53+
54+
def test_project_cleanup_with_filters(self):
55+
arglist = [
56+
'--project', self.project.id,
57+
'--created-before', '2200-01-01',
58+
'--updated-before', '2200-01-02'
59+
]
60+
verifylist = [
61+
('dry_run', False),
62+
('auth_project', False),
63+
('project', self.project.id),
64+
('created_before', '2200-01-01'),
65+
('updated_before', '2200-01-02')
66+
]
67+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
68+
result = None
69+
70+
with mock.patch('sys.stdin', StringIO('y')):
71+
result = self.cmd.take_action(parsed_args)
72+
73+
self.sdk_connect_as_project_mock.assert_called_with(
74+
self.project)
75+
filters = {
76+
'created_at': '2200-01-01',
77+
'updated_at': '2200-01-02'
78+
}
79+
80+
calls = [
81+
mock.call(dry_run=True, status_queue=mock.ANY, filters=filters),
82+
mock.call(dry_run=False, status_queue=mock.ANY, filters=filters)
83+
]
84+
self.project_cleanup_mock.assert_has_calls(calls)
85+
86+
self.assertIsNone(result)
87+
88+
def test_project_cleanup_with_project(self):
89+
arglist = [
90+
'--project', self.project.id,
91+
]
92+
verifylist = [
93+
('dry_run', False),
94+
('auth_project', False),
95+
('project', self.project.id),
96+
]
97+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
98+
result = None
99+
100+
with mock.patch('sys.stdin', StringIO('y')):
101+
result = self.cmd.take_action(parsed_args)
102+
103+
self.sdk_connect_as_project_mock.assert_called_with(
104+
self.project)
105+
calls = [
106+
mock.call(dry_run=True, status_queue=mock.ANY, filters={}),
107+
mock.call(dry_run=False, status_queue=mock.ANY, filters={})
108+
]
109+
self.project_cleanup_mock.assert_has_calls(calls)
110+
111+
self.assertIsNone(result)
112+
113+
def test_project_cleanup_with_project_abort(self):
114+
arglist = [
115+
'--project', self.project.id,
116+
]
117+
verifylist = [
118+
('dry_run', False),
119+
('auth_project', False),
120+
('project', self.project.id),
121+
]
122+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
123+
result = None
124+
125+
with mock.patch('sys.stdin', StringIO('n')):
126+
result = self.cmd.take_action(parsed_args)
127+
128+
self.sdk_connect_as_project_mock.assert_called_with(
129+
self.project)
130+
calls = [
131+
mock.call(dry_run=True, status_queue=mock.ANY, filters={}),
132+
]
133+
self.project_cleanup_mock.assert_has_calls(calls)
134+
135+
self.assertIsNone(result)
136+
137+
def test_project_cleanup_with_dry_run(self):
138+
arglist = [
139+
'--dry-run',
140+
'--project', self.project.id,
141+
]
142+
verifylist = [
143+
('dry_run', True),
144+
('auth_project', False),
145+
('project', self.project.id),
146+
]
147+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
148+
result = None
149+
150+
result = self.cmd.take_action(parsed_args)
151+
152+
self.sdk_connect_as_project_mock.assert_called_with(
153+
self.project)
154+
self.project_cleanup_mock.assert_called_once_with(
155+
dry_run=True, status_queue=mock.ANY, filters={})
156+
157+
self.assertIsNone(result)
158+
159+
def test_project_cleanup_with_auth_project(self):
160+
self.app.client_manager.auth_ref = mock.Mock()
161+
self.app.client_manager.auth_ref.project_id = self.project.id
162+
arglist = [
163+
'--auth-project',
164+
]
165+
verifylist = [
166+
('dry_run', False),
167+
('auth_project', True),
168+
('project', None),
169+
]
170+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
171+
result = None
172+
173+
with mock.patch('sys.stdin', StringIO('y')):
174+
result = self.cmd.take_action(parsed_args)
175+
176+
self.sdk_connect_as_project_mock.assert_not_called()
177+
calls = [
178+
mock.call(dry_run=True, status_queue=mock.ANY, filters={}),
179+
mock.call(dry_run=False, status_queue=mock.ANY, filters={})
180+
]
181+
self.project_cleanup_mock.assert_has_calls(calls)
182+
183+
self.assertIsNone(result)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
features:
3+
- |
4+
Add support for project cleanup based on the OpenStackSDK with
5+
create/update time filters. In the long run this will replace
6+
`openstack project purge` command.

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ openstack.common =
4545
extension_list = openstackclient.common.extension:ListExtension
4646
extension_show = openstackclient.common.extension:ShowExtension
4747
limits_show = openstackclient.common.limits:ShowLimits
48+
project_cleanup = openstackclient.common.project_cleanup:ProjectCleanup
4849
project_purge = openstackclient.common.project_purge:ProjectPurge
4950
quota_list = openstackclient.common.quota:ListQuota
5051
quota_set = openstackclient.common.quota:SetQuota

0 commit comments

Comments
 (0)