Skip to content

Commit b18a3f2

Browse files
committed
Reintroduced Linstor Database Backup validation, retention, and secondary backup destination.
Duplicate call to Linstor's API for backupdb with a relative path to the secondary backup location. On call to sr_scan, only if it is executed on the Linstor Controller: - check all database files on both locations. - report (SMlog) and removes invalid files. - Apply retention, keeping the 10 more recent ones. Signed-off-by: Arnaud Garcia-Fernandez <arnaud.garcia-fernandez@vates.tech>
1 parent f5e5bfc commit b18a3f2

2 files changed

Lines changed: 70 additions & 4 deletions

File tree

drivers/LinstorSR.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -825,7 +825,8 @@ def is_master(self):
825825
def check_sr(self, sr_uuid) -> None:
826826
# Automatic backup if there were no backups for the last hour.
827827
# Let it fail if needed, so full Traceback is on SMLog.
828-
self.database_backup("auto", delay=3600, fail=True)
828+
# Launch it only if we are on the controller.
829+
self.database_backup("auto", delay=3600, fail=True, controller=True)
829830

830831

831832
@override
@@ -1574,13 +1575,19 @@ def _kick_gc(self):
15741575
util.SMlog('Kicking GC')
15751576
cleanup.start_gc_service(self.uuid)
15761577

1577-
def database_backup(self, name="", *, delay=0, fail=False):
1578+
def database_backup(self, name="", *, delay=0, fail=False, controller=False):
15781579
"""Generate a new database backup file.
15791580
This operation should not prevent the underlying action to be successful.
15801581
Hence all Exceptions are caught and re-raised only if asked to.
1582+
controller: operate only if the current host is the Linstor Controller.
15811583
"""
15821584
if not self._linstor:
15831585
self._reconnect()
1586+
if controller:
1587+
if self._linstor.is_controller():
1588+
self._linstor.database_invalidation()
1589+
else:
1590+
return
15841591
try:
15851592
self._linstor.database_backup(name, delay=delay)
15861593
except Exception as e:

drivers/linstorvolumemanager.py

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from datetime import datetime
3636
from pathlib import Path
3737
import contextlib
38+
import zipfile
3839

3940
# Persistent prefix to add to RAW persistent volumes.
4041
PERSISTENT_PREFIX = 'xcp-persistent-'
@@ -45,8 +46,10 @@
4546
DATABASE_PATH = '/var/lib/linstor'
4647
DATABASE_MKFS = 'mkfs.ext4'
4748
DATABASE_BACKUP_LOGDIR = Path('/var/lib/linstor.d/db-backups')
49+
DATABASE_BACKUP_RELATIVE = Path("../linstor.d/db-backups")
4850
DATABASE_BACKUP_LOGFILE = DATABASE_BACKUP_LOGDIR / "log.txt"
4951
DATABASE_BACKUP_NAME_FORMAT = "linstor_database_backup-{}-{}"
52+
DATABASE_BACKUP_RETENTION = 10
5053
DATABASE_BACKUP_DATE_FORMAT = "%Y%m%d_%H%M%S"
5154
LINSTOR_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

Comments
 (0)