Lucian Petrut пре 2 недеља
родитељ
комит
9d7e609396

+ 33 - 0
coriolis/api/common.py

@@ -10,3 +10,36 @@ def get_paging_params(req):
     if limit is not None:
         limit = utils.parse_int_value(limit)
     return marker, limit
+
+
+def get_sort_params(req,
+                    default_key='created_at',
+                    default_dir='desc'):
+    """Retrieves sort keys/directions parameters.
+
+    Processes the parameters to create a list of sort keys and sort directions
+    that correspond to the 'sort_key' and 'sort_dir' parameter values. These
+    sorting parameters can be specified multiple times in order to generate
+    the list of sort keys and directions.
+
+    The input parameters are not modified.
+
+    :param req: coriolis.api.wsgi.Request object
+    :param default_key: default sort key value, added to the list if no
+                        'sort_key' parameters are supplied
+    :param default_dir: default sort dir value, added to the list if no
+                        'sort_dir' parameters are supplied
+    :returns: list of sort keys, list of sort dirs
+    """
+    params = req.params.copy()
+    sort_keys = []
+    sort_dirs = []
+    while 'sort_key' in params:
+        sort_keys.append(params.pop('sort_key').strip())
+    while 'sort_dir' in params:
+        sort_dirs.append(params.pop('sort_dir').strip())
+    if len(sort_keys) == 0 and default_key:
+        sort_keys.append(default_key)
+    if len(sort_dirs) == 0 and default_dir:
+        sort_dirs.append(default_dir)
+    return sort_keys, sort_dirs

+ 3 - 1
coriolis/api/v1/transfer_tasks_executions.py

@@ -33,11 +33,13 @@ class TransferTasksExecutionController(api_wsgi.Controller):
             executions_policies.get_transfer_executions_policy_label("list"))
 
         marker, limit = common.get_paging_params(req)
+        sort_keys, sort_dirs = common.get_sort_params(req)
 
         return transfer_tasks_execution_view.collection(
             self._transfer_tasks_execution_api.get_executions(
                 context, transfer_id, include_tasks=False,
-                marker=marker, limit=limit))
+                marker=marker, limit=limit,
+                sort_keys=sort_keys, sort_dirs=sort_dirs))
 
     def detail(self, req, transfer_id):
         context = req.environ["coriolis.context"]

+ 7 - 2
coriolis/conductor/rpc/client.py

@@ -145,13 +145,18 @@ class ConductorClient(rpc.BaseRPCClient):
     def get_transfer_tasks_executions(self, ctxt, transfer_id,
                                       include_tasks=False,
                                       marker=None,
-                                      limit=None):
+                                      limit=None,
+                                      sort_keys=None,
+                                      sort_dirs=None):
         return self._call(
             ctxt, 'get_transfer_tasks_executions',
             transfer_id=transfer_id,
             include_tasks=include_tasks,
             marker=marker,
-            limit=limit)
+            limit=limit,
+            sort_keys=sort_keys,
+            sort_dirs=sort_dirs,
+        )
 
     def get_transfer_tasks_execution(self, ctxt, transfer_id, execution_id,
                                      include_task_info=False):

+ 5 - 1
coriolis/conductor/rpc/server.py

@@ -1154,12 +1154,16 @@ class ConductorServerEndpoint(object):
                                       include_tasks=False,
                                       include_task_info=False,
                                       marker=None,
-                                      limit=None):
+                                      limit=None,
+                                      sort_keys=None,
+                                      sort_dirs=None):
         return db_api.get_transfer_tasks_executions(
             ctxt, transfer_id, include_tasks,
             include_task_info=include_task_info,
             marker=marker,
             limit=limit,
+            sort_keys=sort_keys,
+            sort_dirs=sort_dirs,
             to_dict=True)
 
     @tasks_execution_synchronized

+ 88 - 6
coriolis/db/api.py

@@ -7,7 +7,7 @@ from oslo_config import cfg
 from oslo_db import api as db_api
 from oslo_db import options as db_options
 from oslo_db.sqlalchemy import enginefacade
-from oslo_db.sqlalchemy import utils as sqlalchemyutils
+from oslo_db.sqlalchemy import utils as sqlalchemy_utils
 from oslo_log import log as logging
 from oslo_utils import timeutils
 from sqlalchemy import func
@@ -279,6 +279,8 @@ def get_transfer_tasks_executions(context, transfer_id, include_tasks=False,
                                   include_task_info=False,
                                   marker=None,
                                   limit=None,
+                                  sort_keys: list[str] | None = None,
+                                  sort_dirs: list[str] | None = None,
                                   to_dict=False):
     q = _soft_delete_aware_query(context, models.TasksExecution)
     q = q.join(models.Transfer)
@@ -291,17 +293,22 @@ def get_transfer_tasks_executions(context, transfer_id, include_tasks=False,
 
     q = q.filter(models.Transfer.id == transfer_id)
 
+    sort_keys, sort_dirs = process_sort_params(
+        sort_keys,
+        sort_dirs,
+    )
     if marker:
         try:
             marker = get_transfer_tasks_execution(
                 context, transfer_id, marker)
         except exception.NotFound:
             raise exception.MarkerNotFound(marker=marker)
-
-    if marker or limit:
-        q = sqlalchemy.paginate_query(
-            q, models.TasksExecution, limit,
-            sort_keys=['id'], marker=marker)
+    q = sqlalchemy_utils.paginate_query(
+        q, models.TasksExecution, limit,
+        sort_keys=sort_keys,
+        sort_dirs=sort_dirs,
+        marker=marker,
+    )
 
     db_result = q.all()
     if to_dict:
@@ -1521,3 +1528,78 @@ def update_minion_pool(context, minion_pool_id, updated_values):
     # the oslo_db library uses this method for both the `created_at` and
     # `updated_at` fields
     setattr(lifecycle, 'updated_at', timeutils.utcnow())
+
+
+def process_sort_params(
+    sort_keys,
+    sort_dirs,
+    default_keys=None,
+    default_dir='desc',
+):
+    """Process the sort parameters to include default keys.
+
+    Creates a list of sort keys and a list of sort directions. Adds the default
+    keys to the end of the list if they are not already included.
+
+    When adding the default keys to the sort keys list, the associated
+    direction is:
+    1) The first element in the 'sort_dirs' list (if specified), else
+    2) 'default_dir' value (Note that 'asc' is the default value since this is
+    the default in sqlalchemy.utils.paginate_query)
+
+    :param sort_keys: List of sort keys to include in the processed list
+    :param sort_dirs: List of sort directions to include in the processed list
+    :param default_keys: List of sort keys that need to be included in the
+        processed list, they are added at the end of the list if not already
+        specified.
+    :param default_dir: Sort direction associated with each of the default
+        keys that are not supplied, used when they are added to the processed
+        list
+    :returns: list of sort keys, list of sort directions
+    :raise exception.InvalidInput: If more sort directions than sort keys
+        are specified or if an invalid sort direction is specified
+    """
+    if default_keys is None:
+        default_keys = ['created_at', 'id']
+
+    # Determine direction to use for when adding default keys
+    if sort_dirs and len(sort_dirs):
+        default_dir_value = sort_dirs[0]
+    else:
+        default_dir_value = default_dir
+
+    # Create list of keys (do not modify the input list)
+    if sort_keys:
+        result_keys = list(sort_keys)
+    else:
+        result_keys = []
+
+    # If a list of directions is not provided, use the default sort direction
+    # for all provided keys.
+    if sort_dirs:
+        result_dirs = []
+        # Verify sort direction
+        for sort_dir in sort_dirs:
+            if sort_dir not in ('asc', 'desc'):
+                msg = (f"Unknown sort direction: {sort_dir}, "
+                       "must be 'desc' or 'asc'.")
+                raise exception.InvalidInput(reason=msg)
+            result_dirs.append(sort_dir)
+    else:
+        result_dirs = [default_dir_value for _sort_key in result_keys]
+
+    # Ensure that the key and direction length match
+    while len(result_dirs) < len(result_keys):
+        result_dirs.append(default_dir_value)
+    # Unless more direction are specified, which is an error
+    if len(result_dirs) > len(result_keys):
+        msg = "Sort direction array size exceeds sort key array size."
+        raise exception.InvalidInput(reason=msg)
+
+    # Ensure defaults are included
+    for key in default_keys:
+        if key not in result_keys:
+            result_keys.append(key)
+            result_dirs.append(default_dir_value)
+
+    return result_keys, result_dirs

+ 32 - 0
coriolis/tests/integration/test_pagination.py

@@ -139,3 +139,35 @@ class PaginationTest(base.CoriolisIntegrationTestBase):
             self._get_record_summary(e) for e in sorted_exp_exec]
 
         self.assertEqual(exp_sorted_exec_summary, ret_exec_summary)
+
+    def test_transfer_execution_list_pagination(self):
+        # Get the first 2 entries, sorted by ID in ascending order.
+        executions = self._client.transfer_executions.list(
+            self._transfers[0].id,
+            limit=2,
+            sort_keys=['id'],
+            sort_dirs=['asc'])
+        ret_exec_summary = [self._get_record_summary(e) for e in executions]
+
+        exp_exec = self._executions[self._transfers[0].id]
+        sorted_exp_exec = sorted(
+            exp_exec,
+            key=operator.attrgetter('id'))
+        exp_sorted_exec_summary = [
+            self._get_record_summary(e) for e in sorted_exp_exec][:2]
+        self.assertEqual(exp_sorted_exec_summary, ret_exec_summary)
+
+        # Get the next 2 entries.
+        next_executions = self._client.transfer_executions.list(
+            self._transfers[0].id,
+            limit=2,
+            marker=executions[-1].id,
+            sort_keys=['id'],
+            sort_dirs=['asc'])
+        ret_exec_summary = [
+            self._get_record_summary(e)
+            for e in next_executions]
+
+        exp_sorted_exec_summary = [
+            self._get_record_summary(e) for e in sorted_exp_exec][2:4]
+        self.assertEqual(exp_sorted_exec_summary, ret_exec_summary)

+ 4 - 2
coriolis/transfer_tasks_executions/api.py

@@ -21,9 +21,11 @@ class API(object):
             ctxt, transfer_id, execution_id, force)
 
     def get_executions(self, ctxt, transfer_id, include_tasks=False,
-                       marker=None, limit=None):
+                       marker=None, limit=None,
+                       sort_keys=None, sort_dirs=None):
         return self._rpc_client.get_transfer_tasks_executions(
-            ctxt, transfer_id, include_tasks, marker, limit)
+            ctxt, transfer_id, include_tasks, marker, limit,
+            sort_keys, sort_dirs)
 
     def get_execution(self, ctxt, transfer_id, execution_id):
         return self._rpc_client.get_transfer_tasks_execution(