diff --git a/nova/db/api.py b/nova/db/api.py index b109e1baa7a4..69a521b18920 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -568,9 +568,13 @@ def migration_get_in_progress_by_host_and_node(context, host, node): return IMPL.migration_get_in_progress_by_host_and_node(context, host, node) -def migration_get_all_by_filters(context, filters): +def migration_get_all_by_filters(context, filters, sort_keys=None, + sort_dirs=None, limit=None, marker=None): """Finds all migrations using the provided filters.""" - return IMPL.migration_get_all_by_filters(context, filters) + return IMPL.migration_get_all_by_filters(context, filters, + sort_keys=sort_keys, + sort_dirs=sort_dirs, + limit=limit, marker=marker) def migration_get_in_progress_by_instance(context, instance_uuid, diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index a753ef974ead..13171f7fa415 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -4391,8 +4391,17 @@ def migration_get_in_progress_by_instance(context, instance_uuid, @pick_context_manager_reader -def migration_get_all_by_filters(context, filters): +def migration_get_all_by_filters(context, filters, + sort_keys=None, sort_dirs=None, + limit=None, marker=None): + if limit == 0: + return [] + query = model_query(context, models.Migration) + if 'changes-since' in filters: + changes_since = timeutils.normalize_time(filters['changes-since']) + query = query. \ + filter(models.Migration.updated_at >= changes_since) if "status" in filters: status = filters["status"] status = [status] if isinstance(status, six.string_types) else status @@ -4411,9 +4420,25 @@ def migration_get_all_by_filters(context, filters): hidden = filters["hidden"] query = query.filter(models.Migration.hidden == hidden) if "instance_uuid" in filters: - uuid = filters["instance_uuid"] - query = query.filter(models.Migration.instance_uuid == uuid) - return query.all() + instance_uuid = filters["instance_uuid"] + query = query.filter(models.Migration.instance_uuid == instance_uuid) + if marker: + try: + marker = migration_get_by_uuid(context, marker) + except exception.MigrationNotFound: + raise exception.MarkerNotFound(marker=marker) + if limit or marker or sort_keys or sort_dirs: + # Default sort by desc(['created_at', 'id']) + sort_keys, sort_dirs = process_sort_params(sort_keys, sort_dirs, + default_dir='desc') + return sqlalchemyutils.paginate_query(query, + models.Migration, + limit=limit, + sort_keys=sort_keys, + marker=marker, + sort_dirs=sort_dirs).all() + else: + return query.all() @pick_context_manager_writer diff --git a/nova/objects/migration.py b/nova/objects/migration.py index a097a17a6aac..8393a108b8b1 100644 --- a/nova/objects/migration.py +++ b/nova/objects/migration.py @@ -185,7 +185,9 @@ class MigrationList(base.ObjectListBase, base.NovaObject): # Version 1.2: Migration version 1.2 # Version 1.3: Added a new function to get in progress migrations # for an instance. - VERSION = '1.3' + # Version 1.4: Added sort_keys, sort_dirs, limit, marker kwargs to + # get_by_filters for migrations pagination support. + VERSION = '1.4' fields = { 'objects': fields.ListOfObjectsField('Migration'), @@ -214,8 +216,11 @@ class MigrationList(base.ObjectListBase, base.NovaObject): db_migrations) @base.remotable_classmethod - def get_by_filters(cls, context, filters): - db_migrations = db.migration_get_all_by_filters(context, filters) + def get_by_filters(cls, context, filters, sort_keys=None, sort_dirs=None, + limit=None, marker=None): + db_migrations = db.migration_get_all_by_filters( + context, filters, sort_keys=sort_keys, sort_dirs=sort_dirs, + limit=limit, marker=marker) return base.obj_make_list(context, cls(context), objects.Migration, db_migrations) diff --git a/nova/tests/unit/db/test_db_api.py b/nova/tests/unit/db/test_db_api.py index 01136c48b575..0af8cbdcff54 100644 --- a/nova/tests/unit/db/test_db_api.py +++ b/nova/tests/unit/db/test_db_api.py @@ -1454,7 +1454,8 @@ class MigrationTestCase(test.TestCase): def _create(self, status='migrating', source_compute='host1', source_node='a', dest_compute='host2', dest_node='b', - system_metadata=None, migration_type=None, uuid=None): + system_metadata=None, migration_type=None, uuid=None, + created_at=None, updated_at=None): values = {'host': source_compute} instance = db.instance_create(self.ctxt, values) @@ -1466,6 +1467,10 @@ class MigrationTestCase(test.TestCase): 'source_node': source_node, 'dest_compute': dest_compute, 'dest_node': dest_node, 'instance_uuid': instance['uuid'], 'migration_type': migration_type, 'uuid': uuid} + if created_at: + values['created_at'] = created_at + if updated_at: + values['updated_at'] = updated_at db.migration_create(self.ctxt, values) return values @@ -1699,6 +1704,74 @@ class MigrationTestCase(test.TestCase): db.migration_get_by_id_and_instance, self.ctxt, '500', '501') + def _create_3_migration_after_time(self, time=None): + time = time or timeutils.utcnow() + tmp_time = time + datetime.timedelta(days=1) + after_1hour = datetime.timedelta(hours=1) + self._create(uuid=uuidsentinel.uuid_time1, created_at=tmp_time, + updated_at=tmp_time + after_1hour) + tmp_time = time + datetime.timedelta(days=2) + self._create(uuid=uuidsentinel.uuid_time2, created_at=tmp_time, + updated_at=tmp_time + after_1hour) + tmp_time = time + datetime.timedelta(days=3) + self._create(uuid=uuidsentinel.uuid_time3, created_at=tmp_time, + updated_at=tmp_time + after_1hour) + + def test_get_migrations_by_filters_with_limit(self): + migrations = db.migration_get_all_by_filters(self.ctxt, {}, limit=3) + self.assertEqual(3, len(migrations)) + + def test_get_migrations_by_filters_with_limit_marker(self): + self._create_3_migration_after_time() + # order by created_at, desc: time3, time2, time1 + migrations = db.migration_get_all_by_filters( + self.ctxt, {}, limit=2, marker=uuidsentinel.uuid_time3) + # time3 as marker: time2, time1 + self.assertEqual(2, len(migrations)) + self.assertEqual(migrations[0]['uuid'], uuidsentinel.uuid_time2) + self.assertEqual(migrations[1]['uuid'], uuidsentinel.uuid_time1) + # time3 as marker, limit 2: time3, time2 + migrations = db.migration_get_all_by_filters( + self.ctxt, {}, limit=1, marker=uuidsentinel.uuid_time3) + self.assertEqual(1, len(migrations)) + self.assertEqual(migrations[0]['uuid'], uuidsentinel.uuid_time2) + + def test_get_migrations_by_filters_with_limit_marker_sort(self): + self._create_3_migration_after_time() + # order by created_at, desc: time3, time2, time1 + migrations = db.migration_get_all_by_filters( + self.ctxt, {}, limit=2, marker=uuidsentinel.uuid_time3) + # time2, time1 + self.assertEqual(2, len(migrations)) + self.assertEqual(migrations[0]['uuid'], uuidsentinel.uuid_time2) + self.assertEqual(migrations[1]['uuid'], uuidsentinel.uuid_time1) + + # order by updated_at, desc: time1, time2, time3 + migrations = db.migration_get_all_by_filters( + self.ctxt, {}, sort_keys=['updated_at'], sort_dirs=['asc'], + limit=2, marker=uuidsentinel.uuid_time1) + # time2, time3 + self.assertEqual(2, len(migrations)) + self.assertEqual(migrations[0]['uuid'], uuidsentinel.uuid_time2) + self.assertEqual(migrations[1]['uuid'], uuidsentinel.uuid_time3) + + def test_get_migrations_by_filters_with_not_found_marker(self): + self.assertRaises(exception.MarkerNotFound, + db.migration_get_all_by_filters, self.ctxt, {}, + marker=uuidsentinel.not_found_marker) + + def test_get_migrations_by_filters_with_changes_since(self): + changes_time = timeutils.utcnow(with_timezone=True) + self._create_3_migration_after_time(changes_time) + after_1day_2hours = datetime.timedelta(days=1, hours=2) + filters = {"changes-since": changes_time + after_1day_2hours} + migrations = db.migration_get_all_by_filters( + self.ctxt, filters, + sort_keys=['updated_at'], sort_dirs=['asc']) + self.assertEqual(2, len(migrations)) + self.assertEqual(migrations[0]['uuid'], uuidsentinel.uuid_time2) + self.assertEqual(migrations[1]['uuid'], uuidsentinel.uuid_time3) + class ModelsObjectComparatorMixin(object): def _dict_from_object(self, obj, ignored_keys): diff --git a/nova/tests/unit/objects/test_migration.py b/nova/tests/unit/objects/test_migration.py index c1ee821c3c76..bda9a5c8dbfb 100644 --- a/nova/tests/unit/objects/test_migration.py +++ b/nova/tests/unit/objects/test_migration.py @@ -215,7 +215,9 @@ class _TestMigrationObject(object): self.assertEqual(2, len(migrations)) for index, db_migration in enumerate(db_migrations): self.compare_obj(migrations[index], db_migration) - mock_get.assert_called_once_with(ctxt, filters) + mock_get.assert_called_once_with(ctxt, filters, + sort_dirs=None, sort_keys=None, + limit=None, marker=None) def test_migrate_old_resize_record(self): db_migration = dict(fake_db_migration(), migration_type=None) diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index 2cea737bc39e..d881e3d25146 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1125,7 +1125,7 @@ object_data = { 'MemoryDiagnostics': '1.0-2c995ae0f2223bb0f8e523c5cc0b83da', 'Migration': '1.5-48bebaada664ee15bc23b35b2b814d75', 'MigrationContext': '1.1-9fb17b0b521370957a884636499df52d', - 'MigrationList': '1.3-55595bfc1a299a5962614d0821a3567e', + 'MigrationList': '1.4-983a9c29d4f1e747ce719dc9063b729b', 'MonitorMetric': '1.1-53b1db7c4ae2c531db79761e7acc52ba', 'MonitorMetricList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e', 'NicDiagnostics': '1.0-895e9ad50e0f56d5258585e3e066aea5',