diff --git a/README.md b/README.md index 5c92f9e94e87effee90f10f971f0dad0d63e395e..7f8e417c2fa2e0ff1fabe601612cfaeb6a35aba4 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ somewhere - if I only knew.* * performing backups * restic with S3 repository * simple backup manager/executor - * saves bucket state during backups + * prerun and postrun shell hooks + * bucket state saving after backups * storage management * create, delete and list policed storage buckets diff --git a/rwm.py b/rwm.py index 9c455a4fd0cb63c958e8e8d00d530990ec300574..f5e4e66f817f47e845d610d13abf97c92ae452ec 100755 --- a/rwm.py +++ b/rwm.py @@ -437,6 +437,26 @@ class RWM: return self.restic_cmd(cmd_args) + def _runparts(self, parts_name, parts: list) -> int: + """run all commands in parts in shell""" + + for part in parts: + logger.info("rwm _runparts %s shell command, %s", parts_name, json.dumps(part)) + wrap_output(part_proc := run_command(part, shell=True)) + if part_proc.returncode != 0: + logger.error("rwm _runparts failed") + return part_proc.returncode + + return 0 + + def _prerun(self, name) -> int: + """prerun runparts stub""" + return self._runparts("prerun", self.config["rwm_backups"][name].get("prerun", [])) + + def _postrun(self, name) -> int: + """postrun runparts stub""" + return self._runparts("postrun", self.config["rwm_backups"][name].get("postrun", [])) + def backup(self, name) -> int: """backup command""" @@ -445,11 +465,19 @@ class RWM: if not self.storage_manager.storage_check_policy(bucket_name): logger.warning("used bucket does not have expected policy") + if self._prerun(name) != 0: + logger.error("rwm _prerun failed") + return 1 + wrap_output(backup_proc := self._restic_backup(name)) if backup_proc.returncode != 0: logger.error("rwm _restic_backup failed") return 1 + if self._postrun(name) != 0: + logger.error("rwm _postrun failed") + return 1 + wrap_output(forget_proc := self._restic_forget_prune()) if forget_proc.returncode != 0: logger.error("rwm _restic_forget_prune failed") diff --git a/tests/test_rwm.py b/tests/test_rwm.py index 515ed9bb2a4d84a503bea20980037b26829526b3..8687337804b9898864920c2aa53b17d3988b67dd 100644 --- a/tests/test_rwm.py +++ b/tests/test_rwm.py @@ -56,6 +56,18 @@ def _restic_list_snapshot_files(trwm, snapshot_id): return [x["path"] for x in snapshot_ls if (x["struct_type"] == "node") and (x["type"] == "file")] +def test_runparts(tmpworkdir: str, motoserver: str): # pylint: disable=unused-argument + """test runparts""" + + trwm = rwm.RWM({ + "rwm_s3_endpoint_url": motoserver, + "rwm_s3_access_key": "dummy", + "rwm_s3_secret_key": "dummy", + }) + + assert trwm._runparts("tests", ["false"]) == 1 # pylint: disable=protected-access + + def test_backup(tmpworkdir: str, motoserver: str): # pylint: disable=unused-argument """test backup""" @@ -133,6 +145,36 @@ def test_backup_excludes(tmpworkdir: str, motoserver: str): # pylint: disable=u assert "/testdatadir/var/proc/data" in snapshot_files +def test_backup_runparts(tmpworkdir: str, motoserver: str): # pylint: disable=unused-argument + """test backup""" + + trwm = rwm.RWM({ + "rwm_s3_endpoint_url": motoserver, + "rwm_s3_access_key": "dummy", + "rwm_s3_secret_key": "dummy", + "rwm_restic_bucket": "restictest", + "rwm_restic_password": "dummydummydummydummy", + "rwm_backups": { + "testcfg": { + "filesdirs": ["testdatadir/"], + "prerun": ["false || true"], + "postrun": ["true || false"] + } + } + }) + + mock_ok = Mock(return_value=0) + mock_proc_ok = Mock(return_value=CompletedProcess(args='dummy', returncode=0)) + + with ( + patch.object(rwm.StorageManager, "storage_check_policy", mock_ok), + patch.object(rwm.RWM, "_restic_backup", mock_proc_ok), + patch.object(rwm.RWM, "_restic_forget_prune", mock_proc_ok), + patch.object(rwm.StorageManager, "storage_save_state", mock_ok) + ): + assert trwm.backup("testcfg") == 0 + + def test_backup_error_handling(tmpworkdir: str, motoserver: str): # pylint: disable=unused-argument """test backup command err cases""" @@ -144,22 +186,40 @@ def test_backup_error_handling(tmpworkdir: str, motoserver: str): # pylint: dis } mock_proc_ok = Mock(return_value=CompletedProcess(args='dummy', returncode=0)) mock_proc_fail = Mock(return_value=CompletedProcess(args='dummy', returncode=2)) + mock_ok = Mock(return_value=0) mock_fail = Mock(return_value=11) with ( + patch.object(rwm.RWM, "_prerun", mock_fail) + ): + assert rwm.RWM(rwm_conf).backup("dummycfg") == 1 + + with ( + patch.object(rwm.RWM, "_prerun", mock_ok), patch.object(rwm.RWM, "_restic_backup", mock_proc_fail) ): assert rwm.RWM(rwm_conf).backup("dummycfg") == 1 with ( + patch.object(rwm.RWM, "_prerun", mock_ok), patch.object(rwm.RWM, "_restic_backup", mock_proc_ok), patch.object(rwm.RWM, "_restic_forget_prune", mock_proc_fail) ): assert rwm.RWM(rwm_conf).backup("dummycfg") == 1 with ( + patch.object(rwm.RWM, "_prerun", mock_ok), + patch.object(rwm.RWM, "_restic_backup", mock_proc_ok), + patch.object(rwm.RWM, "_restic_forget_prune", mock_proc_ok), + patch.object(rwm.RWM, "_postrun", mock_fail) + ): + assert rwm.RWM(rwm_conf).backup("dummycfg") == 1 + + with ( + patch.object(rwm.RWM, "_prerun", mock_ok), patch.object(rwm.RWM, "_restic_backup", mock_proc_ok), patch.object(rwm.RWM, "_restic_forget_prune", mock_proc_ok), + patch.object(rwm.RWM, "_postrun", mock_ok), patch.object(rwm.StorageManager, "storage_save_state", mock_fail) ): assert rwm.RWM(rwm_conf).backup("dummycfg") == 1