diff --git a/coriolis/api/v1/deployments.py b/coriolis/api/v1/deployments.py index 2f62ddd7..b57ce457 100644 --- a/coriolis/api/v1/deployments.py +++ b/coriolis/api/v1/deployments.py @@ -8,6 +8,7 @@ from coriolis.api.v1 import utils as api_utils from coriolis.api.v1.views import deployment_view from coriolis.api import wsgi as api_wsgi +from coriolis import constants from coriolis.deployments import api from coriolis.endpoints import api as endpoints_api from coriolis import exception @@ -36,6 +37,18 @@ def show(self, req, id): return deployment_view.single(deployment) + def _get_filters(self, req) -> dict: + filters = {} + # For simplicity and consistency, we'll use "status" to search for a + # given "last_execution_status". + status = req.GET.get("status") + if status is not None: + if status not in constants.ALL_EXECUTION_STATUSES: + raise exc.HTTPBadRequest( + explanation=f"Unknown deployment status: {status}") + filters["status"] = status + return filters + def _list(self, req): show_deleted = api_utils.get_bool_url_arg( req, "show_deleted", default=False) @@ -47,6 +60,7 @@ def _list(self, req): marker, limit = common.get_paging_params(req) sort_keys, sort_dirs = common.get_sort_params(req) + filters = self._get_filters(req) return deployment_view.collection( self._deployment_api.get_deployments( @@ -55,6 +69,7 @@ def _list(self, req): include_task_info=include_task_info, marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, + filters=filters, )) def index(self, req): diff --git a/coriolis/api/v1/transfer_tasks_executions.py b/coriolis/api/v1/transfer_tasks_executions.py index 6fcb5eef..ad3ee8a8 100644 --- a/coriolis/api/v1/transfer_tasks_executions.py +++ b/coriolis/api/v1/transfer_tasks_executions.py @@ -4,6 +4,7 @@ from coriolis.api import common from coriolis.api.v1.views import transfer_tasks_execution_view from coriolis.api import wsgi as api_wsgi +from coriolis import constants from coriolis import exception from coriolis.policies import transfer_tasks_executions as executions_policies from coriolis.transfer_tasks_executions import api @@ -27,6 +28,16 @@ def show(self, req, transfer_id, id): return transfer_tasks_execution_view.single(execution) + def _get_filters(self, req) -> dict: + filters = {} + status = req.GET.get("status") + if status is not None: + if status not in constants.ALL_EXECUTION_STATUSES: + raise exc.HTTPBadRequest( + explanation=f"Unknown task execution status: {status}") + filters["status"] = status + return filters + def index(self, req, transfer_id): context = req.environ["coriolis.context"] context.can( @@ -34,12 +45,14 @@ def index(self, req, transfer_id): marker, limit = common.get_paging_params(req) sort_keys, sort_dirs = common.get_sort_params(req) + filters = self._get_filters(req) return transfer_tasks_execution_view.collection( self._transfer_tasks_execution_api.get_executions( context, transfer_id, include_tasks=False, marker=marker, limit=limit, - sort_keys=sort_keys, sort_dirs=sort_dirs)) + sort_keys=sort_keys, sort_dirs=sort_dirs, + filters=filters)) def detail(self, req, transfer_id): context = req.environ["coriolis.context"] diff --git a/coriolis/api/v1/transfers.py b/coriolis/api/v1/transfers.py index 92df1fde..eca1e725 100644 --- a/coriolis/api/v1/transfers.py +++ b/coriolis/api/v1/transfers.py @@ -42,6 +42,16 @@ def show(self, req, id): return transfer_view.single(transfer) + def _get_filters(self, req) -> dict: + filters = {} + status = req.GET.get("status") + if status is not None: + if status not in constants.ALL_TASK_STATUSES: + raise exc.HTTPBadRequest( + explanation=f"Unknown task status: {status}") + filters["status"] = status + return filters + def _list(self, req): show_deleted = api_utils.get_bool_url_arg( req, "show_deleted", default=False) @@ -52,6 +62,7 @@ def _list(self, req): req, "include_task_info", default=False) marker, limit = common.get_paging_params(req) sort_keys, sort_dirs = common.get_sort_params(req) + filters = self._get_filters(req) return transfer_view.collection( self._transfer_api.get_transfers( context, @@ -59,6 +70,7 @@ def _list(self, req): include_task_info=include_task_info, marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, + filters=filters, )) def index(self, req): diff --git a/coriolis/conductor/rpc/client.py b/coriolis/conductor/rpc/client.py index 0e71d088..9a364fc8 100644 --- a/coriolis/conductor/rpc/client.py +++ b/coriolis/conductor/rpc/client.py @@ -147,7 +147,8 @@ def get_transfer_tasks_executions(self, ctxt, transfer_id, marker=None, limit=None, sort_keys=None, - sort_dirs=None): + sort_dirs=None, + filters=None): return self._call( ctxt, 'get_transfer_tasks_executions', transfer_id=transfer_id, @@ -156,6 +157,7 @@ def get_transfer_tasks_executions(self, ctxt, transfer_id, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, + filters=filters, ) def get_transfer_tasks_execution(self, ctxt, transfer_id, execution_id, @@ -208,7 +210,8 @@ def create_instances_transfer(self, ctxt, def get_transfers(self, ctxt, include_tasks_executions=False, include_task_info=False, marker=None, limit=None, - sort_keys=None, sort_dirs=None): + sort_keys=None, sort_dirs=None, + filters=None): return self._call( ctxt, 'get_transfers', include_tasks_executions=include_tasks_executions, @@ -217,6 +220,7 @@ def get_transfers(self, ctxt, include_tasks_executions=False, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, + filters=filters, ) def get_transfer(self, ctxt, transfer_id, include_task_info=False): @@ -235,7 +239,8 @@ def delete_transfer_disks(self, ctxt, transfer_id): def get_deployments(self, ctxt, include_tasks=False, include_task_info=False, marker=None, limit=None, - sort_keys=None, sort_dirs=None): + sort_keys=None, sort_dirs=None, + filters=None): return self._call( ctxt, 'get_deployments', include_tasks=include_tasks, include_task_info=include_task_info, @@ -243,6 +248,7 @@ def get_deployments(self, ctxt, include_tasks=False, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, + filters=filters, ) def get_deployment(self, ctxt, deployment_id, include_task_info=False): diff --git a/coriolis/conductor/rpc/server.py b/coriolis/conductor/rpc/server.py index 98bec5e1..b39f1b0d 100644 --- a/coriolis/conductor/rpc/server.py +++ b/coriolis/conductor/rpc/server.py @@ -1156,7 +1156,8 @@ def get_transfer_tasks_executions(self, ctxt, transfer_id, marker=None, limit=None, sort_keys=None, - sort_dirs=None): + sort_dirs=None, + filters=None): return db_api.get_transfer_tasks_executions( ctxt, transfer_id, include_tasks, include_task_info=include_task_info, @@ -1164,6 +1165,7 @@ def get_transfer_tasks_executions(self, ctxt, transfer_id, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, + filters=filters, to_dict=True) @tasks_execution_synchronized @@ -1217,7 +1219,8 @@ def _get_transfer_tasks_execution(ctxt, transfer_id, execution_id, def get_transfers(ctxt, include_tasks_executions=False, include_task_info=False, marker=None, limit=None, - sort_keys=None, sort_dirs=None): + sort_keys=None, sort_dirs=None, + filters=None): return db_api.get_transfers( ctxt, include_tasks_executions=include_tasks_executions, include_task_info=include_task_info, @@ -1225,6 +1228,7 @@ def get_transfers(ctxt, include_tasks_executions=False, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, + filters=filters, to_dict=True) @transfer_synchronized @@ -1383,7 +1387,8 @@ def _get_transfer(self, ctxt, transfer_id, include_task_info=False, @staticmethod def get_deployments(ctxt, include_tasks, include_task_info=False, marker=None, limit=None, - sort_keys=None, sort_dirs=None): + sort_keys=None, sort_dirs=None, + filters=None): return db_api.get_deployments( ctxt, include_tasks, include_task_info=include_task_info, @@ -1391,6 +1396,7 @@ def get_deployments(ctxt, include_tasks, include_task_info=False, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, + filters=filters, to_dict=True) @deployment_synchronized diff --git a/coriolis/constants.py b/coriolis/constants.py index 270bf47d..599a5791 100644 --- a/coriolis/constants.py +++ b/coriolis/constants.py @@ -33,6 +33,11 @@ EXECUTION_STATUS_ERROR_ALLOCATING_MINIONS ] +ALL_EXECUTION_STATUSES = ( + ACTIVE_EXECUTION_STATUSES + + FINALIZED_EXECUTION_STATUSES +) + TASK_STATUS_SCHEDULED = "SCHEDULED" TASK_STATUS_PENDING = "PENDING" TASK_STATUS_STARTING = "STARTING" @@ -83,6 +88,12 @@ TASK_STATUS_FAILED_TO_CANCEL ] +ALL_TASK_STATUSES = ( + ACTIVE_TASK_STATUSES + + CANCELED_TASK_STATUSES + + FINALIZED_TASK_STATUSES +) + TASK_TYPE_FINALIZE_INSTANCE_DEPLOYMENT = "FINALIZE_INSTANCE_DEPLOYMENT" TASK_TYPE_CLEANUP_FAILED_INSTANCE_DEPLOYMENT = ( "CLEANUP_FAILED_INSTANCE_DEPLOYMENT") diff --git a/coriolis/db/api.py b/coriolis/db/api.py index 6576d207..c6d5f3f4 100644 --- a/coriolis/db/api.py +++ b/coriolis/db/api.py @@ -1,6 +1,7 @@ # Copyright 2016 Cloudbase Solutions Srl # All Rights Reserved. +import copy import uuid from oslo_config import cfg @@ -281,6 +282,7 @@ def get_transfer_tasks_executions(context, transfer_id, include_tasks=False, limit=None, sort_keys: list[str] | None = None, sort_dirs: list[str] | None = None, + filters: dict | None = None, to_dict=False): q = _soft_delete_aware_query(context, models.TasksExecution) q = q.join(models.Transfer) @@ -293,6 +295,13 @@ def get_transfer_tasks_executions(context, transfer_id, include_tasks=False, q = q.filter(models.Transfer.id == transfer_id) + filters = copy.deepcopy(filters or {}) + if "status" in filters: + status = filters.pop("status") + q = q.filter(models.TasksExecution.status == status) + if filters: + raise ValueError("Unsupported filters: %s" % filters) + sort_keys, sort_dirs = process_sort_params( sort_keys, sort_dirs, @@ -458,6 +467,7 @@ def get_transfers(context, limit=None, sort_keys: list[str] | None = None, sort_dirs: list[str] | None = None, + filters: dict | None = None, to_dict=False): q = _soft_delete_aware_query(context, models.Transfer) if include_tasks_executions: @@ -471,6 +481,13 @@ def get_transfers(context, q = q.filter( models.Transfer.project_id == context.project_id) + filters = copy.deepcopy(filters or {}) + if "status" in filters: + status = filters.pop("status") + q = q.filter(models.Transfer.last_execution_status == status) + if filters: + raise ValueError("Unsupported filters: %s" % filters) + sort_keys, sort_dirs = process_sort_params( sort_keys, sort_dirs, @@ -588,6 +605,7 @@ def get_deployments(context, limit=None, sort_keys: list[str] | None = None, sort_dirs: list[str] | None = None, + filters: dict | None = None, to_dict=False): q = _soft_delete_aware_query(context, models.Deployment) if include_tasks: @@ -600,6 +618,13 @@ def get_deployments(context, if is_user_context(context): q = q.filter_by(project_id=context.project_id) + filters = copy.deepcopy(filters or {}) + if "status" in filters: + status = filters.pop("status") + q = q.filter(models.Deployment.last_execution_status == status) + if filters: + raise ValueError("Unsupported filters: %s" % filters) + sort_keys, sort_dirs = process_sort_params( sort_keys, sort_dirs, diff --git a/coriolis/deployer_manager/rpc/server.py b/coriolis/deployer_manager/rpc/server.py index 21f59696..a76a6198 100644 --- a/coriolis/deployer_manager/rpc/server.py +++ b/coriolis/deployer_manager/rpc/server.py @@ -123,10 +123,10 @@ def _loop(self): try: deployments = self._rpc_conductor_client.get_deployments( self._admin_ctx, include_tasks=False, - include_task_info=False) - for d in deployments: - if d['last_execution_status'] == PENDING_STATUS: - self._check_deployer_status(d['id']) + include_task_info=False, + filters={'status': PENDING_STATUS}) + for pending_deployment in deployments: + self._check_deployer_status(pending_deployment['id']) except Exception: LOG.warning( f"Deployer manager failed to list pending deployments. " diff --git a/coriolis/deployments/api.py b/coriolis/deployments/api.py index 695665fd..ad6274fc 100644 --- a/coriolis/deployments/api.py +++ b/coriolis/deployments/api.py @@ -28,11 +28,14 @@ def cancel(self, ctxt, deployment_id, force): def get_deployments(self, ctxt, include_tasks=False, include_task_info=False, marker=None, limit=None, - sort_keys=None, sort_dirs=None): + sort_keys=None, sort_dirs=None, + filters=None): return self._rpc_client.get_deployments( ctxt, include_tasks, include_task_info=include_task_info, marker=marker, limit=limit, - sort_keys=sort_keys, sort_dirs=sort_dirs) + sort_keys=sort_keys, sort_dirs=sort_dirs, + filters=filters, + ) def get_deployment(self, ctxt, deployment_id, include_task_info=False): return self._rpc_client.get_deployment( diff --git a/coriolis/tests/api/v1/test_transfer_tasks_executions.py b/coriolis/tests/api/v1/test_transfer_tasks_executions.py index 0bf6fabb..a5984cc2 100644 --- a/coriolis/tests/api/v1/test_transfer_tasks_executions.py +++ b/coriolis/tests/api/v1/test_transfer_tasks_executions.py @@ -95,6 +95,9 @@ def test_index( mock.sentinel.marker, mock.sentinel.limit, ) + mock_req.GET = { + "status": "RUNNING", + } result = self.transfer_api.index(mock_req, transfer_id) @@ -111,6 +114,7 @@ def test_index( limit=mock.sentinel.limit, sort_keys=mock.sentinel.sort_keys, sort_dirs=mock.sentinel.sort_dirs, + filters={"status": "RUNNING"}, ) mock_collection.assert_called_once_with( mock_get_executions.return_value) diff --git a/coriolis/tests/api/v1/test_transfers.py b/coriolis/tests/api/v1/test_transfers.py index 25fdfea9..4581486f 100644 --- a/coriolis/tests/api/v1/test_transfers.py +++ b/coriolis/tests/api/v1/test_transfers.py @@ -107,6 +107,9 @@ def test_list( mock.sentinel.marker, mock.sentinel.limit, ) + mock_req.GET = { + "status": "RUNNING", + } mock_get_bool_url_arg.side_effect = [False, False] @@ -132,6 +135,7 @@ def test_list( limit=mock.sentinel.limit, sort_keys=mock.sentinel.sort_keys, sort_dirs=mock.sentinel.sort_dirs, + filters={"status": "RUNNING"}, ) mock_collection.assert_called_once_with( mock_get_transfers.return_value) diff --git a/coriolis/tests/conductor/rpc/test_client.py b/coriolis/tests/conductor/rpc/test_client.py index ddd3686c..741681f7 100644 --- a/coriolis/tests/conductor/rpc/test_client.py +++ b/coriolis/tests/conductor/rpc/test_client.py @@ -166,6 +166,7 @@ def test_get_transfer_tasks_executions(self): args = { "transfer_id": "mock_transfer_id", "include_tasks": False, + "filters": {"status": "RUNNING"}, **self._mock_pagination_args, } self._test(self.client.get_transfer_tasks_executions, args) @@ -210,6 +211,7 @@ def test_get_transfers(self): args = { "include_tasks_executions": False, "include_task_info": False, + "filters": {"status": "RUNNING"}, **self._mock_pagination_args, } self._test(self.client.get_transfers, args) diff --git a/coriolis/tests/conductor/rpc/test_server.py b/coriolis/tests/conductor/rpc/test_server.py index 096cd33a..c3f132c5 100644 --- a/coriolis/tests/conductor/rpc/test_server.py +++ b/coriolis/tests/conductor/rpc/test_server.py @@ -1451,6 +1451,7 @@ def test_get_transfer_tasks_executions( mock.sentinel.transfer_id, mock.sentinel.execution_id, include_task_info=False, + filters={"status": "RUNNING"}, **self._mock_pagination_args, ) @@ -1464,6 +1465,7 @@ def test_get_transfer_tasks_executions( mock.sentinel.execution_id, include_task_info=False, **self._mock_pagination_args, + filters={"status": "RUNNING"}, to_dict=True, ) @@ -1681,6 +1683,7 @@ def test_get_transfers(self, mock_get_transfers): mock.sentinel.context, include_tasks_executions=False, include_task_info=False, + filters={"status": "RUNNING"}, **self._mock_pagination_args, ) @@ -1692,6 +1695,7 @@ def test_get_transfers(self, mock_get_transfers): mock.sentinel.context, include_tasks_executions=False, include_task_info=False, + filters={"status": "RUNNING"}, to_dict=True, **self._mock_pagination_args, ) diff --git a/coriolis/tests/integration/test_pagination.py b/coriolis/tests/integration/test_api_listing.py similarity index 77% rename from coriolis/tests/integration/test_pagination.py rename to coriolis/tests/integration/test_api_listing.py index fefa125a..d120c984 100755 --- a/coriolis/tests/integration/test_pagination.py +++ b/coriolis/tests/integration/test_api_listing.py @@ -7,6 +7,7 @@ import operator import uuid +from keystoneauth1.exceptions import http as http_exc from oslo_utils import timeutils from coriolis import constants @@ -17,7 +18,7 @@ from coriolis.tests.integration import base -class PaginationTest(base.CoriolisIntegrationTestBase): +class APIListingTestBase(base.CoriolisIntegrationTestBase): FAKE_USER_ID = "fake-user-id" FAKE_PROJECT_ID = "fake-project-id" @@ -175,6 +176,8 @@ def _get_record_summary(record): "created_at": created_at, } + +class PaginationTest(APIListingTestBase): def test_transfer_execution_list(self): executions = self._client.transfer_executions.list( self._transfers[0].id) @@ -295,3 +298,78 @@ def test_transfer_list_pagination(self): ret_transfer_summary = [self._get_record_summary(t) for t in transfers] self.assertEqual( exp_sorted_transfer_summary[2:4], ret_transfer_summary) + + +class APIFilterTest(APIListingTestBase): + @classmethod + def _setup_mocks(cls): + super()._setup_mocks() + + # We'll create a few more resources, some in error state, some in + # completed state. + cls._completed_transfer = cls._create_db_transfer( + origin_endpoint_id=cls._src_endpoint.id, + destination_endpoint_id=cls._dst_endpoint.id, + last_execution_status=constants.TASK_STATUS_COMPLETED, + ) + + cls._failed_execution = cls._create_db_execution( + transfer=cls._completed_transfer, + status=constants.EXECUTION_STATUS_ERROR, + ) + cls._completed_execution = cls._create_db_execution( + transfer=cls._completed_transfer, + status=constants.EXECUTION_STATUS_COMPLETED, + ) + + cls._failed_deployment = cls._create_db_deployment( + transfer_id=cls._completed_transfer.id, + origin_endpoint_id=cls._src_endpoint.id, + destination_endpoint_id=cls._dst_endpoint.id, + last_execution_status=constants.EXECUTION_STATUS_ERROR, + ) + cls._completed_deployment = cls._create_db_deployment( + transfer_id=cls._completed_transfer.id, + origin_endpoint_id=cls._src_endpoint.id, + destination_endpoint_id=cls._dst_endpoint.id, + last_execution_status=constants.EXECUTION_STATUS_COMPLETED, + ) + + def test_transfer_execution_filter(self): + executions = self._client.transfer_executions.list( + self._completed_transfer.id, + filters={"status": constants.EXECUTION_STATUS_COMPLETED}) + ret_summary = [self._get_record_summary(e) for e in executions] + exp_summary = [self._get_record_summary(self._completed_execution)] + self.assertEqual(exp_summary, ret_summary) + + def test_transfer_filter(self): + executions = self._client.transfers.list( + filters={"status": constants.TASK_STATUS_COMPLETED}) + ret_summary = [self._get_record_summary(e) for e in executions] + exp_summary = [self._get_record_summary(self._completed_transfer)] + self.assertEqual(exp_summary, ret_summary) + + def test_deployment_filter(self): + executions = self._client.deployments.list( + filters={"status": constants.EXECUTION_STATUS_COMPLETED}) + ret_summary = [self._get_record_summary(e) for e in executions] + exp_summary = [self._get_record_summary(self._completed_deployment)] + self.assertEqual(exp_summary, ret_summary) + + def test_invalid_filter(self): + self.assertRaises( + http_exc.BadRequest, + self._client.deployments.list, + filters={"status": "fake-status"}) + + self.assertRaises( + http_exc.BadRequest, + self._client.transfers.list, + filters={"status": "fake-status"}) + + self.assertRaises( + http_exc.BadRequest, + self._client.transfer_executions.list, + self._completed_transfer.id, + filters={"status": "fake-status"}) diff --git a/coriolis/tests/transfer_tasks_executions/test_api.py b/coriolis/tests/transfer_tasks_executions/test_api.py index 36192446..46700335 100644 --- a/coriolis/tests/transfer_tasks_executions/test_api.py +++ b/coriolis/tests/transfer_tasks_executions/test_api.py @@ -55,6 +55,7 @@ def test_get_executions(self): mock.sentinel.limit, mock.sentinel.sort_keys, mock.sentinel.sort_dirs, + mock.sentinel.filters, ) self.rpc_client.get_transfer_tasks_executions.assert_called_once_with( @@ -64,6 +65,7 @@ def test_get_executions(self): mock.sentinel.limit, mock.sentinel.sort_keys, mock.sentinel.sort_dirs, + mock.sentinel.filters, ) self.assertEqual( result, self.rpc_client.get_transfer_tasks_executions.return_value) diff --git a/coriolis/tests/transfers/test_api.py b/coriolis/tests/transfers/test_api.py index 9992e9eb..e3889e89 100644 --- a/coriolis/tests/transfers/test_api.py +++ b/coriolis/tests/transfers/test_api.py @@ -70,11 +70,13 @@ def test_delete(self): def test_get_transfers(self): result = self.api.get_transfers( self.ctxt, include_tasks_executions=False, include_task_info=False, + filters={"status": "RUNNING"}, **self._mock_pagination_args, ) self.rpc_client.get_transfers.assert_called_once_with( self.ctxt, False, include_task_info=False, + filters={"status": "RUNNING"}, **self._mock_pagination_args, ) self.assertEqual(result, self.rpc_client.get_transfers.return_value) diff --git a/coriolis/transfer_tasks_executions/api.py b/coriolis/transfer_tasks_executions/api.py index 7158f93e..44877146 100644 --- a/coriolis/transfer_tasks_executions/api.py +++ b/coriolis/transfer_tasks_executions/api.py @@ -22,10 +22,11 @@ def cancel(self, ctxt, transfer_id, execution_id, force): def get_executions(self, ctxt, transfer_id, include_tasks=False, marker=None, limit=None, - sort_keys=None, sort_dirs=None): + sort_keys=None, sort_dirs=None, + filters=None): return self._rpc_client.get_transfer_tasks_executions( ctxt, transfer_id, include_tasks, marker, limit, - sort_keys, sort_dirs) + sort_keys, sort_dirs, filters) def get_execution(self, ctxt, transfer_id, execution_id): return self._rpc_client.get_transfer_tasks_execution( diff --git a/coriolis/transfers/api.py b/coriolis/transfers/api.py index 31ff9623..10aa1156 100644 --- a/coriolis/transfers/api.py +++ b/coriolis/transfers/api.py @@ -34,12 +34,14 @@ def delete(self, ctxt, transfer_id): def get_transfers(self, ctxt, include_tasks_executions=False, include_task_info=False, marker=None, limit=None, - sort_keys=None, sort_dirs=None): + sort_keys=None, sort_dirs=None, + filters=None): return self._rpc_client.get_transfers( ctxt, include_tasks_executions, include_task_info=include_task_info, marker=marker, limit=limit, - sort_keys=sort_keys, sort_dirs=sort_dirs) + sort_keys=sort_keys, sort_dirs=sort_dirs, + filters=filters) def get_transfer(self, ctxt, transfer_id, include_task_info=False): return self._rpc_client.get_transfer(