3535from datetime import datetime
3636from pathlib import Path
3737import contextlib
38+ import zipfile
3839
3940# Persistent prefix to add to RAW persistent volumes.
4041PERSISTENT_PREFIX = 'xcp-persistent-'
4546DATABASE_PATH = '/var/lib/linstor'
4647DATABASE_MKFS = 'mkfs.ext4'
4748DATABASE_BACKUP_LOGDIR = Path ('/var/lib/linstor.d/db-backups' )
49+ DATABASE_BACKUP_RELATIVE = Path ("../linstor.d/db-backups" )
4850DATABASE_BACKUP_LOGFILE = DATABASE_BACKUP_LOGDIR / "log.txt"
4951DATABASE_BACKUP_NAME_FORMAT = "linstor_database_backup-{}-{}"
52+ DATABASE_BACKUP_RETENTION = 10
5053DATABASE_BACKUP_DATE_FORMAT = "%Y%m%d_%H%M%S"
5154LINSTOR_SATELLITE_PORT = 3366
5255
@@ -246,6 +249,8 @@ def __init__(self, message, code=ERR_GENERIC):
246249 def code (self ):
247250 return self ._code
248251
252+ class LinstorDatabaseBackupError (Exception ):
253+ pass
249254
250255# ==============================================================================
251256
@@ -1765,6 +1770,12 @@ def get_database_path(self):
17651770 """
17661771 return self ._request_database_path (self ._linstor , activate = True )
17671772
1773+ def is_controller (self ):
1774+ """Checks if the current host is the Linstor Controller.
1775+ This is done by checking that the Linstor database path is a mountpoint.
1776+ Which should only be the case on the Linstor Controller."""
1777+ return os .path .ismount (DATABASE_PATH )
1778+
17681779 def database_backup (self , name = "" , * , delay = 0 ):
17691780 now = datetime .now ()
17701781 # Throttling to avoid too many backups on a short period
@@ -1778,9 +1789,28 @@ def database_backup(self, name="", *, delay=0):
17781789 date = now .strftime (DATABASE_BACKUP_DATE_FORMAT )
17791790 filename = DATABASE_BACKUP_NAME_FORMAT .format (date , name )
17801791 self ._linstor .controller_backupdb (filename )
1792+ # Relative path are ok for a secondary backup filename:
1793+ # https://github.com/LINBIT/linstor-server/blob/3e9306a9d8215606544c64c50ced150625ee4926/controller/src/main/java/com/linbit/linstor/api/rest/v1/Controller.java#L408
1794+ self ._linstor .controller_backupdb (str (DATABASE_BACKUP_RELATIVE / filename ))
17811795 self ._log_database_backup (date , name )
17821796 util .SMlog ("[database_backup] Created: {}" .format (filename ), priority = util .LOG_INFO )
17831797
1798+ def database_invalidation (self ):
1799+ for directory in (Path (DATABASE_PATH ), DATABASE_BACKUP_LOGDIR ):
1800+ file_ok = 0
1801+ # Validate file and apply retention
1802+ for database_backup_file , _ in self ._get_sorted_database_backup (directory ):
1803+ try :
1804+ self ._check_database_backup (database_backup_file )
1805+ file_ok += 1
1806+ if file_ok < DATABASE_BACKUP_RETENTION :
1807+ continue
1808+ except LinstorDatabaseBackupError as error :
1809+ util .SMlog ("[database_backup] Check failed: `{}` [{}]" .format (
1810+ error , database_backup_file ), priority = util .LOG_ERR )
1811+ with contextlib .suppress (OSError ):
1812+ os .unlink (database_backup_file )
1813+
17841814 @classmethod
17851815 def get_all_group_names (cls , base_name ):
17861816 """
@@ -2643,7 +2673,7 @@ def _log_database_backup(self, date, name):
26432673 this file is on the pool-master, and serves for the throttling."""
26442674 os .makedirs (DATABASE_BACKUP_LOGDIR , mode = 0o755 , exist_ok = True )
26452675 with open (DATABASE_BACKUP_LOGFILE , "a" , encoding = "utf8" ) as f :
2646- f .write (f"{ date } { name } " )
2676+ f .write (f"{ date } { name } \n " )
26472677
26482678 def _get_latest_logged_database_backup_date (self ):
26492679 # get last log line if it exists, and return the corresponding date
@@ -2653,7 +2683,36 @@ def _get_latest_logged_database_backup_date(self):
26532683 f .seek (- min (os .stat (DATABASE_BACKUP_LOGFILE ).st_size , 256 ), os .SEEK_END )
26542684 return f .read ().decode ().splitlines ()[- 1 ].split ()[0 ]
26552685 except FileNotFoundError :
2656- return datetime .utcfromtimestamp (0 )
2686+ return "20000101_000000"
2687+
2688+ def _list_database_backup (self , database_backup_dir ):
2689+ for path in database_backup_dir .glob (DATABASE_BACKUP_NAME_FORMAT .format (
2690+ "20[0-9][0-9][01][0-9][0-3][0-9]_[0-2][0-9][0-5][0-9][0-5][0-9]" , "*" ) + ".zip" ):
2691+ try :
2692+ yield path , datetime .strptime (path .name .split ("-" )[1 ], DATABASE_BACKUP_DATE_FORMAT )
2693+ except (ValueError , IndexError ):
2694+ continue
2695+
2696+ def _get_sorted_database_backup (self , database_backup_dir ):
2697+ return sorted (self ._list_database_backup (database_backup_dir ),
2698+ reverse = True ,
2699+ key = lambda p : p [1 ])
2700+
2701+ def _check_database_backup (self , database_backup_file ):
2702+ try :
2703+ with zipfile .ZipFile (database_backup_file , mode = "r" ) as archive :
2704+ if archive .testzip () is not None :
2705+ raise LinstorDatabaseBackupError ("zip archive CRC failed" )
2706+ linstordb = [f for f in archive .filelist if f .filename == "linstordb.mv.db" ]
2707+ if not linstordb :
2708+ raise LinstorDatabaseBackupError ("cannot find linstordb.mv.db" )
2709+ linstordb = linstordb [0 ]
2710+ if linstordb .file_size == 0 :
2711+ raise LinstorDatabaseBackupError ("linstordb.mv.db is empty" )
2712+ except LinstorDatabaseBackupError :
2713+ raise
2714+ except Exception as e :
2715+ raise LinstorDatabaseBackupError (e ) from e
26572716
26582717 @classmethod
26592718 def _build_sr_namespace (cls ):
0 commit comments