Skip to content

Commit b6b7676

Browse files
committed
Separation between backup validation, and backup creation.
check_sr works only on the Controller to have access to the backup files. It applies Validation, Retention, and Throttling for the automatic backup. All three of which requires access to the backup files, on the Controller. Removal of the logfile to manage throttling for normal operation. Hence removal of the delay parameter. Restricted caught Exceptions in backup validation to (FileNotFoundError, zipfile.BadZipFile). Signed-off-by: Arnaud Garcia-Fernandez <arnaud.garcia-fernandez@vates.tech>
1 parent 38b2b19 commit b6b7676

2 files changed

Lines changed: 65 additions & 71 deletions

File tree

drivers/LinstorSR.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -823,11 +823,19 @@ def is_master(self):
823823

824824
@override
825825
def check_sr(self, sr_uuid) -> None:
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)
830-
826+
# Only applied on the Linstor Controller, for various reasons.
827+
if not LinstorVolumeManager.is_controller():
828+
return
829+
# Start database invalidation.
830+
# Needs access to backup files, available only on the Controller.
831+
LinstorVolumeManager.database_invalidation()
832+
# check_sr is launched on *all* hosts, but it turns out that
833+
# we do not want all of them to blindly generate concurrencing backups.
834+
# Hence we must choose one, either one is good, but there must be only one.
835+
# Apply throttling: only backup if last one is >1h old.
836+
# Needs access to backup files, available only on the Controller.
837+
if LinstorVolumeManager.database_backup_age() > 3600:
838+
self.database_backup("auto")
831839

832840
@override
833841
@_locked_load
@@ -1575,30 +1583,22 @@ def _kick_gc(self):
15751583
util.SMlog('Kicking GC')
15761584
cleanup.start_gc_service(self.uuid)
15771585

1578-
def database_backup(self, name="", *, delay=0, fail=False, controller=False):
1579-
"""Generate a new database backup file.
1586+
def database_backup(self, name=""):
1587+
"""
1588+
Generate a new database backup file.
15801589
This operation should not prevent the underlying action to be successful.
15811590
Hence all Exceptions are caught and re-raised only if asked to.
15821591
delay: skip backup if the last one was generated less than delay seconds ago.
1583-
fail: If fail is True, caught Exception are raised after being logged in SMlog.
1584-
controller: operate only if the current host is the Linstor Controller.
1585-
> This will trigger controller-only operations like retention and validation.
15861592
"""
15871593
if not self._linstor:
15881594
self._reconnect()
1589-
if controller and not self._linstor.is_controller():
1590-
return
15911595
try:
1592-
if controller:
1593-
self._linstor.database_invalidation()
1594-
self._linstor.database_backup(name, delay=delay)
1596+
self._linstor.database_backup(name)
15951597
except Exception as e:
15961598
util.SMlog(
15971599
"[database_backup] Error during creation: {}".format(e),
15981600
priority=util.LOG_ERR,
15991601
)
1600-
if fail:
1601-
raise
16021602

16031603
# ==============================================================================
16041604
# LinstorSr VDI

drivers/linstorvolumemanager.py

Lines changed: 48 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,8 @@
4545
DATABASE_SIZE = 1 << 30 # 1GB.
4646
DATABASE_PATH = '/var/lib/linstor'
4747
DATABASE_MKFS = 'mkfs.ext4'
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"
48+
DATABASE_BACKUP_DIR_MAIN = Path(DATABASE_PATH)
49+
DATABASE_BACKUP_DIR_SPARE = Path("/var/lib/linstor.d/db-backups")
5150
DATABASE_BACKUP_NAME_FORMAT = "linstor_database_backup-{}-{}"
5251
DATABASE_BACKUP_RETENTION = 10
5352
DATABASE_BACKUP_DATE_FORMAT = "%Y%m%d_%H%M%S"
@@ -1770,44 +1769,48 @@ def get_database_path(self):
17701769
"""
17711770
return self._request_database_path(self._linstor, activate=True)
17721771

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-
1779-
def database_backup(self, name="", *, delay=0):
1780-
now = datetime.now()
1781-
# Throttling to avoid too many backups on a short period
1782-
if 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:
1787-
return # No backup for now
1772+
@classmethod
1773+
def is_controller(cls):
1774+
return cls._is_mounted(DATABASE_PATH)
1775+
1776+
@classmethod
1777+
def database_backup_age(cls):
1778+
"""
1779+
Return the latest backup age in seconds.
1780+
If not called on the Controller, since backups are not available,
1781+
returns a huge value (a timestamp of now).
1782+
"""
1783+
return (datetime.now() - cls._get_latest_database_backup()).total_seconds()
1784+
1785+
def database_backup(self, name=""):
17881786
# Create new backup
1789-
date = now.strftime(DATABASE_BACKUP_DATE_FORMAT)
1787+
date = datetime.now().strftime(DATABASE_BACKUP_DATE_FORMAT)
17901788
filename = DATABASE_BACKUP_NAME_FORMAT.format(date, name)
17911789
self._linstor.controller_backupdb(filename)
17921790
# Relative path are ok for a secondary backup filename:
17931791
# 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)
1792+
self._linstor.controller_backupdb(f"../linstor.d/db-backups/{filename}")
1793+
util.SMlog(f"[database_backup] Created: {filename}", priority=util.LOG_INFO)
17971794

1798-
def database_invalidation(self):
1799-
for directory in (Path(DATABASE_PATH), DATABASE_BACKUP_LOGDIR):
1800-
file_ok = 0
1795+
@classmethod
1796+
def database_invalidation(cls):
1797+
"""
1798+
Removes old backup based on two criterias:
1799+
- Validity of the zipfile done by self._check_database_backup.
1800+
- Number of valid files found, only the nth latest are kept.
1801+
"""
1802+
for directory in (DATABASE_BACKUP_DIR_MAIN, DATABASE_BACKUP_DIR_SPARE):
1803+
valid_backup_count = 0
18011804
# Validate file and apply retention
1802-
for database_backup_file, _ in self._get_sorted_database_backup(directory):
1805+
for database_backup_file, _ in cls._get_sorted_database_backup(directory):
18031806
try:
1804-
self._check_database_backup(database_backup_file)
1805-
file_ok += 1
1806-
if file_ok < DATABASE_BACKUP_RETENTION:
1807+
cls._check_database_backup(database_backup_file)
1808+
valid_backup_count += 1
1809+
if valid_backup_count < DATABASE_BACKUP_RETENTION:
18071810
continue
18081811
except LinstorDatabaseBackupError as error:
1809-
util.SMlog("[database_backup] Check failed: `{}` [{}]".format(
1810-
error, database_backup_file), priority=util.LOG_ERR)
1812+
util.SMlog(f"[database_backup] Check failed `{error}` [{database_backup_file}]",
1813+
priority=util.LOG_ERR)
18111814
with contextlib.suppress(OSError):
18121815
os.unlink(database_backup_file)
18131816

@@ -2667,38 +2670,29 @@ def _get_volume_properties(self, volume_uuid):
26672670
properties.namespace = self._build_volume_namespace(volume_uuid)
26682671
return properties
26692672

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 Controller,
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[:15]}\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, a line length can't be more than 32
2683-
f.seek(-min(os.stat(DATABASE_BACKUP_LOGFILE).st_size, 32), 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):
2673+
@classmethod
2674+
def _list_database_backup(cls, database_backup_dir):
26892675
for path in database_backup_dir.glob(DATABASE_BACKUP_NAME_FORMAT.format(
26902676
"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"):
26912677
try:
26922678
yield path, datetime.strptime(path.name.split("-")[1], DATABASE_BACKUP_DATE_FORMAT)
26932679
except (ValueError, IndexError):
26942680
continue
26952681

2696-
def _get_sorted_database_backup(self, database_backup_dir):
2697-
return sorted(self._list_database_backup(database_backup_dir),
2682+
@classmethod
2683+
def _get_sorted_database_backup(cls, database_backup_dir):
2684+
return sorted(cls._list_database_backup(database_backup_dir),
26982685
reverse=True,
26992686
key=lambda p: p[1])
27002687

2701-
def _check_database_backup(self, database_backup_file):
2688+
@classmethod
2689+
def _get_latest_database_backup(cls):
2690+
return max(cls._list_database_backup(DATABASE_BACKUP_DIR_MAIN),
2691+
default=(None, datetime.fromtimestamp(0)),
2692+
key=lambda p: p[1])
2693+
2694+
@classmethod
2695+
def _check_database_backup(cls, database_backup_file):
27022696
try:
27032697
with zipfile.ZipFile(database_backup_file, mode="r") as archive:
27042698
if archive.testzip() is not None:
@@ -2711,7 +2705,7 @@ def _check_database_backup(self, database_backup_file):
27112705
raise LinstorDatabaseBackupError("linstordb.mv.db is empty")
27122706
except LinstorDatabaseBackupError:
27132707
raise
2714-
except Exception as e:
2708+
except (FileNotFoundError, zipfile.BadZipFile, zipfile.LargeZipFile) as e:
27152709
raise LinstorDatabaseBackupError(e) from e
27162710

27172711
@classmethod

0 commit comments

Comments
 (0)