From b61cd62db672bda60dc194ad6c3326ae4737f973 Mon Sep 17 00:00:00 2001 From: Liang Chen Date: Sat, 12 Oct 2013 16:08:05 +0800 Subject: [PATCH] Add granularity option to purge_deleted Allow users to choose how many days, hours, minutes, or seconds to preserve deleted data. Closes-bug: #1237195 Change-Id: I9227f5bf53c5b099a7a691a49c4ff2f5f0662098 --- heat/cmd/manage.py | 10 +++++-- heat/db/sqlalchemy/api.py | 30 ++++++++++++-------- heat/db/utils.py | 4 +-- heat/tests/test_sqlalchemy_api.py | 46 ++++++++++++++++++++++++++++++- 4 files changed, 73 insertions(+), 17 deletions(-) diff --git a/heat/cmd/manage.py b/heat/cmd/manage.py index 089c21eda8..0549ef7f7b 100644 --- a/heat/cmd/manage.py +++ b/heat/cmd/manage.py @@ -48,7 +48,7 @@ def purge_deleted(): """ Remove database records that have been previously soft deleted """ - utils.purge_deleted(CONF.command.age) + utils.purge_deleted(CONF.command.age, CONF.command.granularity) def add_command_parsers(subparsers): @@ -62,8 +62,12 @@ def add_command_parsers(subparsers): parser = subparsers.add_parser('purge_deleted') parser.set_defaults(func=purge_deleted) - parser.add_argument('age', nargs='?', - help=_('Number of days to preserve.')) + parser.add_argument('age', nargs='?', default='90', + help=_('How long to preserve deleted data.')) + parser.add_argument( + '-g', '--granularity', default='days', + choices=['days', 'hours', 'minutes', 'seconds'], + help=_('Granularity to use for age argument, defaults to days.')) command_opt = cfg.SubCommandOpt('command', title='Commands', diff --git a/heat/db/sqlalchemy/api.py b/heat/db/sqlalchemy/api.py index d51517ce78..18a988bcc0 100644 --- a/heat/db/sqlalchemy/api.py +++ b/heat/db/sqlalchemy/api.py @@ -470,18 +470,26 @@ def watch_data_get_all(context): return results -def purge_deleted(age): - if age is not None: - try: - age = int(age) - except ValueError: - raise exception.Error(_("age should be an integer")) - if age < 0: - raise exception.Error(_("age should be a positive integer")) - else: - age = 90 +def purge_deleted(age, granularity='days'): + try: + age = int(age) + except ValueError: + raise exception.Error(_("age should be an integer")) + if age < 0: + raise exception.Error(_("age should be a positive integer")) - time_line = datetime.now() - timedelta(days=age) + if granularity not in ('days', 'hours', 'minutes', 'seconds'): + raise exception.Error( + _("granularity should be days, hours, minutes, or seconds")) + + if granularity == 'days': + age = age * 86400 + elif granularity == 'hours': + age = age * 3600 + elif granularity == 'minutes': + age = age * 60 + + time_line = datetime.now() - timedelta(seconds=age) engine = get_engine() meta = sqlalchemy.MetaData() meta.bind = engine diff --git a/heat/db/utils.py b/heat/db/utils.py index 245042611a..2351d45d0d 100644 --- a/heat/db/utils.py +++ b/heat/db/utils.py @@ -45,5 +45,5 @@ IMPL = LazyPluggable('db_backend', sqlalchemy='heat.db.sqlalchemy.api') -def purge_deleted(age): - IMPL.purge_deleted(age) +def purge_deleted(age, granularity='days'): + IMPL.purge_deleted(age, granularity) diff --git a/heat/tests/test_sqlalchemy_api.py b/heat/tests/test_sqlalchemy_api.py index fa61f6ad06..a3ef6d94c0 100644 --- a/heat/tests/test_sqlalchemy_api.py +++ b/heat/tests/test_sqlalchemy_api.py @@ -10,9 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. -import mox +from datetime import datetime +from datetime import timedelta + +import fixtures from json import loads from json import dumps +import mox + from heat.db.sqlalchemy import api as db_api from heat.engine import environment @@ -659,6 +664,45 @@ class DBAPIStackTest(HeatTestCase): self.assertEqual(2, db_api.stack_count_all_by_tenant(self.ctx)) + def test_purge_deleted(self): + now = datetime.now() + delta = timedelta(seconds=3600 * 7) + deleted = [now - delta * i for i in range(1, 6)] + templates = [create_raw_template(self.ctx) for i in range(5)] + creds = [create_user_creds(self.ctx) for i in range(5)] + stacks = [create_stack(self.ctx, templates[i], creds[i], + deleted_at=deleted[i]) for i in range(5)] + + class MyDatetime(): + def now(self): + return now + self.useFixture(fixtures.MonkeyPatch('heat.db.sqlalchemy.api.datetime', + MyDatetime())) + + db_api.purge_deleted(age=1, granularity='days') + self._deleted_stack_existance(utils.dummy_context(), stacks, + (0, 1, 2), (3, 4)) + + db_api.purge_deleted(age=22, granularity='hours') + self._deleted_stack_existance(utils.dummy_context(), stacks, + (0, 1, 2), (3, 4)) + + db_api.purge_deleted(age=1100, granularity='minutes') + self._deleted_stack_existance(utils.dummy_context(), stacks, + (0, 1), (2, 3, 4)) + + db_api.purge_deleted(age=3600, granularity='seconds') + self._deleted_stack_existance(utils.dummy_context(), stacks, + (), (0, 1, 2, 3, 4)) + + def _deleted_stack_existance(self, ctx, stacks, existing, deleted): + for s in existing: + self.assertIsNotNone(db_api.stack_get(ctx, stacks[s].id, + show_deleted=True)) + for s in deleted: + self.assertIsNone(db_api.stack_get(ctx, stacks[s].id, + show_deleted=True)) + class DBAPIResourceTest(HeatTestCase): def setUp(self):