Skip to content

Commit 46bc6e3

Browse files
committed
fix(linstor): Database Backup when Linstor Controller is not the Pool Master.
* Added secondary backup location outside of Linstor's DRBD mounts. * Throttling uses a logfile for backup operations. * sr_scan operates a regular backup only if it runs on the Linstor Controller. * Retention is enforced by sr_scan on controller, with access to actual files. * Exceptions are raised only on backup errors from sr_scan ; Other operations will only report in SMlog. * Retention checks all backup.zip files for consistency : Valid zip with a non-empty linstordb.mv.db file inside, Reporting errors, and keeping only valid files. Signed-off-by: Arnaud Garcia-Fernandez <arnaud.garcia-fernandez@vates.tech>
1 parent b67f4ab commit 46bc6e3

2 files changed

Lines changed: 103 additions & 38 deletions

File tree

drivers/LinstorSR.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,10 @@ def is_master(self):
823823

824824
@override
825825
def check_sr(self, sr_uuid) -> None:
826-
self.database_backup("auto", delay=3600)
826+
# Automatic backup if there were no backups for the last hour.
827+
# Let it fail if needed, so full Traceback is on SMLog.
828+
# Launch it only if we are on the controller.
829+
self.database_backup("auto", delay=3600, fail=True, controller=True)
827830

828831

829832
@override
@@ -1572,10 +1575,28 @@ def _kick_gc(self):
15721575
util.SMlog('Kicking GC')
15731576
cleanup.start_gc_service(self.uuid)
15741577

1575-
def database_backup(self, name="", *, delay=0):
1578+
def database_backup(self, name="", *, delay=0, fail=False, controller=False):
1579+
"""Generate a new database backup file.
1580+
This operation should not prevent the underlying action to be successful.
1581+
Hence all Exceptions are caught and re-raised only if asked to.
1582+
controller: operate only if the current host is the Linstor Controller.
1583+
"""
15761584
if not self._linstor:
15771585
self._reconnect()
1578-
self._linstor.database_backup(name, delay=delay)
1586+
if controller:
1587+
if self._linstor.is_controller():
1588+
self._linstor.database_invalidation()
1589+
else:
1590+
return
1591+
try:
1592+
self._linstor.database_backup(name, delay=delay)
1593+
except Exception as e:
1594+
util.SMlog(
1595+
"[database_backup] Error during creation: {}".format(e),
1596+
priority=util.LOG_ERR,
1597+
)
1598+
if fail:
1599+
raise
15791600

15801601
# ==============================================================================
15811602
# LinstorSr VDI

drivers/linstorvolumemanager.py

Lines changed: 79 additions & 35 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-'
@@ -44,10 +45,10 @@
4445
DATABASE_SIZE = 1 << 30 # 1GB.
4546
DATABASE_PATH = '/var/lib/linstor'
4647
DATABASE_MKFS = 'mkfs.ext4'
47-
DATABASE_BACKUP_DIR_MAIN = Path(DATABASE_PATH)
48-
DATABASE_BACKUP_DIR_SPARE = Path('/var/lib/linstor.d/db-backups')
48+
DATABASE_BACKUP_LOGDIR = Path('/var/lib/linstor.d/db-backups')
49+
DATABASE_BACKUP_RELATIVE = Path("../linstor.d/db-backups")
50+
DATABASE_BACKUP_LOGFILE = DATABASE_BACKUP_LOGDIR / "log.txt"
4951
DATABASE_BACKUP_NAME_FORMAT = "linstor_database_backup-{}-{}"
50-
DATABASE_BACKUP_NAME_LATEST = "linstor_database_backup-latest.zip"
5152
DATABASE_BACKUP_RETENTION = 10
5253
DATABASE_BACKUP_DATE_FORMAT = "%Y%m%d_%H%M%S"
5354
LINSTOR_SATELLITE_PORT = 3366
@@ -248,6 +249,8 @@ def __init__(self, message, code=ERR_GENERIC):
248249
def code(self):
249250
return self._code
250251

252+
class LinstorDatabaseBackupError(Exception):
253+
pass
251254

252255
# ==============================================================================
253256

@@ -1767,34 +1770,46 @@ def get_database_path(self):
17671770
"""
17681771
return self._request_database_path(self._linstor, activate=True)
17691772

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+
17701779
def database_backup(self, name="", *, delay=0):
17711780
now = datetime.now()
1772-
# Throttling to avoid too many backups of the same kind on a short period
1781+
# Throttling to avoid too many backups on a short period
17731782
if delay:
1774-
_, date_latest = self._get_latest_database_backup(name)
1775-
if date_latest and ((now - date_latest).total_seconds() < delay):
1783+
latest = datetime.strptime(
1784+
self._get_latest_logged_database_backup_date(),
1785+
DATABASE_BACKUP_DATE_FORMAT)
1786+
if (now - latest).total_seconds() < delay:
17761787
return # No backup for now
1777-
1778-
# Create new backup with link to latest
1779-
filename = DATABASE_BACKUP_NAME_FORMAT.format(now.strftime(DATABASE_BACKUP_DATE_FORMAT), name)
1788+
# Create new backup
1789+
date = now.strftime(DATABASE_BACKUP_DATE_FORMAT)
1790+
filename = DATABASE_BACKUP_NAME_FORMAT.format(date, name)
17801791
self._linstor.controller_backupdb(filename)
1781-
# Copy to secondary backup location
1782-
with contextlib.suppress(OSError):
1783-
os.makedirs(DATABASE_BACKUP_DIR_SPARE, mode=0o755, exist_ok=True)
1784-
shutil.copy2(
1785-
(DATABASE_BACKUP_DIR_MAIN / filename).with_suffix(".zip"),
1786-
DATABASE_BACKUP_DIR_SPARE,
1787-
)
1788-
for directory in (DATABASE_BACKUP_DIR_MAIN, DATABASE_BACKUP_DIR_SPARE):
1789-
# Remove and set latest
1790-
with contextlib.suppress(OSError):
1791-
(directory / DATABASE_BACKUP_NAME_LATEST).unlink()
1792-
os.link(str((directory / filename).with_suffix(".zip")),
1793-
str((directory / DATABASE_BACKUP_NAME_LATEST)))
1794-
# Apply retention
1795-
for old_file, _ in self._get_sorted_database_backup(directory)[DATABASE_BACKUP_RETENTION:]:
1796-
os.unlink(old_file)
1797-
util.SMlog("[database_backup] Created: {}".format(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))
1795+
self._log_database_backup(date, name)
1796+
util.SMlog("[database_backup] Created: {}".format(filename), priority=util.LOG_INFO)
1797+
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)
17981813

17991814
@classmethod
18001815
def get_all_group_names(cls, base_name):
@@ -2652,23 +2667,52 @@ def _get_volume_properties(self, volume_uuid):
26522667
properties.namespace = self._build_volume_namespace(volume_uuid)
26532668
return properties
26542669

2655-
def _list_database_backup(self, database_backup_dir, name="*"):
2670+
def _log_database_backup(self, date, name):
2671+
"""Log a database backup operation: "date name"
2672+
We cannot assume the pool-master is the same as the linstor-master,
2673+
this file is on the pool-master, and serves for the throttling."""
2674+
os.makedirs(DATABASE_BACKUP_LOGDIR, mode=0o755, exist_ok=True)
2675+
with open(DATABASE_BACKUP_LOGFILE, "a", encoding="utf8") as f:
2676+
f.write(f"{date} {name}\n")
2677+
2678+
def _get_latest_logged_database_backup_date(self):
2679+
# get last log line if it exists, and return the corresponding date
2680+
try:
2681+
with open(DATABASE_BACKUP_LOGFILE, "rb") as f:
2682+
# seek from the end, with 256 as a most-probable maximum line length
2683+
f.seek(-min(os.stat(DATABASE_BACKUP_LOGFILE).st_size, 256), os.SEEK_END)
2684+
return f.read().decode().splitlines()[-1].split()[0]
2685+
except FileNotFoundError:
2686+
return "20000101_000000"
2687+
2688+
def _list_database_backup(self, database_backup_dir):
26562689
for path in database_backup_dir.glob(DATABASE_BACKUP_NAME_FORMAT.format(
2657-
"20[0-9][0-9][01][0-9][0-3][0-9]_[0-2][0-9][0-5][0-9][0-5][0-9]", name) + ".zip"):
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"):
26582691
try:
26592692
yield path, datetime.strptime(path.name.split("-")[1], DATABASE_BACKUP_DATE_FORMAT)
26602693
except (ValueError, IndexError):
26612694
continue
26622695

2663-
def _get_sorted_database_backup(self, database_backup_dir, name="*"):
2664-
return sorted(self._list_database_backup(database_backup_dir, name),
2696+
def _get_sorted_database_backup(self, database_backup_dir):
2697+
return sorted(self._list_database_backup(database_backup_dir),
26652698
reverse=True,
2666-
key=lambda p: p[0].stat().st_mtime)
2699+
key=lambda p: p[1])
26672700

2668-
def _get_latest_database_backup(self, name="*"):
2669-
return max(self._list_database_backup(DATABASE_BACKUP_DIR_MAIN, name),
2670-
default=(None, None),
2671-
key=lambda p: p[0].stat().st_mtime)
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
26722716

26732717
@classmethod
26742718
def _build_sr_namespace(cls):

0 commit comments

Comments
 (0)