Split configuration file handling into its own module

This commit is contained in:
Eric Torres 2019-04-12 12:03:16 -07:00
parent 792d641ff9
commit 49f5d0f276
2 changed files with 119 additions and 57 deletions

View File

@ -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"

108
rbackup/config.py Normal file
View File

@ -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