fix(linstor): Database Backup when Linstor Controller is not the Pool Master. - #142
fix(linstor): Database Backup when Linstor Controller is not the Pool Master.#142Ythogtha wants to merge 8 commits into
Conversation
0e5dcb8 to
f5e5bfc
Compare
b18a3f2 to
46bc6e3
Compare
Millefeuille42
left a comment
There was a problem hiding this comment.
Only small changes / clarification. Not all comments are actually blocking though.
| """Generate a new database backup file. | ||
| This operation should not prevent the underlying action to be successful. | ||
| Hence all Exceptions are caught and re-raised only if asked to. | ||
| controller: operate only if the current host is the Linstor Controller. | ||
| """ |
There was a problem hiding this comment.
| """Generate a new database backup file. | |
| This operation should not prevent the underlying action to be successful. | |
| Hence all Exceptions are caught and re-raised only if asked to. | |
| controller: operate only if the current host is the Linstor Controller. | |
| """ | |
| """ | |
| Generate a new database backup file. | |
| This operation should not prevent the underlying action to be successful. | |
| Hence all Exceptions are caught and re-raised only if asked to. | |
| controller: operate only if the current host is the Linstor Controller. | |
| """ |
| except Exception as e: | ||
| raise LinstorDatabaseBackupError(e) from e |
There was a problem hiding this comment.
Can you catch ZIP exception instead? Or add a comment about exceptions here?
There was a problem hiding this comment.
That'd be (FileNotFoundError, zipfile.BadZipFile).
I'm somehow unhappy with what will happen if we don't catch something:
Retention fails, backup accumulate without hope for cleanup, and later along the road, everything breaks with a full /var/lib/linstor/
That is if the file is bad in a way we haven't anticipated.
I'm unsure here, of what is best.
There was a problem hiding this comment.
Well, according to https://docs.python.org/3/library/zipfile.html there are only two zipfile Exceptions.
I'va added zipfile.LargeZipFile, because if that exception is raised, it is very strange, and may be the result of a malformed or malicious zipfile.
It feels better to handle the situation, and remove it.
I'm ok with this code.
|
|
||
| def database_invalidation(self): | ||
| for directory in (Path(DATABASE_PATH), DATABASE_BACKUP_LOGDIR): | ||
| file_ok = 0 |
There was a problem hiding this comment.
| file_ok = 0 | |
| valid_backup_count = 0 |
| util.SMlog("[database_backup] Check failed: `{}` [{}]".format( | ||
| error, database_backup_file), priority=util.LOG_ERR) |
| # Automatic backup if there were no backups for the last hour. | ||
| # Let it fail if needed, so full Traceback is on SMLog. | ||
| # Launch it only if we are on the controller. | ||
| self.database_backup("auto", delay=3600, fail=True, controller=True) |
There was a problem hiding this comment.
I think we should remove the controller flag, and test if we are on a host with a running controller before the database backup call.
Same for the fail flag, IMHO it adds complexity.
| DATABASE_BACKUP_DIR_MAIN = Path(DATABASE_PATH) | ||
| DATABASE_BACKUP_DIR_SPARE = Path('/var/lib/linstor.d/db-backups') | ||
| DATABASE_BACKUP_LOGDIR = Path('/var/lib/linstor.d/db-backups') | ||
| DATABASE_BACKUP_RELATIVE = Path("../linstor.d/db-backups") |
There was a problem hiding this comment.
IMHO, the use of RELATIVE in the variable name seems to me to be more an effect of the path content than an approach to the semantics and what this variable represents.
| DATABASE_BACKUP_RELATIVE = Path("../linstor.d/db-backups") | |
| DATABASE_BACKUP_DIR_SECONDARY = Path("../linstor.d/db-backups") |
There was a problem hiding this comment.
Let's say I'd be glad to use DATABASE_BACKUP_DIR_SPARE.relative_to(DATABASE_BACKUP_DIR_MAIN, walk_up=True), but Python 3.6 is in the way.
Fixed.
| DATABASE_MKFS = 'mkfs.ext4' | ||
| DATABASE_BACKUP_DIR_MAIN = Path(DATABASE_PATH) | ||
| DATABASE_BACKUP_DIR_SPARE = Path('/var/lib/linstor.d/db-backups') | ||
| DATABASE_BACKUP_LOGDIR = Path('/var/lib/linstor.d/db-backups') |
There was a problem hiding this comment.
I don't understand the var name: this constant is used to list backups and to manipulate logs.
There was a problem hiding this comment.
I removed the logs and changed names according to new way of doing things.
| def is_controller(self): | ||
| """Checks if the current host is the Linstor Controller. | ||
| This is done by checking that the Linstor database path is a mountpoint. | ||
| Which should only be the case on the Linstor Controller.""" | ||
| return os.path.ismount(DATABASE_PATH) |
There was a problem hiding this comment.
Should be merged with existing code like cls._is_mounted(DATABASE_PATH).
| # https://github.com/LINBIT/linstor-server/blob/3e9306a9d8215606544c64c50ced150625ee4926/controller/src/main/java/com/linbit/linstor/api/rest/v1/Controller.java#L408 | ||
| self._linstor.controller_backupdb(str(DATABASE_BACKUP_RELATIVE / filename)) | ||
| self._log_database_backup(date, name) | ||
| util.SMlog("[database_backup] Created: {}".format(filename), priority=util.LOG_INFO) |
|
I don't understand the choice of using a log file rather than directly listing the logs, either by using:
|
Because we can't use throttling with filenames on SR operations (not check_sr), because we don't know if we have access to the files. |
9126138 to
6e1ab3d
Compare
|
Also, I converted some method into |
b090447 to
b6b7676
Compare
3dba5e3 to
09c9925
Compare
e712aa5 to
fce2d13
Compare
09c9925 to
6c0d0a3
Compare
- Retention of 10 backups from operations, and check_sr always saves on latest. - Provides a secondary backup location, with the same retention policy. Signed-off-by: Arnaud Garcia-Fernandez <arnaud.garcia-fernandez@vates.tech>
… 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>
Signed-off-by: Arnaud Garcia-Fernandez <arnaud.garcia-fernandez@vates.tech>
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>
Don't except/raise LinstorDatabaseBackupError in _check_database_backup(). Simpler glob search for backup files, as the date is properly validated by datetime. Fix bug in database_backup_age(). Signed-off-by: Arnaud Garcia-Fernandez <arnaud.garcia-fernandez@vates.tech>
6c0d0a3 to
b42bcd2
Compare
3ed3cbd to
b2d00c7
Compare
Signed-off-by: Arnaud Garcia-Fernandez <arnaud.garcia-fernandez@vates.tech>
| if not self._linstor: | ||
| self._reconnect() | ||
| try: | ||
| self._linstor.database_backup(name) # type: ignore |
There was a problem hiding this comment.
You can import cast helper from sm_typing here.
| self._linstor.database_backup(name) # type: ignore | |
| self._linstor.database_backup(cast(str, name)) |
There was a problem hiding this comment.
The # type: ignore is not because of name, but of _linstor, defines that way:
_linstor: Optional["LinstorVolumeManager"] = None
And since I've added a type for the name parameter, the linter checks types inside the function.
And AttributeError: 'NoneType' object has no attribute 'database_backup', because _linstor could be None.
But we just _reconnect(), and here I'm not caring about why something might fail: I log it and raise nothing.
Because the backup is a sub-product of an action which was a success, and should be reported as such.
So I am covered, by the reconnection, and the generic try/except, I'm ok having the AttributeError raised here, it is explicit enough for debugging purposes.
And I don't want to ignore a _linstor is None, that is an error which ought to be reported as such.
So I ask the linter to ignore its type.
There was a problem hiding this comment.
Oh ok! I naively thought that old mypy version or something similar had an issue with the str type and Literal.
So you can use this before the call and without explicit message:
assert self._linstorIt's common to do that, we apply the classic usage of assert: "It must be true; it's a contract. If not, I messed up the code somewhere else."
There was a problem hiding this comment.
Oh, great, I'll remember that, I like it.
| util.SMlog(f"[database_backup] Created: {filename}", priority=util.LOG_INFO) | ||
|
|
||
| @classmethod | ||
| def database_invalidation(cls): |
There was a problem hiding this comment.
Regarding description, I think we can change this name to: remove_old_database_backups.
There was a problem hiding this comment.
It removes old database backups, but also checks existing files for validation (proper non-empty zipfile with a non-empty linstordb.mv.db.
That's why I used invalidation, but I'm open to a better name.
There was a problem hiding this comment.
I agree, so maybe something like validate_and_prune_backups?
There was a problem hiding this comment.
Ok, while keeping the database_backup prefix: database_backup_validate_and_prune
| key=lambda p: p[1]) | ||
|
|
||
| @classmethod | ||
| def _check_database_backup(cls, database_backup_file): |
There was a problem hiding this comment.
I suggest this:
| def _check_database_backup(cls, database_backup_file): | |
| def _check_database_backup(cls, database_backup_path): |
It hasn't been formally documented yet, but:
- A
pathsuffix is useful for indicating a string or aPath. - A
filesuffix is useful for indicating that a file descriptor is involved.
| # Applied only on the Linstor Controller, for reasons -> listed below. | ||
| if not LinstorVolumeManager.is_controller(): | ||
| return | ||
| # Start database invalidation. |
There was a problem hiding this comment.
I think this comment needs to be modified. I added a note where the function is declared.
| # Start database invalidation. | |
| # Clean previous backups if necessary. |
| # Hence we must choose one, either one is good, but there must be only one. | ||
| # Apply throttling: only backup if last one is >1h old. | ||
| # -> Needs access to backup files, available only on the Controller. | ||
| if LinstorVolumeManager.database_backup_age() > 3600: |
There was a problem hiding this comment.
IMHO we should use a constant here for maintenance.
| linstordb = [f for f in archive.filelist if f.filename == "linstordb.mv.db"] | ||
| if not linstordb: | ||
| raise LinstorDatabaseBackupError("cannot find linstordb.mv.db") | ||
| linstordb = linstordb[0] |
There was a problem hiding this comment.
| linstordb = [f for f in archive.filelist if f.filename == "linstordb.mv.db"] | |
| if not linstordb: | |
| raise LinstorDatabaseBackupError("cannot find linstordb.mv.db") | |
| linstordb = linstordb[0] | |
| linstordb = next((f for f in archive.filelist if f.filename == "linstordb.mv.db"), None) | |
| if not linstordb: | |
| raise LinstorDatabaseBackupError("cannot find linstordb.mv.db") |
Small suggestion with small perf improvement.
There was a problem hiding this comment.
It might end faster if there are several files and linstordb.mv.db is not the last one?
Well True, but in a normal situation, the zipfile contains only one file.
But that's elegant, so Ok ;)
| return cls._is_mounted(DATABASE_PATH) | ||
|
|
||
| @classmethod | ||
| def database_backup_age(cls): |
There was a problem hiding this comment.
| def database_backup_age(cls): | |
| def get_database_backup_age(cls): |
Because it's not a property and also we use this prefix for getters.
| return properties | ||
|
|
||
| @classmethod | ||
| def _list_database_backup(cls, database_backup_dir): |
There was a problem hiding this comment.
| def _list_database_backup(cls, database_backup_dir): | |
| def _list_database_backups(cls, database_backup_dir): |
database_backup_path instead of database_backup_file because it is a str|Path. Throttling delay converted to Constant. Signed-off-by: Arnaud Garcia-Fernandez <arnaud.garcia-fernandez@vates.tech>
| util.SMlog(f"[database_backup] Created: {filename}", priority=util.LOG_INFO) | ||
|
|
||
| @classmethod | ||
| def database_invalidation(cls): |
There was a problem hiding this comment.
I agree, so maybe something like validate_and_prune_backups?
| if not self._linstor: | ||
| self._reconnect() | ||
| try: | ||
| self._linstor.database_backup(name) # type: ignore |
There was a problem hiding this comment.
Oh ok! I naively thought that old mypy version or something similar had an issue with the str type and Literal.
So you can use this before the call and without explicit message:
assert self._linstorIt's common to do that, we apply the classic usage of assert: "It must be true; it's a contract. If not, I messed up the code somewhere else."
…le None self._linstor Signed-off-by: Arnaud Garcia-Fernandez <arnaud.garcia-fernandez@vates.tech>
Linstor Controller(needs access to files).check_sroperates a regular backup only if it runs on theLinstor Controller, to allow throttling.check_sronController, with access to actual files.backup.zipfiles for consistency :Valid zip with a non-empty
linstordb.mv.dbfile inside,Reporting errors, and keeping only valid files.