Skip to content
Snippets Groups Projects
Commit 276698ec authored by Radoslav Bodó's avatar Radoslav Bodó
Browse files

general: logging and backup tests

parent 7d610afc
No related branches found
No related tags found
No related merge requests found
# all commands
RWM_S3_ENDPOINT_URL: "" RWM_S3_ENDPOINT_URL: ""
RWM_S3_ACCESS_KEY: "" RWM_S3_ACCESS_KEY: ""
RWM_S3_SECRET_KEY: "" RWM_S3_SECRET_KEY: ""
# rclone_crypt
RWM_RCLONE_CRYPT_BUCKET: "rwmcrypt" RWM_RCLONE_CRYPT_BUCKET: "rwmcrypt"
RWM_RCLONE_CRYPT_PASSWORD: "" RWM_RCLONE_CRYPT_PASSWORD: ""
# restic, backup
RWM_RESTIC_BUCKET: "rwmcrypt" RWM_RESTIC_BUCKET: "rwmcrypt"
RWM_RESTIC_PASSWORD: "" RWM_RESTIC_PASSWORD: ""
\ No newline at end of file
# backup
RWM_BACKUPS: []
RWM_RETENTION: []
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import base64 import base64
import logging import logging
import os import os
import shlex
import subprocess import subprocess
import sys import sys
from argparse import ArgumentParser from argparse import ArgumentParser
...@@ -16,7 +17,7 @@ from cryptography.hazmat.backends import default_backend ...@@ -16,7 +17,7 @@ from cryptography.hazmat.backends import default_backend
__version__ = "0.2" __version__ = "0.2"
logger = logging.getLogger("rwm") logger = logging.getLogger("rwm")
logger.setLevel(logging.DEBUG) logger.setLevel(logging.INFO)
def is_sublist(needle, haystack): def is_sublist(needle, haystack):
...@@ -44,7 +45,7 @@ def run_command(*args, **kwargs): ...@@ -44,7 +45,7 @@ def run_command(*args, **kwargs):
"text": True, "text": True,
"encoding": "utf-8", "encoding": "utf-8",
}) })
logger.debug("run_command, %s", (args, kwargs)) logger.debug("run_command: %s", shlex.join(args[0]))
return subprocess.run(*args, **kwargs, check=False) return subprocess.run(*args, **kwargs, check=False)
...@@ -147,14 +148,69 @@ class RWM: ...@@ -147,14 +148,69 @@ class RWM:
} }
return run_command(["restic"] + args, env=env) return run_command(["restic"] + args, env=env)
def backup(self, name):
"""do restic backup from config"""
def main(argv=None): if self.restic_cmd(["cat", "config"]).returncode != 0:
"""main""" if (proc := self.restic_cmd(["init"])).returncode != 0:
logger.error("failed to autoinitialize restic repository")
return proc
conf = self.config["RWM_BACKUPS"][name]
# restic backup
excludes = conf.get("excludes", [])
excludes = [x for pair in zip(["--exclude"] * len(excludes), excludes) for x in pair]
extras = conf.get("extras", [])
cmd_args = ["backup"] + extras + excludes + conf["filesdirs"]
logger.info("running backup")
backup_proc = self.restic_cmd(cmd_args)
wrap_output(backup_proc)
if backup_proc.returncode != 0:
logger.error("backup failed, %s", backup_proc)
return backup_proc
# restic forget prune
keeps = []
for key, val in self.config.get("RWM_RETENTION", {}).items():
keeps += [f"--{key}", val]
if not keeps:
logger.error("no retention policy found")
cmd_args = ["forget", "--prune"] + keeps
logger.info("running forget prune")
forget_proc = self.restic_cmd(cmd_args)
wrap_output(forget_proc)
if forget_proc.returncode != 0:
logger.error("forget prune failed, %s", forget_proc)
return forget_proc
return backup_proc
def configure_logging(debug):
"""configure logger"""
log_handler = logging.StreamHandler(sys.stdout)
log_handler.setFormatter(
logging.Formatter(
fmt="%(asctime)s %(name)s[%(process)d]: %(levelname)s %(message)s"
)
)
logger.addHandler(log_handler)
if debug: # pragma: no cover ; would reconfigure pylint environment
logger.setLevel(logging.DEBUG)
def parse_arguments(argv):
"""parse arguments"""
parser = ArgumentParser(description="restics3 worm manager") parser = ArgumentParser(description="restics3 worm manager")
parser.add_argument("--debug", action="store_true")
parser.add_argument("--config", default="rwm.conf") parser.add_argument("--config", default="rwm.conf")
subparsers = parser.add_subparsers(title="commands", dest="command", required=False)
subparsers = parser.add_subparsers(title="commands", dest="command", required=False)
subparsers.add_parser("version", help="show version") subparsers.add_parser("version", help="show version")
aws_cmd_parser = subparsers.add_parser("aws", help="aws command") aws_cmd_parser = subparsers.add_parser("aws", help="aws command")
aws_cmd_parser.add_argument("cmd_args", nargs="*") aws_cmd_parser.add_argument("cmd_args", nargs="*")
...@@ -164,27 +220,43 @@ def main(argv=None): ...@@ -164,27 +220,43 @@ def main(argv=None):
rcc_cmd_parser.add_argument("cmd_args", nargs="*") rcc_cmd_parser.add_argument("cmd_args", nargs="*")
res_cmd_parser = subparsers.add_parser("restic", help="restic command") res_cmd_parser = subparsers.add_parser("restic", help="restic command")
res_cmd_parser.add_argument("cmd_args", nargs="*") res_cmd_parser.add_argument("cmd_args", nargs="*")
backup_cmd_parser = subparsers.add_parser("backup", help="backup command")
backup_cmd_parser.add_argument("name", help="backup config name")
return parser.parse_args(argv)
args = parser.parse_args(argv)
def main(argv=None):
"""main"""
args = parse_arguments(argv)
configure_logging(args.debug)
config = {} config = {}
if args.config: if args.config:
config.update(get_config(args.config)) config.update(get_config(args.config))
logger.debug("config, %s", config)
# assert config ? # assert config ?
rwmi = RWM(config) rwmi = RWM(config)
if args.command == "version": if args.command == "version":
print(__version__) print(__version__)
return 0
ret = -1
if args.command == "aws": if args.command == "aws":
return wrap_output(rwmi.aws_cmd(args.cmd_args)) ret = wrap_output(rwmi.aws_cmd(args.cmd_args))
if args.command == "rclone": if args.command == "rclone":
return wrap_output(rwmi.rclone_cmd(args.cmd_args)) ret = wrap_output(rwmi.rclone_cmd(args.cmd_args))
if args.command == "rclone_crypt": if args.command == "rclone_crypt":
return wrap_output(rwmi.rclone_crypt_cmd(args.cmd_args)) ret = wrap_output(rwmi.rclone_crypt_cmd(args.cmd_args))
if args.command == "restic": if args.command == "restic":
return wrap_output(rwmi.restic_cmd(args.cmd_args)) ret = wrap_output(rwmi.restic_cmd(args.cmd_args))
if args.command == "backup":
ret = rwmi.backup(args.name).returncode
return 0 logger.info("rwm finished with %s (ret %d)", "success" if ret == 0 else "errors", ret)
return ret
if __name__ == "__main__": # pragma: nocover if __name__ == "__main__": # pragma: nocover
......
...@@ -40,7 +40,7 @@ def test_main(tmpworkdir: str): # pylint: disable=unused-argument ...@@ -40,7 +40,7 @@ def test_main(tmpworkdir: str): # pylint: disable=unused-argument
"""test main""" """test main"""
# optional and default config hanling # optional and default config hanling
assert rwm_main([]) == 0 assert rwm_main(["version"]) == 0
Path("rwm.conf").touch() Path("rwm.conf").touch()
assert rwm_main(["version"]) == 0 assert rwm_main(["version"]) == 0
...@@ -48,7 +48,10 @@ def test_main(tmpworkdir: str): # pylint: disable=unused-argument ...@@ -48,7 +48,10 @@ def test_main(tmpworkdir: str): # pylint: disable=unused-argument
mock = Mock(return_value=CompletedProcess(args='dummy', returncode=0)) mock = Mock(return_value=CompletedProcess(args='dummy', returncode=0))
for item in ["aws", "rclone", "rclone_crypt", "restic"]: for item in ["aws", "rclone", "rclone_crypt", "restic"]:
with patch.object(rwm.RWM, f"{item}_cmd", mock): with patch.object(rwm.RWM, f"{item}_cmd", mock):
assert rwm_main([item]) == 0 assert rwm_main([item, "dummy"]) == 0
with patch.object(rwm.RWM, "backup", mock):
assert rwm_main(["backup", "dummy"]) == 0
def test_aws_cmd(tmpworkdir: str, motoserver: str): # pylint: disable=unused-argument def test_aws_cmd(tmpworkdir: str, motoserver: str): # pylint: disable=unused-argument
...@@ -137,3 +140,130 @@ def test_restic_cmd(tmpworkdir: str, motoserver: str): # pylint: disable=unused ...@@ -137,3 +140,130 @@ def test_restic_cmd(tmpworkdir: str, motoserver: str): # pylint: disable=unused
assert trwm.restic_cmd(["init"]).returncode == 0 assert trwm.restic_cmd(["init"]).returncode == 0
proc = trwm.restic_cmd(["cat", "config"]) proc = trwm.restic_cmd(["cat", "config"])
assert "id" in json.loads(proc.stdout) assert "id" in json.loads(proc.stdout)
def _list_snapshots(trwm):
"""test helper"""
return json.loads(trwm.restic_cmd(["snapshots", "--json"]).stdout)
def _list_files(trwm, snapshot_id):
"""test helper"""
snapshot_ls = [
json.loads(x)
for x in
trwm.restic_cmd(["ls", snapshot_id, "--json"]).stdout.splitlines()
]
return [
x["path"] for x in snapshot_ls
if (x["struct_type"] == "node" and x["type"] == "file")
]
def test_backup(tmpworkdir: str, motoserver: str): # pylint: disable=unused-argument
"""test backup command"""
trwm = RWM({
"RWM_S3_ENDPOINT_URL": motoserver,
"RWM_S3_ACCESS_KEY": "dummy",
"RWM_S3_SECRET_KEY": "dummy",
"RWM_RESTIC_BUCKET": "restictest",
"RWM_RESTIC_PASSWORD": "dummydummydummydummydummydummydummydummy",
"RWM_BACKUPS": {
"testcfg": {
"filesdirs": ["testdatadir/"],
"excludes": ["testfile_to_be_ignored"],
"extras": ["--tag", "dummytag"],
}
},
"RWM_RETENTION": {
"keep-daily": "1"
}
})
Path("testdatadir").mkdir()
Path("testdatadir/testdata1.txt").write_text("dummydata", encoding="utf-8")
Path("testdatadir/testfile_to_be_ignored").write_text("dummydata", encoding="utf-8")
assert trwm.backup("testcfg").returncode == 0
snapshots = _list_snapshots(trwm)
assert len(snapshots) == 1
snapshot_files = _list_files(trwm, snapshots[0]["id"])
assert "/testdatadir/testdata1.txt" in snapshot_files
def test_backup_autoinit(): # pylint: disable=unused-argument
"""mocked error handling test"""
mock = Mock(return_value=CompletedProcess(args='dummy', returncode=1))
with patch.object(rwm.RWM, "restic_cmd", mock):
RWM({}).backup("dummy")
def test_backup_excludes(tmpworkdir: str, motoserver: str): # pylint: disable=unused-argument
"""test backup command"""
trwm = RWM({
"RWM_S3_ENDPOINT_URL": motoserver,
"RWM_S3_ACCESS_KEY": "dummy",
"RWM_S3_SECRET_KEY": "dummy",
"RWM_RESTIC_BUCKET": "restictest",
"RWM_RESTIC_PASSWORD": "dummydummydummydummydummydummydummydummy",
"RWM_BACKUPS": {
"testcfg": {
"filesdirs": ["testdatadir"],
"excludes": ["proc", "*.ignored"],
"extras": ["--tag", "dummytag"],
}
}
})
Path("testdatadir").mkdir()
Path("testdatadir/etc").mkdir()
Path("testdatadir/etc/config").write_text("dummydata", encoding="utf-8")
Path("testdatadir/etc/config2").write_text("dummydata", encoding="utf-8")
Path("testdatadir/etc/config3.ignored").write_text("dummydata", encoding="utf-8")
Path("testdatadir/proc").mkdir()
Path("testdatadir/proc/to_be_also_excluded").write_text("dummydata", encoding="utf-8")
assert trwm.backup("testcfg").returncode == 0
snapshots = _list_snapshots(trwm)
assert len(snapshots) == 1
snapshot_files = _list_files(trwm, snapshots[0]["id"])
assert "/testdatadir/etc/config" in snapshot_files
assert "/testdatadir/etc/config2" in snapshot_files
def test_backup_error_handling(tmpworkdir: str, motoserver: str): # pylint: disable=unused-argument
"""test backup command err cases"""
mock = Mock(side_effect=[
CompletedProcess(args='dummy', returncode=0), # autoinit
CompletedProcess(args='dummy', returncode=11) # backup
])
with patch.object(rwm.RWM, "restic_cmd", mock):
trwm = RWM({
"RWM_BACKUPS": {
"dummycfg": {"filesdirs": ["dummydir"]}
}
})
proc = trwm.backup("dummycfg")
assert proc.returncode == 11
mock = Mock(side_effect=[
CompletedProcess(args='dummy', returncode=0), # autoinit
CompletedProcess(args='dummy', returncode=0), # backup
CompletedProcess(args='dummy', returncode=12) # forget
])
with patch.object(rwm.RWM, "restic_cmd", mock):
trwm = RWM({
"RWM_BACKUPS": {
"dummycfg": {"filesdirs": ["dummydir"]}
}
})
proc = trwm.backup("dummycfg")
assert proc.returncode == 12
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment