From 7c75496e2c28b6c9e9ea8b6f4c7475c79e565b42 Mon Sep 17 00:00:00 2001 From: Mihaela Balutoiu Date: Fri, 19 Jun 2026 10:08:13 +0300 Subject: [PATCH 1/2] Fix `transfer show` after the `get_transfer` optimization The transfer payload no longer embeds executions, so `transfer show` rendered an empty `executions` section. Signed-off-by: Mihaela Balutoiu --- coriolisclient/cli/transfers.py | 26 +++++---- coriolisclient/tests/cli/test_transfers.py | 65 ++++++---------------- 2 files changed, 34 insertions(+), 57 deletions(-) diff --git a/coriolisclient/cli/transfers.py b/coriolisclient/cli/transfers.py index d80f419..4a49793 100644 --- a/coriolisclient/cli/transfers.py +++ b/coriolisclient/cli/transfers.py @@ -30,6 +30,8 @@ TRANSFER_SCENARIO_REPLICA = "replica" TRANSFER_SCENARIO_LIVE_MIGRATION = "live_migration" +TRANSFER_SHOW_EXECUTIONS_LIMIT = 10 + def _add_default_deployment_args_to_parser(parser): cd_group = parser.add_mutually_exclusive_group() @@ -70,12 +72,6 @@ class TransferFormatter(formatter.EntityFormatter): def _get_sorted_list(self, obj_list): return sorted(obj_list, key=lambda o: o.created_at) - def _format_last_execution(self, obj): - if obj.executions: - execution = sorted(obj.executions, key=lambda e: e.created_at)[-1] - return "%(id)s %(status)s" % execution.to_dict() - return "" - def _get_formatted_data(self, obj): data = (obj.id, getattr(obj, "scenario", "replica"), @@ -89,7 +85,8 @@ def _get_formatted_data(self, obj): class TransferDetailFormatter(formatter.EntityFormatter): - def __init__(self, show_instances_data=False): + def __init__(self, show_instances_data=False, executions=None): + self._executions = executions self.columns = [ "id", "created", @@ -133,6 +130,9 @@ def _get_formatted_data(self, obj): storage_mappings = obj.to_dict().get("storage_mappings", {}) default_storage, backend_mappings, disk_mappings = ( cli_utils.parse_storage_mappings(storage_mappings)) + executions = ( + self._executions if self._executions is not None + else obj.executions) data = [obj.id, obj.created_at, obj.updated_at, @@ -158,7 +158,7 @@ def _get_formatted_data(self, obj): cli_utils.format_json_for_object_property(obj, 'user_scripts'), obj.clone_disks, obj.skip_os_morphing, - self._format_executions(obj.executions)] + self._format_executions(executions)] if "instances-data" in self.columns: data.append(obj.info) @@ -307,9 +307,15 @@ def get_parser(self, prog_name): return parser def take_action(self, args): - transfer = self.app.client_manager.coriolis.transfers.get(args.id) + coriolis = self.app.client_manager.coriolis + transfer = coriolis.transfers.get(args.id) + executions = coriolis.transfer_executions.list( + args.id, + limit=TRANSFER_SHOW_EXECUTIONS_LIMIT, + sort_keys=["number"], sort_dirs=["desc"]) return TransferDetailFormatter( - args.show_instances_data).get_formatted_entity(transfer) + args.show_instances_data, + executions=executions).get_formatted_entity(transfer) class DeleteTransfer(command.Command): diff --git a/coriolisclient/tests/cli/test_transfers.py b/coriolisclient/tests/cli/test_transfers.py index 5c093f5..e713b64 100644 --- a/coriolisclient/tests/cli/test_transfers.py +++ b/coriolisclient/tests/cli/test_transfers.py @@ -36,44 +36,6 @@ def test_get_sorted_list(self): result ) - def test_format_last_execution(self): - obj = mock.Mock() - obj.executions = None - - result = self.transfer._format_last_execution(obj) - - self.assertEqual( - "", - result - ) - - execution1 = mock.Mock() - execution2 = mock.Mock() - execution3 = mock.Mock() - execution1.created_at = "date1" - execution2.created_at = "date2" - execution3.created_at = "date3" - execution1.to_dict.return_value = { - "id": "mock_id1", - "status": "mock_status1" - } - execution2.to_dict.return_value = { - "id": "mock_id2", - "status": "mock_status2" - } - execution3.to_dict.return_value = { - "id": "mock_id3", - "status": "mock_status3" - } - obj.executions = [execution1, execution3, execution2] - - result = self.transfer._format_last_execution(obj) - - self.assertEqual( - "mock_id3 mock_status3", - result - ) - def test_get_formatted_data(self): obj = mock.Mock() obj.id = mock.sentinel.id @@ -362,23 +324,32 @@ def test_get_parser(self, mock_get_parser): ) mock_get_parser.assert_called_once_with(mock.sentinel.prog_name) - @mock.patch.object(transfers.TransferDetailFormatter, - 'get_formatted_entity') - def test_take_action(self, mock_get_formatted_entity): + @mock.patch.object(transfers, 'TransferDetailFormatter') + def test_take_action(self, mock_formatter_class): args = mock.Mock() args.id = mock.sentinel.id - mock_transfer = mock.Mock() - self.mock_app.client_manager.coriolis.transfers.get = mock_transfer + args.show_instances_data = mock.sentinel.show_instances_data + coriolis = self.mock_app.client_manager.coriolis + mock_get = coriolis.transfers.get + mock_list = coriolis.transfer_executions.list + mock_formatter = mock_formatter_class.return_value result = self.transfer.take_action(args) self.assertEqual( - mock_get_formatted_entity.return_value, + mock_formatter.get_formatted_entity.return_value, result ) - mock_transfer.assert_called_once_with(mock.sentinel.id) - mock_get_formatted_entity.assert_called_once_with( - mock_transfer.return_value) + mock_get.assert_called_once_with(mock.sentinel.id) + mock_list.assert_called_once_with( + mock.sentinel.id, + limit=transfers.TRANSFER_SHOW_EXECUTIONS_LIMIT, + sort_keys=["number"], sort_dirs=["desc"]) + mock_formatter_class.assert_called_once_with( + mock.sentinel.show_instances_data, + executions=mock_list.return_value) + mock_formatter.get_formatted_entity.assert_called_once_with( + mock_get.return_value) class DeleteTransferTestCase(test_base.CoriolisBaseTestCase): From 040240dc8065938d0e885d955d12358774fa0790 Mon Sep 17 00:00:00 2001 From: Mihaela Balutoiu Date: Fri, 19 Jun 2026 10:14:48 +0300 Subject: [PATCH 2/2] Fix include_task_info for `transfer show --show-instances-data` The transfer's `info` is only returned with `include_task_info=true`, which the CLI never sent, so `--show-instances-data` failed with "ERROR: info". Add an `include_task_info` flag to TransferManager.get and set it when the option is passed. --- coriolisclient/cli/transfers.py | 3 ++- coriolisclient/tests/cli/test_transfers.py | 4 +++- coriolisclient/tests/v1/test_transfers.py | 13 +++++++++++++ coriolisclient/v1/transfers.py | 7 +++++-- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/coriolisclient/cli/transfers.py b/coriolisclient/cli/transfers.py index 4a49793..1b9ad4f 100644 --- a/coriolisclient/cli/transfers.py +++ b/coriolisclient/cli/transfers.py @@ -308,7 +308,8 @@ def get_parser(self, prog_name): def take_action(self, args): coriolis = self.app.client_manager.coriolis - transfer = coriolis.transfers.get(args.id) + transfer = coriolis.transfers.get( + args.id, include_task_info=args.show_instances_data) executions = coriolis.transfer_executions.list( args.id, limit=TRANSFER_SHOW_EXECUTIONS_LIMIT, diff --git a/coriolisclient/tests/cli/test_transfers.py b/coriolisclient/tests/cli/test_transfers.py index e713b64..7bacb78 100644 --- a/coriolisclient/tests/cli/test_transfers.py +++ b/coriolisclient/tests/cli/test_transfers.py @@ -340,7 +340,9 @@ def test_take_action(self, mock_formatter_class): mock_formatter.get_formatted_entity.return_value, result ) - mock_get.assert_called_once_with(mock.sentinel.id) + mock_get.assert_called_once_with( + mock.sentinel.id, + include_task_info=mock.sentinel.show_instances_data) mock_list.assert_called_once_with( mock.sentinel.id, limit=transfers.TRANSFER_SHOW_EXECUTIONS_LIMIT, diff --git a/coriolisclient/tests/v1/test_transfers.py b/coriolisclient/tests/v1/test_transfers.py index 94ec783..d0075ba 100644 --- a/coriolisclient/tests/v1/test_transfers.py +++ b/coriolisclient/tests/v1/test_transfers.py @@ -135,6 +135,19 @@ def test_get(self, mock_get): mock_get.assert_called_once_with( "/transfers/%s" % mock.sentinel.transfer, "transfer") + @mock.patch.object(transfers.TransferManager, "_get") + def test_get_with_task_info(self, mock_get): + result = self.transfer.get( + mock.sentinel.transfer, include_task_info=True) + + self.assertEqual( + mock_get.return_value, + result + ) + mock_get.assert_called_once_with( + "/transfers/%s?include_task_info=true" % mock.sentinel.transfer, + "transfer") + @mock.patch.object(transfers.TransferManager, "_post") def test_create(self, mock_post): expected_data = { diff --git a/coriolisclient/v1/transfers.py b/coriolisclient/v1/transfers.py index 52c6ba1..412dd68 100644 --- a/coriolisclient/v1/transfers.py +++ b/coriolisclient/v1/transfers.py @@ -69,8 +69,11 @@ def list(self, detail=False, marker=None, limit=None, path = "%s/detail" % path return self._list(path, 'transfers', query=query) - def get(self, transfer): - return self._get('/transfers/%s' % base.getid(transfer), 'transfer') + def get(self, transfer, include_task_info=False): + url = '/transfers/%s' % base.getid(transfer) + if include_task_info: + url += '?include_task_info=true' + return self._get(url, 'transfer') def create(self, origin_endpoint_id, destination_endpoint_id, source_environment, destination_environment, instances,