Skip to content

Commit 90d6bd2

Browse files
author
Lucas Gameiro
authored
[DPE-4479][DPE-4625] Make list-backups action conformant + type check (#522)
* add list backup extra info + type check * fix unit test * revert unit test time * add libpq connection string * Revert "add libpq connection string" This reverts commit a6d3c56.
1 parent ac88aca commit 90d6bd2

File tree

3 files changed

+159
-39
lines changed

3 files changed

+159
-39
lines changed

src/backups.py

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -342,11 +342,43 @@ def result():
342342

343343
def _format_backup_list(self, backup_list) -> str:
344344
"""Formats provided list of backups as a table."""
345-
backups = ["{:<21s} | {:<12s} | {:s}".format("backup-id", "backup-type", "backup-status")]
346-
backups.append("-" * len(backups[0]))
347-
for backup_id, backup_type, backup_status in backup_list:
345+
s3_parameters, _ = self._retrieve_s3_parameters()
346+
backups = [
347+
"Storage bucket name: {:s}".format(s3_parameters["bucket"]),
348+
"Backups base path: {:s}/backup/\n".format(s3_parameters["path"]),
349+
"{:<20s} | {:<12s} | {:<8s} | {:<20s} | {:<23s} | {:<20s} | {:<20s} | {:s}".format(
350+
"backup-id",
351+
"type",
352+
"status",
353+
"reference-backup-id",
354+
"LSN start/stop",
355+
"start-time",
356+
"finish-time",
357+
"backup-path",
358+
),
359+
]
360+
backups.append("-" * len(backups[2]))
361+
for (
362+
backup_id,
363+
backup_type,
364+
backup_status,
365+
reference,
366+
lsn_start_stop,
367+
start,
368+
stop,
369+
path,
370+
) in backup_list:
348371
backups.append(
349-
"{:<21s} | {:<12s} | {:s}".format(backup_id, backup_type, backup_status)
372+
"{:<20s} | {:<12s} | {:<8s} | {:<20s} | {:<23s} | {:<20s} | {:<20s} | {:s}".format(
373+
backup_id,
374+
backup_type,
375+
backup_status,
376+
reference,
377+
lsn_start_stop,
378+
start,
379+
stop,
380+
path,
381+
)
350382
)
351383
return "\n".join(backups)
352384

@@ -368,11 +400,29 @@ def _generate_backup_list_output(self) -> str:
368400
backups = json.loads(output)[0]["backup"]
369401
for backup in backups:
370402
backup_id, backup_type = self._parse_backup_id(backup["label"])
403+
backup_reference = "None"
404+
if backup["reference"]:
405+
backup_reference, _ = self._parse_backup_id(backup["reference"][-1])
406+
lsn_start_stop = f'{backup["lsn"]["start"]} / {backup["lsn"]["stop"]}'
407+
time_start, time_stop = (
408+
datetime.strftime(datetime.fromtimestamp(stamp), "%Y-%m-%dT%H:%M:%SZ")
409+
for stamp in backup["timestamp"].values()
410+
)
411+
backup_path = f'/{self.stanza_name}/{backup["label"]}'
371412
error = backup["error"]
372413
backup_status = "finished"
373414
if error:
374415
backup_status = f"failed: {error}"
375-
backup_list.append((backup_id, backup_type, backup_status))
416+
backup_list.append((
417+
backup_id,
418+
backup_type,
419+
backup_status,
420+
backup_reference,
421+
lsn_start_stop,
422+
time_start,
423+
time_stop,
424+
backup_path,
425+
))
376426
return self._format_backup_list(backup_list)
377427

378428
def _list_backups(self, show_failed: bool, parse=True) -> OrderedDict[str, str]:
@@ -634,6 +684,17 @@ def _on_create_backup_action(self, event) -> None: # noqa: C901
634684
event.fail(error_message)
635685
return
636686

687+
if (
688+
backup_type in ["differential", "incremental"]
689+
and len(self._list_backups(show_failed=False)) == 0
690+
):
691+
error_message = (
692+
f"Invalid backup type: {backup_type}. No previous full backup to reference."
693+
)
694+
logger.error(f"Backup failed: {error_message}")
695+
event.fail(error_message)
696+
return
697+
637698
logger.info(f"A {backup_type} backup has been requested on unit")
638699
can_unit_perform_backup, validation_message = self._can_unit_perform_backup()
639700
if not can_unit_perform_backup:

tests/integration/helpers.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,8 +1150,8 @@ async def backup_operations(
11501150
action = await ops_test.model.units.get(replica).run_action("list-backups")
11511151
await action.wait()
11521152
backups = action.results.get("backups")
1153-
# 2 lines for header output, 1 backup line ==> 3 total lines
1154-
assert len(backups.split("\n")) == 3, "full backup is not outputted"
1153+
# 5 lines for header output, 1 backup line ==> 6 total lines
1154+
assert len(backups.split("\n")) == 6, "full backup is not outputted"
11551155
await ops_test.model.wait_for_idle(status="active", timeout=1000)
11561156

11571157
# Write some data.
@@ -1177,8 +1177,8 @@ async def backup_operations(
11771177
action = await ops_test.model.units.get(replica).run_action("list-backups")
11781178
await action.wait()
11791179
backups = action.results.get("backups")
1180-
# 2 lines for header output, 2 backup lines ==> 4 total lines
1181-
assert len(backups.split("\n")) == 4, "differential backup is not outputted"
1180+
# 5 lines for header output, 2 backup lines ==> 7 total lines
1181+
assert len(backups.split("\n")) == 7, "differential backup is not outputted"
11821182
await ops_test.model.wait_for_idle(status="active", timeout=1000)
11831183

11841184
# Write some data.

tests/unit/test_backups.py

Lines changed: 89 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -602,49 +602,98 @@ def test_execute_command(harness):
602602

603603

604604
def test_format_backup_list(harness):
605-
# Test when there are no backups.
606-
tc.assertEqual(
607-
harness.charm.backup._format_backup_list([]),
608-
"""backup-id | backup-type | backup-status
609-
----------------------------------------------------""",
610-
)
605+
with patch(
606+
"charms.data_platform_libs.v0.s3.S3Requirer.get_s3_connection_info"
607+
) as _get_s3_connection_info:
608+
# Test when there are no backups.
609+
_get_s3_connection_info.return_value = {
610+
"bucket": " /test-bucket/ ",
611+
"access-key": " test-access-key ",
612+
"secret-key": " test-secret-key ",
613+
"path": " test-path/ ",
614+
}
615+
assert (
616+
harness.charm.backup._format_backup_list([])
617+
== """Storage bucket name: test-bucket
618+
Backups base path: /test-path/backup/
611619
612-
# Test when there are backups.
613-
backup_list = [
614-
("2023-01-01T09:00:00Z", "full", "failed: fake error"),
615-
("2023-01-01T10:00:00Z", "full", "finished"),
616-
]
617-
tc.assertEqual(
618-
harness.charm.backup._format_backup_list(backup_list),
619-
"""backup-id | backup-type | backup-status
620-
----------------------------------------------------
621-
2023-01-01T09:00:00Z | full | failed: fake error
622-
2023-01-01T10:00:00Z | full | finished""",
623-
)
620+
backup-id | type | status | reference-backup-id | LSN start/stop | start-time | finish-time | backup-path
621+
-----------------------------------------------------------------------------------------------------------------------------------------------------------"""
622+
)
623+
624+
# Test when there are backups.
625+
backup_list = [
626+
(
627+
"2023-01-01T09:00:00Z",
628+
"full",
629+
"failed: fake error",
630+
"None",
631+
"0/3000000 / 0/5000000",
632+
"2023-01-01T09:00:00Z",
633+
"2023-01-01T09:00:05Z",
634+
"a/b/c",
635+
),
636+
(
637+
"2023-01-01T10:00:00Z",
638+
"full",
639+
"finished",
640+
"None",
641+
"0/5000000 / 0/7000000",
642+
"2023-01-01T10:00:00Z",
643+
"2023-01-01T010:00:07Z",
644+
"a/b/d",
645+
),
646+
]
647+
assert (
648+
harness.charm.backup._format_backup_list(backup_list)
649+
== """Storage bucket name: test-bucket
650+
Backups base path: /test-path/backup/
651+
652+
backup-id | type | status | reference-backup-id | LSN start/stop | start-time | finish-time | backup-path
653+
-----------------------------------------------------------------------------------------------------------------------------------------------------------
654+
2023-01-01T09:00:00Z | full | failed: fake error | None | 0/3000000 / 0/5000000 | 2023-01-01T09:00:00Z | 2023-01-01T09:00:05Z | a/b/c
655+
2023-01-01T10:00:00Z | full | finished | None | 0/5000000 / 0/7000000 | 2023-01-01T10:00:00Z | 2023-01-01T010:00:07Z | a/b/d"""
656+
)
624657

625658

626659
def test_generate_backup_list_output(harness):
627-
with patch("charm.PostgreSQLBackups._execute_command") as _execute_command:
660+
with (
661+
patch(
662+
"charms.data_platform_libs.v0.s3.S3Requirer.get_s3_connection_info"
663+
) as _get_s3_connection_info,
664+
patch("charm.PostgreSQLBackups._execute_command") as _execute_command,
665+
):
666+
_get_s3_connection_info.return_value = {
667+
"bucket": " /test-bucket/ ",
668+
"access-key": " test-access-key ",
669+
"secret-key": " test-secret-key ",
670+
"path": " test-path/ ",
671+
}
628672
# Test when no backups are returned.
629673
_execute_command.return_value = (0, '[{"backup":[]}]', "")
630-
tc.assertEqual(
631-
harness.charm.backup._generate_backup_list_output(),
632-
"""backup-id | backup-type | backup-status
633-
----------------------------------------------------""",
674+
assert (
675+
harness.charm.backup._generate_backup_list_output()
676+
== """Storage bucket name: test-bucket
677+
Backups base path: /test-path/backup/
678+
679+
backup-id | type | status | reference-backup-id | LSN start/stop | start-time | finish-time | backup-path
680+
-----------------------------------------------------------------------------------------------------------------------------------------------------------"""
634681
)
635682

636683
# Test when backups are returned.
637684
_execute_command.return_value = (
638685
0,
639-
'[{"backup":[{"label":"20230101-090000F","error":"fake error"},{"label":"20230101-100000F","error":null}]}]',
686+
'[{"backup":[{"label":"20230101-090000F","error":"fake error","reference":null,"lsn":{"start":"0/3000000","stop":"0/5000000"},"timestamp":{"start":1719866711,"stop":1719866714}}]}]',
640687
"",
641688
)
642-
tc.assertEqual(
643-
harness.charm.backup._generate_backup_list_output(),
644-
"""backup-id | backup-type | backup-status
645-
----------------------------------------------------
646-
2023-01-01T09:00:00Z | full | failed: fake error
647-
2023-01-01T10:00:00Z | full | finished""",
689+
assert (
690+
harness.charm.backup._generate_backup_list_output()
691+
== """Storage bucket name: test-bucket
692+
Backups base path: /test-path/backup/
693+
694+
backup-id | type | status | reference-backup-id | LSN start/stop | start-time | finish-time | backup-path
695+
-----------------------------------------------------------------------------------------------------------------------------------------------------------
696+
2023-01-01T09:00:00Z | full | failed: fake error | None | 0/3000000 / 0/5000000 | 2024-07-01T20:45:11Z | 2024-07-01T20:45:14Z | /None.postgresql/20230101-090000F"""
648697
)
649698

650699

@@ -1230,8 +1279,18 @@ def test_on_create_backup_action(harness):
12301279
mock_event.fail.assert_called_once()
12311280
mock_event.set_results.assert_not_called()
12321281

1282+
# Test when the backup is of type diff/incr when there's no previous full backup.
1283+
mock_event.reset_mock()
1284+
mock_event.params = {"type": "differential"}
1285+
_upload_content_to_s3.return_value = True
1286+
_is_primary.return_value = True
1287+
harness.charm.backup._on_create_backup_action(mock_event)
1288+
mock_event.fail.assert_called_once()
1289+
mock_event.set_results.assert_not_called()
1290+
12331291
# Test when the backup fails.
12341292
mock_event.reset_mock()
1293+
mock_event.params = {"type": "full"}
12351294
_upload_content_to_s3.return_value = True
12361295
_is_primary.return_value = True
12371296
_execute_command.return_value = (1, "", "fake error")

0 commit comments

Comments
 (0)