From d2a0a5ae26d41ab0e71f97955ff7c372fd1ed71b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sybren=20A=2E=20St=C3=BCvel?= Date: Tue, 27 Mar 2018 15:31:10 +0200 Subject: [PATCH] Added CLI command 'maintenance purge_home_projects' This command soft-deletes home projects when their owning user is no longer there. --- pillar/__init__.py | 13 +++++++++ pillar/cli/maintenance.py | 47 ++++++++++++++++++++++++++++++ tests/test_cli/__init__.py | 0 tests/test_cli/test_maintenance.py | 35 ++++++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 tests/test_cli/__init__.py create mode 100644 tests/test_cli/test_maintenance.py diff --git a/pillar/__init__.py b/pillar/__init__.py index 458e113c..f2e9e713 100644 --- a/pillar/__init__.py +++ b/pillar/__init__.py @@ -808,6 +808,19 @@ class PillarServer(BlinkerCompatibleEve): return patch_internal(resource, payload=payload, concurrency_check=concurrency_check, skip_validation=skip_validation, **lookup)[:4] + def delete_internal(self, resource: str, concurrency_check=False, + suppress_callbacks=False, **lookup): + """Workaround for Eve issue https://github.com/nicolaiarocci/eve/issues/810""" + from eve.methods.delete import deleteitem_internal + + url = self.config['URLS'][resource] + path = '%s/%s/%s' % (self.api_prefix, url, lookup['_id']) + with self.__fake_request_url_rule('DELETE', path): + return deleteitem_internal(resource, + concurrency_check=concurrency_check, + suppress_callbacks=suppress_callbacks, + **lookup)[:4] + def _list_routes(self): from pprint import pprint from flask import url_for diff --git a/pillar/cli/maintenance.py b/pillar/cli/maintenance.py index 2750fc99..5cb45f13 100644 --- a/pillar/cli/maintenance.py +++ b/pillar/cli/maintenance.py @@ -263,6 +263,53 @@ def check_home_project_groups(): return bad +@manager_maintenance.option('-g', '--go', dest='go', + action='store_true', default=False, + help='Actually go and perform the changes, without this just ' + 'shows differences.') +def purge_home_projects(go=False): + """Deletes all home projects that have no owner.""" + from pillar.api.utils.authentication import force_cli_user + force_cli_user() + + users_coll = current_app.data.driver.db['users'] + proj_coll = current_app.data.driver.db['projects'] + good = bad = 0 + + def bad_projects(): + nonlocal good, bad + + for proj in proj_coll.find({'category': 'home', '_deleted': {'$ne': True}}): + pid = proj['_id'] + uid = proj.get('user') + if not uid: + log.info('Project %s has no user assigned', uid) + bad += 1 + yield pid + continue + + if users_coll.find({'_id': uid, '_deleted': {'$ne': True}}).count() == 0: + log.info('Project %s has non-existing owner %s', pid, uid) + bad += 1 + yield pid + continue + + good += 1 + + if not go: + log.info('Dry run, use --go to actually perform the changes.') + + for project_id in bad_projects(): + log.info('Soft-deleting project %s', project_id) + if go: + r, _, _, status = current_app.delete_internal('projects', _id=project_id) + if status != 204: + raise ValueError(f'Error {status} deleting {project_id}: {r}') + + log.info('%i projects OK, %i projects deleted', good, bad) + return bad + + @manager_maintenance.command @manager_maintenance.option('-c', '--chunk', dest='chunk_size', default=50, help='Number of links to update, use 0 to update all.') diff --git a/tests/test_cli/__init__.py b/tests/test_cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_cli/test_maintenance.py b/tests/test_cli/test_maintenance.py new file mode 100644 index 00000000..1e41b3ca --- /dev/null +++ b/tests/test_cli/test_maintenance.py @@ -0,0 +1,35 @@ +from bson import ObjectId + +from pillar.tests import AbstractPillarTest + + +class PurgeHomeProjectsTest(AbstractPillarTest): + def test_purge(self): + self.create_standard_groups() + # user_a will be soft-deleted, user_b will be hard-deleted. + # We don't support soft-deleting users yet, but the code should be + # handling that properly anyway. + user_a = self.create_user(user_id=24 * 'a', roles={'subscriber'}, token='token-a') + user_b = self.create_user(user_id=24 * 'b', roles={'subscriber'}, token='token-b') + + # GET the home project to create it. + home_a = self.get('/api/bcloud/home-project', auth_token='token-a').json() + home_b = self.get('/api/bcloud/home-project', auth_token='token-b').json() + + with self.app.app_context(): + users_coll = self.app.db('users') + + res = users_coll.update_one({'_id': user_a}, {'$set': {'_deleted': True}}) + self.assertEqual(1, res.modified_count) + + res = users_coll.delete_one({'_id': user_b}) + self.assertEqual(1, res.deleted_count) + + from pillar.cli.maintenance import purge_home_projects + + with self.app.app_context(): + self.assertEqual(2, purge_home_projects(go=True)) + + proj_coll = self.app.db('projects') + self.assertEqual(True, proj_coll.find_one({'_id': ObjectId(home_a['_id'])})['_deleted']) + self.assertEqual(True, proj_coll.find_one({'_id': ObjectId(home_b['_id'])})['_deleted'])