Skip to content

Commit 9126138

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 9126138

2 files changed

Lines changed: 55 additions & 62 deletions

File tree

drivers/LinstorSR.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -823,11 +823,21 @@ 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+
if not self._linstor:
827+
self._reconnect()
828+
# Only applied on the Linstor Controller, for various reasons.
829+
if not self._linstor.is_controller():
830+
return
831+
# Start database invalidation.
832+
# Needs access to backup files, available only on the Controller.
833+
self._linstor.database_invalidation()
834+
# check_sr is launched on *all* hosts, but it turns out that
835+
# we do not want all of them to blindly generate concurrencing backups.
836+
# Hence we must choose one, either one is good, but there must be only one.
837+
# Apply throttling: only backup if last one is >1h old.
838+
# Needs access to backup files, available only on the Controller.
839+
if self._linstor.database_backup_age() > 3600:
840+
self.database_backup("auto")
831841

832842
@override
833843
@_locked_load
@@ -1575,30 +1585,22 @@ def _kick_gc(self):
15751585
util.SMlog('Kicking GC')
15761586
cleanup.start_gc_service(self.uuid)
15771587

1578-
def database_backup(self, name="", *, delay=0, fail=False, controller=False):
1579-
"""Generate a new database backup file.
1588+
def database_backup(self, name=""):
1589+
"""
1590+
Generate a new database backup file.
15801591
This operation should not prevent the underlying action to be successful.
15811592
Hence all Exceptions are caught and re-raised only if asked to.
15821593
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.
15861594
"""
15871595
if not self._linstor:
15881596
self._reconnect()
1589-
if controller and not self._linstor.is_controller():
1590-
return
15911597
try:
1592-
if controller:
1593-
self._linstor.database_invalidation()
1594-
self._linstor.database_backup(name, delay=delay)
1598+
self._linstor.database_backup(name)
15951599
except Exception as e:
15961600
util.SMlog(
15971601
"[database_backup] Error during creation: {}".format(e),
15981602
priority=util.LOG_ERR,
15991603
)
1600-
if fail:
1601-
raise
16021604

16031605
# ==============================================================================
16041606
# LinstorSr VDI

drivers/linstorvolumemanager.py

Lines changed: 36 additions & 45 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"
@@ -1771,43 +1770,48 @@ def get_database_path(self):
17711770
return self._request_database_path(self._linstor, activate=True)
17721771

17731772
def is_controller(self):
1774-
"""Checks if the current host is the Linstor Controller.
1773+
"""
1774+
Checks if the current host is the Linstor Controller.
17751775
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
1776+
Which should only be the case on the Linstor Controller.
1777+
"""
1778+
return LinstorVolumeManager._is_mounted(DATABASE_PATH)
1779+
1780+
def database_backup_age(self):
1781+
"""
1782+
Return the latest backup age in seconds.
1783+
If not called on the Controller, since backups are not available,
1784+
returns a huge value (a timestamp of now).
1785+
"""
1786+
return (datetime.now() - self._get_latest_database_backup()).total_seconds()
1787+
1788+
def database_backup(self, name=""):
17881789
# Create new backup
1789-
date = now.strftime(DATABASE_BACKUP_DATE_FORMAT)
1790+
date = datetime.now().strftime(DATABASE_BACKUP_DATE_FORMAT)
17901791
filename = DATABASE_BACKUP_NAME_FORMAT.format(date, name)
17911792
self._linstor.controller_backupdb(filename)
17921793
# Relative path are ok for a secondary backup filename:
17931794
# 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)
1795+
self._linstor.controller_backupdb(f"../linstor.d/db-backups/{filename}")
1796+
util.SMlog(f"[database_backup] Created: {filename}", priority=util.LOG_INFO)
17971797

17981798
def database_invalidation(self):
1799-
for directory in (Path(DATABASE_PATH), DATABASE_BACKUP_LOGDIR):
1800-
file_ok = 0
1799+
"""
1800+
Removes old backup based on two criterias:
1801+
- Validity of the zipfile done by self._check_database_backup.
1802+
- Number of valid files found, only the nth latest are kept.
1803+
"""
1804+
for directory in (DATABASE_BACKUP_DIR_MAIN, DATABASE_BACKUP_DIR_SPARE):
1805+
valid_backup_count = 0
18011806
# Validate file and apply retention
18021807
for database_backup_file, _ in self._get_sorted_database_backup(directory):
18031808
try:
18041809
self._check_database_backup(database_backup_file)
1805-
file_ok += 1
1806-
if file_ok < DATABASE_BACKUP_RETENTION:
1810+
valid_backup_count += 1
1811+
if valid_backup_count < DATABASE_BACKUP_RETENTION:
18071812
continue
18081813
except LinstorDatabaseBackupError as error:
1809-
util.SMlog("[database_backup] Check failed: `{}` [{}]".format(
1810-
error, database_backup_file), priority=util.LOG_ERR)
1814+
util.SMlog(f"[database_backup] Check failed: `{error}` [{database_backup_file}]", priority=util.LOG_ERR)
18111815
with contextlib.suppress(OSError):
18121816
os.unlink(database_backup_file)
18131817

@@ -2667,24 +2671,6 @@ def _get_volume_properties(self, volume_uuid):
26672671
properties.namespace = self._build_volume_namespace(volume_uuid)
26682672
return properties
26692673

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-
26882674
def _list_database_backup(self, 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"):
@@ -2698,6 +2684,11 @@ def _get_sorted_database_backup(self, database_backup_dir):
26982684
reverse=True,
26992685
key=lambda p: p[1])
27002686

2687+
def _get_latest_database_backup(self, name="*"):
2688+
return max(self._list_database_backup(DATABASE_BACKUP_DIR_MAIN, name),
2689+
default=(None, datetime.fromtimestamp(0)),
2690+
key=lambda p: p[1])
2691+
27012692
def _check_database_backup(self, database_backup_file):
27022693
try:
27032694
with zipfile.ZipFile(database_backup_file, mode="r") as archive:
@@ -2711,7 +2702,7 @@ def _check_database_backup(self, database_backup_file):
27112702
raise LinstorDatabaseBackupError("linstordb.mv.db is empty")
27122703
except LinstorDatabaseBackupError:
27132704
raise
2714-
except Exception as e:
2705+
except (FileNotFoundError, zipfile.BadZipFile) as e:
27152706
raise LinstorDatabaseBackupError(e) from e
27162707

27172708
@classmethod

0 commit comments

Comments
 (0)