"""Implementation of SQLAlchemy backend."""
+from datetime import datetime
+from datetime import timedelta
import functools
import sys
import threading
import osprofiler.sqlalchemy
import six
import sqlalchemy
+from sqlalchemy import MetaData
from sqlalchemy import or_
from sqlalchemy.orm import joinedload, joinedload_all
from sqlalchemy.orm import RelationshipProperty
+from sqlalchemy.schema import Table
from sqlalchemy.sql.expression import literal_column
from sqlalchemy.sql.expression import true
from sqlalchemy.sql import func
from cinder.common import sqlalchemyutils
from cinder.db.sqlalchemy import models
from cinder import exception
-from cinder.i18n import _, _LW
+from cinder.i18n import _, _LW, _LE, _LI
from cinder.openstack.common import log as logging
from cinder.openstack.common import uuidutils
'deleted': True,
'deleted_at': timeutils.utcnow(),
'updated_at': literal_column('updated_at')})
+
+
+@require_admin_context
+def purge_deleted_rows(context, age_in_days):
+ """Purge deleted rows older than age from cinder tables."""
+ try:
+ age_in_days = int(age_in_days)
+ except ValueError:
+ msg = _LE('Invalid valude for age, %(age)s')
+ LOG.exception(msg, {'age': age_in_days})
+ raise exception.InvalidParameterValue(msg % {'age': age_in_days})
+ if age_in_days <= 0:
+ msg = _LE('Must supply a positive value for age')
+ LOG.exception(msg)
+ raise exception.InvalidParameterValue(msg)
+
+ engine = get_engine()
+ session = get_session()
+ metadata = MetaData()
+ metadata.bind = engine
+ tables = []
+
+ for model_class in models.__dict__.itervalues():
+ if hasattr(model_class, "__tablename__"):
+ tables.append(model_class.__tablename__)
+
+ # Reorder the list so the volumes table is last to avoid FK constraints
+ tables.remove("volumes")
+ tables.append("volumes")
+ for table in tables:
+ t = Table(table, metadata, autoload=True)
+ LOG.info(_LI('Purging deleted rows older than age=%(age)d days '
+ 'from table=%(table)s'), {'age': age_in_days,
+ 'table': table})
+ deleted_age = datetime.now() - timedelta(days=age_in_days)
+ try:
+ with session.begin():
+ result = session.execute(
+ t.delete()
+ .where(t.c.deleted_at < deleted_age))
+ except db_exc.DBReferenceError:
+ LOG.exception(_LE('DBError detected when purging from '
+ 'table=%(table)s'), {'table': table})
+ raise
+
+ rows_purged = result.rowcount
+ LOG.info(_LI("Deleted %(row)d rows from table=%(table)s"),
+ {'row': rows_purged, 'table': table})
--- /dev/null
+# Copyright (C) 2015 OpenStack Foundation
+# All Rights Reserved.
+#
+# 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.
+
+"""Tests for db purge."""
+
+from datetime import datetime
+from datetime import timedelta
+import uuid
+
+from cinder import context
+from cinder import db
+from cinder.db.sqlalchemy import api as db_api
+from cinder import exception
+from cinder.openstack.common import log as logging
+from cinder import test
+
+from oslo_db.sqlalchemy import utils as sqlalchemyutils
+
+
+LOG = logging.getLogger(__name__)
+
+
+class PurgeDeletedTest(test.TestCase):
+
+ def setUp(self):
+ super(PurgeDeletedTest, self).setUp()
+ self.context = context.get_admin_context()
+ self.engine = db_api.get_engine()
+ self.session = db_api.get_session()
+ self.conn = self.engine.connect()
+ self.volumes = sqlalchemyutils.get_table(
+ self.engine, "volumes")
+ # The volume_metadata table has a FK of volume_id
+ self.vm = sqlalchemyutils.get_table(
+ self.engine, "volume_metadata")
+ self.uuidstrs = []
+ for unused in range(6):
+ self.uuidstrs.append(uuid.uuid4().hex)
+ # Add 6 rows to table
+ for uuidstr in self.uuidstrs:
+ ins_stmt = self.volumes.insert().values(id=uuidstr)
+ self.conn.execute(ins_stmt)
+ ins_stmt = self.vm.insert().values(volume_id=uuidstr)
+ self.conn.execute(ins_stmt)
+ # Set 4 of them deleted, 2 are 60 days ago, 2 are 20 days ago
+ old = datetime.now() - timedelta(days=20)
+ older = datetime.now() - timedelta(days=60)
+ make_old = self.volumes.update().\
+ where(self.volumes.c.id.in_(self.uuidstrs[1:3]))\
+ .values(deleted_at=old)
+ make_older = self.volumes.update().\
+ where(self.volumes.c.id.in_(self.uuidstrs[4:6]))\
+ .values(deleted_at=older)
+ make_meta_old = self.vm.update().\
+ where(self.vm.c.volume_id.in_(self.uuidstrs[1:3]))\
+ .values(deleted_at=old)
+ make_meta_older = self.vm.update().\
+ where(self.vm.c.volume_id.in_(self.uuidstrs[4:6]))\
+ .values(deleted_at=older)
+ self.conn.execute(make_old)
+ self.conn.execute(make_older)
+ self.conn.execute(make_meta_old)
+ self.conn.execute(make_meta_older)
+
+ def test_purge_deleted_rows_old(self):
+ # Purge at 30 days old, should only delete 2 rows
+ db.purge_deleted_rows(self.context, age_in_days=30)
+ rows = self.session.query(self.volumes).count()
+ meta_rows = self.session.query(self.vm).count()
+ # Verify that we only deleted 2
+ self.assertEqual(4, rows)
+ self.assertEqual(4, meta_rows)
+
+ def test_purge_deleted_rows_older(self):
+ # Purge at 10 days old now, should delete 2 more rows
+ db.purge_deleted_rows(self.context, age_in_days=10)
+ rows = self.session.query(self.volumes).count()
+ meta_rows = self.session.query(self.vm).count()
+ # Verify that we only have 2 rows now
+ self.assertEqual(2, rows)
+ self.assertEqual(2, meta_rows)
+
+ def test_purge_deleted_rows_bad_args(self):
+ # Test with no age argument
+ self.assertRaises(TypeError, db.purge_deleted_rows, self.context)
+ # Test purge with non-integer
+ self.assertRaises(exception.InvalidParameterValue,
+ db.purge_deleted_rows, self.context,
+ age_in_days='ten')
+ # Test with negative value
+ self.assertRaises(exception.InvalidParameterValue,
+ db.purge_deleted_rows, self.context,
+ age_in_days=-1)