From 49f5d0f2761c00800955fc70c327db3bdf27614c Mon Sep 17 00:00:00 2001 From: Eric Torres Date: Fri, 12 Apr 2019 12:03:16 -0700 Subject: [PATCH] Split configuration file handling into its own module --- bin/backup | 68 +++++------------------------ rbackup/config.py | 108 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 57 deletions(-) create mode 100644 rbackup/config.py diff --git a/bin/backup b/bin/backup index 37280ab..a8df920 100644 --- a/bin/backup +++ b/bin/backup @@ -17,13 +17,10 @@ files are hardlinked into the new snapshot. import argparse import logging import os -import re import sys -from contextlib import contextmanager -from pathlib import Path from subprocess import CalledProcessError -from tempfile import NamedTemporaryFile +import rbackup.config as config from rbackup.rsync import rsync from rbackup.struct.repository import Repository @@ -48,12 +45,6 @@ EXTRA_RSYNC_OPTS = { "update": "--update", } -# ----- File Options ----- -CONFIG_DIR = "/etc/rbackup" -FILE_OPTS = [f"--exclude-from={CONFIG_DIR}/home-exclude.conf"] -INCLUDE_PATHS = [f"{CONFIG_DIR}/etc-include.conf", f"{CONFIG_DIR}/system-include.conf"] -COMMENT_REGEX = r"^[^#; ]+" - # ----- Error Codes ----- E_FAILED_PROCESS = 1 @@ -119,43 +110,6 @@ def parse_cmdline_arguments(**kwargs): return parser.parse_args(**kwargs) -@contextmanager -def merge_files(*config_files): - """Parse, filter, and sort through config files to create a single - --files-from argument. - - Any files included that do not exist send a warning to the log. - - >>> merge_files('/etc/rbackup/etc-include.conf', '/etc/rbackup/system-include.conf') # doctest: +ELLIPSIS - >>> '/tmp/...' - - :param config_files: files including paths to read from - :type config_files: iterable of str - :returns: path to file that lists include paths - :rtype: str - """ - file_paths = [Path(p) for p in config_files] - include_lines = [] - - for config_file in file_paths: - if config_file.exists(): - with config_file.open(mode="r") as opened_file: - include_lines.extend( - l for l in opened_file.readlines() if re.match(COMMENT_REGEX, l) - ) - else: - syslog.warning(f"{config_file} does not exist, ignoring.") - - include_lines.sort() - - with NamedTemporaryFile(mode="w", delete=False) as include_paths: - include_paths.writelines(include_lines) - - tmpfile = Path(include_paths.name) - yield tmpfile - tmpfile.unlink() - - # ========== Main Script ========== if __name__ == "__main__": args = parse_cmdline_arguments() @@ -178,24 +132,24 @@ if __name__ == "__main__": curr_snapshot = None - try: - with merge_files(*INCLUDE_PATHS) as include_file: + with config.merge_include_files() as include_file, config.merge_exclude_files() as exclude_file: + try: curr_snapshot = repo.create_snapshot(args.name) rsync( *rsync_opts, - *FILE_OPTS, + f"--exclude-from={exclude_file}", f"--files-from={include_file}", *link_dests, "/", str(curr_snapshot.path), ) - except ValueError as e: - syslog.critical(e) - exit(E_INVALID_SNAPSHOT_NAME) - except CalledProcessError as e: - syslog.critical("Backup process failed") - syslog.critical(f"Failing command: {e.cmd}") - exit(E_FAILED_PROCESS) + except ValueError as e: + syslog.critical(e) + exit(E_INVALID_SNAPSHOT_NAME) + except CalledProcessError as e: + syslog.critical("Backup process failed") + syslog.critical(f"Failing command: {e.cmd}") + exit(E_FAILED_PROCESS) snapshot_symlink = repo.path / "current" diff --git a/rbackup/config.py b/rbackup/config.py new file mode 100644 index 0000000..e23d435 --- /dev/null +++ b/rbackup/config.py @@ -0,0 +1,108 @@ +""" +.. :author:: Eric Torres +.. :module:: rbackup.config +.. :synopsis: Functions for handling config files. +""" +import configparser +import logging +import re +from contextlib import contextmanager +from pathlib import Path +from tempfile import NamedTemporaryFile + +# ========== Logging Setup =========== +syslog = logging.getLogger(__name__) + +# ========== Constants ========== +COMMENT_REGEX = r"^[^#; ]+" + +# ----- Paths ----- +CONFIG_DIR = Path("/etc/rbackup") +MAIN_CONFIG_FILE = CONFIG_DIR / "backup.conf" + +# ----- Exit Codes ----- +E_NO_CONFIG_FILE = 4 + + +# ========== Functions ========== +def get_files_by_suffix(suffix): + """Retrieve all include files from the program configuration directory. + + >>> get_files_by_suffix('-include.conf') # doctest: +ELLIPSIS + ... + + :param suffix: the suffix to search for + :type suffix: str + :returns: paths pointing to include files + :rtype: generator of path-like objects + """ + yield from CONFIG_DIR.glob(f"*{suffix}") + + +def merge_files(files): + """Parse, filter, and sort through config files to create a single + --files-from argument. + + Any files included that do not exist send a warning to the log. + + >>> merge_files() # doctest: +ELLIPSIS + PosixPath(/tmp/...) + + :param files: files including paths to read from + :type files: iterable of path-like objects + :returns: path to file that lists include paths + :rtype: path-like object + """ + include_lines = [] + + for file in files: + with file.open(mode="r") as opened_file: + include_lines.extend( + l for l in opened_file.readlines() if re.match(COMMENT_REGEX, l) + ) + + include_lines.sort() + + with NamedTemporaryFile(mode="w", delete=False) as include_paths: + include_paths.writelines(include_lines) + + return Path(include_paths.name) + + +@contextmanager +def merge_include_files(): + """Merge include file paths into one file and yield its path for use with rsync. + + :return: path-like object + """ + filelist = merge_files(get_files_by_suffix("-include.conf")) + yield filelist + filelist.unlink() + + +@contextmanager +def merge_exclude_files(): + """Merge exclude file paths into one file and yield its path for use with rsync. + + :return: path-like object + """ + filelist = merge_files(get_files_by_suffix("-exclude.conf")) + yield filelist + filelist.unlink() + + +def parse_configfile(): + """Parse a config file given its path and return + a ConfigParser object. + + :returns: object used to parse config file + :rtype: ConfigParser object + :raises: FileNotFoundError if path does not exist + """ + if not MAIN_CONFIG_FILE.is_file(): + raise FileNotFoundError(f"{MAIN_CONFIG_FILE} does not exist") + + config_reader = configparser.ConfigParser() + config_reader.read(MAIN_CONFIG_FILE) + + return config_reader