29
29
30
30
from constants import (
31
31
BACKUP_ID_FORMAT ,
32
+ BACKUP_TYPE_OVERRIDES ,
32
33
BACKUP_USER ,
33
34
PATRONI_CONF_PATH ,
34
35
PGBACKREST_BACKUP_ID_FORMAT ,
@@ -331,22 +332,20 @@ def _generate_backup_list_output(self) -> str:
331
332
332
333
backups = json .loads (output )[0 ]["backup" ]
333
334
for backup in backups :
334
- backup_id = datetime .strftime (
335
- datetime .strptime (backup ["label" ][:- 1 ], PGBACKREST_BACKUP_ID_FORMAT ),
336
- BACKUP_ID_FORMAT ,
337
- )
335
+ backup_id , backup_type = self ._parse_backup_id (backup ["label" ])
338
336
error = backup ["error" ]
339
337
backup_status = "finished"
340
338
if error :
341
339
backup_status = f"failed: { error } "
342
- backup_list .append ((backup_id , "physical" , backup_status ))
340
+ backup_list .append ((backup_id , backup_type , backup_status ))
343
341
return self ._format_backup_list (backup_list )
344
342
345
- def _list_backups (self , show_failed : bool ) -> OrderedDict [str , str ]:
343
+ def _list_backups (self , show_failed : bool , parse = True ) -> OrderedDict [str , str ]:
346
344
"""Retrieve the list of backups.
347
345
348
346
Args:
349
347
show_failed: whether to also return the failed backups.
348
+ parse: whether to convert backup labels to their IDs or not.
350
349
351
350
Returns:
352
351
a dict of previously created backups (id + stanza name) or an empty list
@@ -371,16 +370,35 @@ def _list_backups(self, show_failed: bool) -> OrderedDict[str, str]:
371
370
stanza_name = repository_info ["name" ]
372
371
return OrderedDict [str , str ](
373
372
(
374
- datetime .strftime (
375
- datetime .strptime (backup ["label" ][:- 1 ], PGBACKREST_BACKUP_ID_FORMAT ),
376
- BACKUP_ID_FORMAT ,
377
- ),
373
+ self ._parse_backup_id (backup ["label" ])[0 ] if parse else backup ["label" ],
378
374
stanza_name ,
379
375
)
380
376
for backup in backups
381
377
if show_failed or not backup ["error" ]
382
378
)
383
379
380
+ def _parse_backup_id (self , label ) -> Tuple [str , str ]:
381
+ """Parse backup ID as a timestamp."""
382
+ if label [- 1 ] == "F" :
383
+ timestamp = label
384
+ backup_type = "full"
385
+ elif label [- 1 ] == "D" :
386
+ timestamp = label .split ("_" )[1 ]
387
+ backup_type = "differential"
388
+ elif label [- 1 ] == "I" :
389
+ timestamp = label .split ("_" )[1 ]
390
+ backup_type = "incremental"
391
+ else :
392
+ raise ValueError ("Unknown label format for backup ID: %s" , label )
393
+
394
+ return (
395
+ datetime .strftime (
396
+ datetime .strptime (timestamp [:- 1 ], PGBACKREST_BACKUP_ID_FORMAT ),
397
+ BACKUP_ID_FORMAT ,
398
+ ),
399
+ backup_type ,
400
+ )
401
+
384
402
def _initialise_stanza (self ) -> None :
385
403
"""Initialize the stanza.
386
404
@@ -557,8 +575,16 @@ def _on_s3_credential_gone(self, _) -> None:
557
575
if self .charm .is_blocked and self .charm .unit .status .message in S3_BLOCK_MESSAGES :
558
576
self .charm .unit .status = ActiveStatus ()
559
577
560
- def _on_create_backup_action (self , event ) -> None :
578
+ def _on_create_backup_action (self , event ) -> None : # noqa: C901
561
579
"""Request that pgBackRest creates a backup."""
580
+ backup_type = event .params .get ("type" , "full" )
581
+ if backup_type not in BACKUP_TYPE_OVERRIDES :
582
+ error_message = f"Invalid backup type: { backup_type } . Possible values: { ', ' .join (BACKUP_TYPE_OVERRIDES .keys ())} ."
583
+ logger .error (f"Backup failed: { error_message } " )
584
+ event .fail (error_message )
585
+ return
586
+
587
+ logger .info (f"A { backup_type } backup has been requested on unit" )
562
588
can_unit_perform_backup , validation_message = self ._can_unit_perform_backup ()
563
589
if not can_unit_perform_backup :
564
590
logger .error (f"Backup failed: { validation_message } " )
@@ -600,7 +626,7 @@ def _on_create_backup_action(self, event) -> None:
600
626
# (reference: https://github.com/pgbackrest/pgbackrest/issues/2007)
601
627
self .charm .update_config (is_creating_backup = True )
602
628
603
- self ._run_backup (event , s3_parameters , datetime_backup_requested )
629
+ self ._run_backup (event , s3_parameters , datetime_backup_requested , backup_type )
604
630
605
631
if not self .charm .is_primary :
606
632
# Remove the rule that marks the cluster as in a creating backup state
@@ -611,14 +637,18 @@ def _on_create_backup_action(self, event) -> None:
611
637
self .charm .unit .status = ActiveStatus ()
612
638
613
639
def _run_backup (
614
- self , event : ActionEvent , s3_parameters : Dict , datetime_backup_requested : str
640
+ self ,
641
+ event : ActionEvent ,
642
+ s3_parameters : Dict ,
643
+ datetime_backup_requested : str ,
644
+ backup_type : str ,
615
645
) -> None :
616
646
command = [
617
647
PGBACKREST_EXECUTABLE ,
618
648
PGBACKREST_CONFIGURATION_FILE ,
619
649
f"--stanza={ self .stanza_name } " ,
620
650
"--log-level-console=debug" ,
621
- "--type=full " ,
651
+ f "--type={ BACKUP_TYPE_OVERRIDES [ backup_type ] } " ,
622
652
"backup" ,
623
653
]
624
654
if self .charm .is_primary :
@@ -638,7 +668,7 @@ def _run_backup(
638
668
else :
639
669
# Generate a backup id from the current date and time if the backup failed before
640
670
# generating the backup label (our backup id).
641
- backup_id = datetime . strftime ( datetime . now (), "%Y%m%d-%H%M%SF" )
671
+ backup_id = self . _generate_fake_backup_id ( backup_type )
642
672
643
673
# Upload the logs to S3.
644
674
logs = f"""Stdout:
@@ -750,7 +780,7 @@ def _on_restore_action(self, event):
750
780
# Mark the cluster as in a restoring backup state and update the Patroni configuration.
751
781
logger .info ("Configuring Patroni to restore the backup" )
752
782
self .charm .app_peer_data .update ({
753
- "restoring-backup" : f" { datetime . strftime ( datetime . strptime ( backup_id , BACKUP_ID_FORMAT ), PGBACKREST_BACKUP_ID_FORMAT ) } F" ,
783
+ "restoring-backup" : self . _fetch_backup_from_id ( backup_id ) ,
754
784
"restore-stanza" : backups [backup_id ],
755
785
})
756
786
self .charm .update_config ()
@@ -780,6 +810,37 @@ def _on_restore_action(self, event):
780
810
781
811
event .set_results ({"restore-status" : "restore started" })
782
812
813
+ def _generate_fake_backup_id (self , backup_type : str ) -> str :
814
+ """Creates a backup id for failed backup operations (to store log file)."""
815
+ if backup_type == "full" :
816
+ return datetime .strftime (datetime .now (), "%Y%m%d-%H%M%SF" )
817
+ if backup_type == "differential" :
818
+ backups = self ._list_backups (show_failed = False , parse = False ).keys ()
819
+ last_full_backup = None
820
+ for label in backups [::- 1 ]:
821
+ if label .endswith ("F" ):
822
+ last_full_backup = label
823
+ break
824
+
825
+ if last_full_backup is None :
826
+ raise TypeError ("Differential backup requested but no previous full backup" )
827
+ return f'{ last_full_backup } _{ datetime .strftime (datetime .now (), "%Y%m%d-%H%M%SD" )} '
828
+ if backup_type == "incremental" :
829
+ backups = self ._list_backups (show_failed = False , parse = False ).keys ()
830
+ if not backups :
831
+ raise TypeError ("Incremental backup requested but no previous successful backup" )
832
+ return f'{ backups [- 1 ]} _{ datetime .strftime (datetime .now (), "%Y%m%d-%H%M%SI" )} '
833
+
834
+ def _fetch_backup_from_id (self , backup_id : str ) -> str :
835
+ """Fetches backup's pgbackrest label from backup id."""
836
+ timestamp = f'{ datetime .strftime (datetime .strptime (backup_id , "%Y-%m-%dT%H:%M:%SZ" ), "%Y%m%d-%H%M%S" )} '
837
+ backups = self ._list_backups (show_failed = False , parse = False ).keys ()
838
+ for label in backups :
839
+ if timestamp in label :
840
+ return label
841
+
842
+ return None
843
+
783
844
def _pre_restore_checks (self , event : ActionEvent ) -> bool :
784
845
"""Run some checks before starting the restore.
785
846
0 commit comments