|
35 | 35 | from datetime import datetime |
36 | 36 | from pathlib import Path |
37 | 37 | import contextlib |
| 38 | +import zipfile |
38 | 39 |
|
39 | 40 | # Persistent prefix to add to RAW persistent volumes. |
40 | 41 | PERSISTENT_PREFIX = 'xcp-persistent-' |
|
44 | 45 | DATABASE_SIZE = 1 << 30 # 1GB. |
45 | 46 | DATABASE_PATH = '/var/lib/linstor' |
46 | 47 | 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" |
49 | 51 | DATABASE_BACKUP_NAME_FORMAT = "linstor_database_backup-{}-{}" |
50 | | -DATABASE_BACKUP_NAME_LATEST = "linstor_database_backup-latest.zip" |
51 | 52 | DATABASE_BACKUP_RETENTION = 10 |
52 | 53 | DATABASE_BACKUP_DATE_FORMAT = "%Y%m%d_%H%M%S" |
53 | 54 | LINSTOR_SATELLITE_PORT = 3366 |
@@ -248,6 +249,8 @@ def __init__(self, message, code=ERR_GENERIC): |
248 | 249 | def code(self): |
249 | 250 | return self._code |
250 | 251 |
|
| 252 | +class LinstorDatabaseBackupError(Exception): |
| 253 | + pass |
251 | 254 |
|
252 | 255 | # ============================================================================== |
253 | 256 |
|
@@ -1767,34 +1770,46 @@ def get_database_path(self): |
1767 | 1770 | """ |
1768 | 1771 | return self._request_database_path(self._linstor, activate=True) |
1769 | 1772 |
|
| 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 | + |
1770 | 1779 | def database_backup(self, name="", *, delay=0): |
1771 | 1780 | 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 |
1773 | 1782 | 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: |
1776 | 1787 | 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) |
1780 | 1791 | 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) |
1798 | 1813 |
|
1799 | 1814 | @classmethod |
1800 | 1815 | def get_all_group_names(cls, base_name): |
@@ -2652,23 +2667,52 @@ def _get_volume_properties(self, volume_uuid): |
2652 | 2667 | properties.namespace = self._build_volume_namespace(volume_uuid) |
2653 | 2668 | return properties |
2654 | 2669 |
|
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): |
2656 | 2689 | 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"): |
2658 | 2691 | try: |
2659 | 2692 | yield path, datetime.strptime(path.name.split("-")[1], DATABASE_BACKUP_DATE_FORMAT) |
2660 | 2693 | except (ValueError, IndexError): |
2661 | 2694 | continue |
2662 | 2695 |
|
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), |
2665 | 2698 | reverse=True, |
2666 | | - key=lambda p: p[0].stat().st_mtime) |
| 2699 | + key=lambda p: p[1]) |
2667 | 2700 |
|
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 |
2672 | 2716 |
|
2673 | 2717 | @classmethod |
2674 | 2718 | def _build_sr_namespace(cls): |
|
0 commit comments