project cleanup
New implementation of the project cleanup based on the sdk.project_cleanup. It is implemented as an additional OSC operation and will ideally obsolete the `openstack project purge` giving flexibility to extend services support, parallelization, filters, etc. Change-Id: Ie08877f182379f73e5ec5ad4daaf84b3092c829c
This commit is contained in:
parent
01a53fa96f
commit
119d2fae25
12
doc/source/cli/command-objects/project-cleanup.rst
Normal file
12
doc/source/cli/command-objects/project-cleanup.rst
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
===============
|
||||||
|
project cleanup
|
||||||
|
===============
|
||||||
|
|
||||||
|
Clean resources associated with a specific project based on OpenStackSDK
|
||||||
|
implementation
|
||||||
|
|
||||||
|
Block Storage v2, v3; Compute v2; Network v2; DNS v2; Orchestrate v1
|
||||||
|
|
||||||
|
|
||||||
|
.. autoprogram-cliff:: openstack.common
|
||||||
|
:command: project cleanup
|
@ -270,6 +270,7 @@ Those actions with an opposite action are noted in parens if applicable.
|
|||||||
* ``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
|
* ``purge`` - clean resources associated with a specific project
|
||||||
|
* ``cleanup`` - flexible 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
|
||||||
|
140
openstackclient/common/project_cleanup.py
Normal file
140
openstackclient/common/project_cleanup.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# Copyright 2020 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 getpass
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import queue
|
||||||
|
|
||||||
|
from cliff.formatters import table
|
||||||
|
from osc_lib.command import command
|
||||||
|
|
||||||
|
from openstackclient.i18n import _
|
||||||
|
from openstackclient.identity import common as identity_common
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def ask_user_yesno(msg, default=True):
|
||||||
|
"""Ask user Y/N question
|
||||||
|
|
||||||
|
:param str msg: question text
|
||||||
|
:param bool default: default value
|
||||||
|
:return bool: User choice
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
answer = getpass._raw_input(
|
||||||
|
'{} [{}]: '.format(msg, 'y/N' if not default else 'Y/n'))
|
||||||
|
if answer in ('y', 'Y', 'yes'):
|
||||||
|
return True
|
||||||
|
elif answer in ('n', 'N', 'no'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectCleanup(command.Command):
|
||||||
|
_description = _("Clean resources associated with a project")
|
||||||
|
|
||||||
|
def get_parser(self, prog_name):
|
||||||
|
parser = super(ProjectCleanup, self).get_parser(prog_name)
|
||||||
|
parser.add_argument(
|
||||||
|
'--dry-run',
|
||||||
|
action='store_true',
|
||||||
|
help=_("List a project's resources")
|
||||||
|
)
|
||||||
|
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)')
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--created-before',
|
||||||
|
metavar='<YYYY-MM-DDTHH24:MI:SS>',
|
||||||
|
help=_('Drop resources created before the given time')
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--updated-before',
|
||||||
|
metavar='<YYYY-MM-DDTHH24:MI:SS>',
|
||||||
|
help=_('Drop resources updated before the given time')
|
||||||
|
)
|
||||||
|
identity_common.add_project_domain_option_to_parser(parser)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def take_action(self, parsed_args):
|
||||||
|
sdk = self.app.client_manager.sdk_connection
|
||||||
|
|
||||||
|
if parsed_args.auth_project:
|
||||||
|
project_connect = sdk
|
||||||
|
elif parsed_args.project:
|
||||||
|
project = sdk.identity.find_project(
|
||||||
|
name_or_id=parsed_args.project,
|
||||||
|
ignore_missing=False)
|
||||||
|
project_connect = sdk.connect_as_project(project)
|
||||||
|
|
||||||
|
if project_connect:
|
||||||
|
status_queue = queue.Queue()
|
||||||
|
parsed_args.max_width = int(os.environ.get('CLIFF_MAX_TERM_WIDTH',
|
||||||
|
0))
|
||||||
|
parsed_args.fit_width = bool(int(os.environ.get('CLIFF_FIT_WIDTH',
|
||||||
|
0)))
|
||||||
|
parsed_args.print_empty = False
|
||||||
|
table_fmt = table.TableFormatter()
|
||||||
|
|
||||||
|
self.log.info('Searching resources...')
|
||||||
|
|
||||||
|
filters = {}
|
||||||
|
if parsed_args.created_before:
|
||||||
|
filters['created_at'] = parsed_args.created_before
|
||||||
|
|
||||||
|
if parsed_args.updated_before:
|
||||||
|
filters['updated_at'] = parsed_args.updated_before
|
||||||
|
|
||||||
|
project_connect.project_cleanup(dry_run=True,
|
||||||
|
status_queue=status_queue,
|
||||||
|
filters=filters)
|
||||||
|
|
||||||
|
data = []
|
||||||
|
while not status_queue.empty():
|
||||||
|
resource = status_queue.get_nowait()
|
||||||
|
data.append(
|
||||||
|
(type(resource).__name__, resource.id, resource.name))
|
||||||
|
status_queue.task_done()
|
||||||
|
status_queue.join()
|
||||||
|
table_fmt.emit_list(
|
||||||
|
('Type', 'ID', 'Name'),
|
||||||
|
data,
|
||||||
|
self.app.stdout,
|
||||||
|
parsed_args
|
||||||
|
)
|
||||||
|
|
||||||
|
if parsed_args.dry_run:
|
||||||
|
return
|
||||||
|
|
||||||
|
confirm = ask_user_yesno(
|
||||||
|
_("These resources will be deleted. Are you sure"),
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
if confirm:
|
||||||
|
self.log.warning(_('Deleting resources'))
|
||||||
|
|
||||||
|
project_connect.project_cleanup(dry_run=False,
|
||||||
|
status_queue=status_queue,
|
||||||
|
filters=filters)
|
183
openstackclient/tests/unit/common/test_project_cleanup.py
Normal file
183
openstackclient/tests/unit/common/test_project_cleanup.py
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
# 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 io import StringIO
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from openstackclient.common import project_cleanup
|
||||||
|
from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
|
||||||
|
from openstackclient.tests.unit import utils as tests_utils
|
||||||
|
|
||||||
|
|
||||||
|
class TestProjectCleanupBase(tests_utils.TestCommand):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestProjectCleanupBase, self).setUp()
|
||||||
|
|
||||||
|
self.app.client_manager.sdk_connection = mock.Mock()
|
||||||
|
|
||||||
|
|
||||||
|
class TestProjectCleanup(TestProjectCleanupBase):
|
||||||
|
|
||||||
|
project = identity_fakes.FakeProject.create_one_project()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestProjectCleanup, self).setUp()
|
||||||
|
self.cmd = project_cleanup.ProjectCleanup(self.app, None)
|
||||||
|
|
||||||
|
self.project_cleanup_mock = mock.Mock()
|
||||||
|
self.sdk_connect_as_project_mock = \
|
||||||
|
mock.Mock(return_value=self.app.client_manager.sdk_connection)
|
||||||
|
self.app.client_manager.sdk_connection.project_cleanup = \
|
||||||
|
self.project_cleanup_mock
|
||||||
|
self.app.client_manager.sdk_connection.identity.find_project = \
|
||||||
|
mock.Mock(return_value=self.project)
|
||||||
|
self.app.client_manager.sdk_connection.connect_as_project = \
|
||||||
|
self.sdk_connect_as_project_mock
|
||||||
|
|
||||||
|
def test_project_no_options(self):
|
||||||
|
arglist = []
|
||||||
|
verifylist = []
|
||||||
|
|
||||||
|
self.assertRaises(tests_utils.ParserException, self.check_parser,
|
||||||
|
self.cmd, arglist, verifylist)
|
||||||
|
|
||||||
|
def test_project_cleanup_with_filters(self):
|
||||||
|
arglist = [
|
||||||
|
'--project', self.project.id,
|
||||||
|
'--created-before', '2200-01-01',
|
||||||
|
'--updated-before', '2200-01-02'
|
||||||
|
]
|
||||||
|
verifylist = [
|
||||||
|
('dry_run', False),
|
||||||
|
('auth_project', False),
|
||||||
|
('project', self.project.id),
|
||||||
|
('created_before', '2200-01-01'),
|
||||||
|
('updated_before', '2200-01-02')
|
||||||
|
]
|
||||||
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||||
|
result = None
|
||||||
|
|
||||||
|
with mock.patch('sys.stdin', StringIO('y')):
|
||||||
|
result = self.cmd.take_action(parsed_args)
|
||||||
|
|
||||||
|
self.sdk_connect_as_project_mock.assert_called_with(
|
||||||
|
self.project)
|
||||||
|
filters = {
|
||||||
|
'created_at': '2200-01-01',
|
||||||
|
'updated_at': '2200-01-02'
|
||||||
|
}
|
||||||
|
|
||||||
|
calls = [
|
||||||
|
mock.call(dry_run=True, status_queue=mock.ANY, filters=filters),
|
||||||
|
mock.call(dry_run=False, status_queue=mock.ANY, filters=filters)
|
||||||
|
]
|
||||||
|
self.project_cleanup_mock.assert_has_calls(calls)
|
||||||
|
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_project_cleanup_with_project(self):
|
||||||
|
arglist = [
|
||||||
|
'--project', self.project.id,
|
||||||
|
]
|
||||||
|
verifylist = [
|
||||||
|
('dry_run', False),
|
||||||
|
('auth_project', False),
|
||||||
|
('project', self.project.id),
|
||||||
|
]
|
||||||
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||||
|
result = None
|
||||||
|
|
||||||
|
with mock.patch('sys.stdin', StringIO('y')):
|
||||||
|
result = self.cmd.take_action(parsed_args)
|
||||||
|
|
||||||
|
self.sdk_connect_as_project_mock.assert_called_with(
|
||||||
|
self.project)
|
||||||
|
calls = [
|
||||||
|
mock.call(dry_run=True, status_queue=mock.ANY, filters={}),
|
||||||
|
mock.call(dry_run=False, status_queue=mock.ANY, filters={})
|
||||||
|
]
|
||||||
|
self.project_cleanup_mock.assert_has_calls(calls)
|
||||||
|
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_project_cleanup_with_project_abort(self):
|
||||||
|
arglist = [
|
||||||
|
'--project', self.project.id,
|
||||||
|
]
|
||||||
|
verifylist = [
|
||||||
|
('dry_run', False),
|
||||||
|
('auth_project', False),
|
||||||
|
('project', self.project.id),
|
||||||
|
]
|
||||||
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||||
|
result = None
|
||||||
|
|
||||||
|
with mock.patch('sys.stdin', StringIO('n')):
|
||||||
|
result = self.cmd.take_action(parsed_args)
|
||||||
|
|
||||||
|
self.sdk_connect_as_project_mock.assert_called_with(
|
||||||
|
self.project)
|
||||||
|
calls = [
|
||||||
|
mock.call(dry_run=True, status_queue=mock.ANY, filters={}),
|
||||||
|
]
|
||||||
|
self.project_cleanup_mock.assert_has_calls(calls)
|
||||||
|
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_project_cleanup_with_dry_run(self):
|
||||||
|
arglist = [
|
||||||
|
'--dry-run',
|
||||||
|
'--project', self.project.id,
|
||||||
|
]
|
||||||
|
verifylist = [
|
||||||
|
('dry_run', True),
|
||||||
|
('auth_project', False),
|
||||||
|
('project', self.project.id),
|
||||||
|
]
|
||||||
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||||
|
result = None
|
||||||
|
|
||||||
|
result = self.cmd.take_action(parsed_args)
|
||||||
|
|
||||||
|
self.sdk_connect_as_project_mock.assert_called_with(
|
||||||
|
self.project)
|
||||||
|
self.project_cleanup_mock.assert_called_once_with(
|
||||||
|
dry_run=True, status_queue=mock.ANY, filters={})
|
||||||
|
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_project_cleanup_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),
|
||||||
|
('auth_project', True),
|
||||||
|
('project', None),
|
||||||
|
]
|
||||||
|
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||||
|
result = None
|
||||||
|
|
||||||
|
with mock.patch('sys.stdin', StringIO('y')):
|
||||||
|
result = self.cmd.take_action(parsed_args)
|
||||||
|
|
||||||
|
self.sdk_connect_as_project_mock.assert_not_called()
|
||||||
|
calls = [
|
||||||
|
mock.call(dry_run=True, status_queue=mock.ANY, filters={}),
|
||||||
|
mock.call(dry_run=False, status_queue=mock.ANY, filters={})
|
||||||
|
]
|
||||||
|
self.project_cleanup_mock.assert_has_calls(calls)
|
||||||
|
|
||||||
|
self.assertIsNone(result)
|
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Add support for project cleanup based on the OpenStackSDK with
|
||||||
|
create/update time filters. In the long run this will replace
|
||||||
|
`openstack project purge` command.
|
@ -45,6 +45,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_cleanup = openstackclient.common.project_cleanup:ProjectCleanup
|
||||||
project_purge = openstackclient.common.project_purge:ProjectPurge
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user