diff --git a/glance/cmd/manage.py b/glance/cmd/manage.py index cefa89275b..550f462bd3 100644 --- a/glance/cmd/manage.py +++ b/glance/cmd/manage.py @@ -329,24 +329,17 @@ class DbCommands(object): metadata.db_export_metadefs(db_api.get_engine(), path) - @args('--age_in_days', type=int, - help='Purge deleted rows older than age in days') - @args('--max_rows', type=int, - help='Limit number of records to delete') - def purge(self, age_in_days=30, max_rows=100): - """Purge deleted rows older than a given age from glance tables.""" + def _purge(self, age_in_days, max_rows, purge_images_only=False): try: age_in_days = int(age_in_days) except ValueError: sys.exit(_("Invalid int value for age_in_days: " "%(age_in_days)s") % {'age_in_days': age_in_days}) - try: max_rows = int(max_rows) except ValueError: sys.exit(_("Invalid int value for max_rows: " "%(max_rows)s") % {'max_rows': max_rows}) - if age_in_days < 0: sys.exit(_("Must supply a non-negative value for age.")) if age_in_days >= (int(time.time()) / 86400): @@ -354,15 +347,34 @@ class DbCommands(object): if max_rows < 1: sys.exit(_("Minimal rows limit is 1.")) ctx = context.get_admin_context(show_deleted=True) - try: - db_api.purge_deleted_rows(ctx, age_in_days, max_rows) + if purge_images_only: + db_api.purge_deleted_rows_from_images(ctx, age_in_days, + max_rows) + else: + db_api.purge_deleted_rows(ctx, age_in_days, max_rows) except exception.Invalid as exc: sys.exit(exc.msg) except db_exc.DBReferenceError: sys.exit(_("Purge command failed, check glance-manage" " logs for more details.")) + @args('--age_in_days', type=int, + help='Purge deleted rows older than age in days') + @args('--max_rows', type=int, + help='Limit number of records to delete') + def purge(self, age_in_days=30, max_rows=100): + """Purge deleted rows older than a given age from glance tables.""" + self._purge(age_in_days, max_rows) + + @args('--age_in_days', type=int, + help='Purge deleted rows older than age in days') + @args('--max_rows', type=int, + help='Limit number of records to delete') + def purge_images_table(self, age_in_days=30, max_rows=100): + """Purge deleted rows older than a given age from images table.""" + self._purge(age_in_days, max_rows, purge_images_only=True) + class DbLegacyCommands(object): """Class for managing the db using legacy commands""" diff --git a/glance/db/sqlalchemy/api.py b/glance/db/sqlalchemy/api.py index 44656e62da..8fca7b7b9f 100644 --- a/glance/db/sqlalchemy/api.py +++ b/glance/db/sqlalchemy/api.py @@ -1322,6 +1322,11 @@ def purge_deleted_rows(context, age_in_days, max_rows, session=None): LOG.warning(_LW('Expected table %(tbl)s was not found in DB.'), {'tbl': tbl}) else: + # NOTE(abhishekk): To mitigate OSSN-0075 images records should be + # purged with new ``purge-images-table`` command. + if tbl == 'images': + continue + tables.append(tbl) for tbl in tables: @@ -1354,6 +1359,50 @@ def purge_deleted_rows(context, age_in_days, max_rows, session=None): {'rows': rows, 'tbl': tbl}) +def purge_deleted_rows_from_images(context, age_in_days, max_rows, + session=None): + """Purges soft deleted rows + + Deletes rows of table images table according to given age for + relevant models. + """ + # check max_rows for its maximum limit + _validate_db_int(max_rows=max_rows) + + session = session or get_session() + metadata = MetaData(get_engine()) + deleted_age = timeutils.utcnow() - datetime.timedelta(days=age_in_days) + + tbl = 'images' + tab = Table(tbl, metadata, autoload=True) + LOG.info( + _LI('Purging deleted rows older than %(age_in_days)d day(s) ' + 'from table %(tbl)s'), + {'age_in_days': age_in_days, 'tbl': tbl}) + + column = tab.c.id + deleted_at_column = tab.c.deleted_at + + query_delete = sql.select( + [column], deleted_at_column < deleted_age).order_by( + deleted_at_column).limit(max_rows) + + delete_statement = DeleteFromSelect(tab, query_delete, column) + + try: + with session.begin(): + result = session.execute(delete_statement) + except db_exception.DBReferenceError as ex: + with excutils.save_and_reraise_exception(): + LOG.error(_LE('DBError detected when purging from ' + "%(tablename)s: %(error)s"), + {'tablename': tbl, 'error': six.text_type(ex)}) + + rows = result.rowcount + LOG.info(_LI('Deleted %(rows)d row(s) from table %(tbl)s'), + {'rows': rows, 'tbl': tbl}) + + def user_get_storage_usage(context, owner_id, image_id=None, session=None): _check_image_id(image_id) session = session or get_session() diff --git a/glance/tests/functional/db/base.py b/glance/tests/functional/db/base.py index 803025278a..0b08f83702 100644 --- a/glance/tests/functional/db/base.py +++ b/glance/tests/functional/db/base.py @@ -1990,11 +1990,27 @@ class DBPurgeTests(test_utils.BaseTestCase): def test_db_purge(self): self.db_api.purge_deleted_rows(self.adm_context, 1, 5) images = self.db_api.image_get_all(self.adm_context) + + # Verify that no records from images have been deleted + # as images table will be purged using 'purge_images_table' + # command. + self.assertEqual(len(images), 3) + tasks = self.db_api.task_get_all(self.adm_context) + self.assertEqual(len(tasks), 2) + + def test_db_purge_images_table(self): + # purge records from images_tags table + self.db_api.purge_deleted_rows(self.adm_context, 1, 5) + + # purge records from images table + self.db_api.purge_deleted_rows_from_images(self.adm_context, 1, 5) + images = self.db_api.image_get_all(self.adm_context) + self.assertEqual(len(images), 2) tasks = self.db_api.task_get_all(self.adm_context) self.assertEqual(len(tasks), 2) - def test_purge_fk_constraint_failure(self): + def test_purge_images_table_fk_constraint_failure(self): """Test foreign key constraint failure Test whether foreign key constraint failure during purge @@ -2053,7 +2069,7 @@ class DBPurgeTests(test_utils.BaseTestCase): # Purge all records deleted at least 10 days ago self.assertRaises(db_exception.DBReferenceError, - db_api.purge_deleted_rows, + db_api.purge_deleted_rows_from_images, self.adm_context, age_in_days=10, max_rows=50)