Split configuration file handling into its own module
This commit is contained in:
68
bin/backup
68
bin/backup
@@ -17,13 +17,10 @@ files are hardlinked into the new snapshot.
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
from contextlib import contextmanager
|
|
||||||
from pathlib import Path
|
|
||||||
from subprocess import CalledProcessError
|
from subprocess import CalledProcessError
|
||||||
from tempfile import NamedTemporaryFile
|
|
||||||
|
|
||||||
|
import rbackup.config as config
|
||||||
from rbackup.rsync import rsync
|
from rbackup.rsync import rsync
|
||||||
from rbackup.struct.repository import Repository
|
from rbackup.struct.repository import Repository
|
||||||
|
|
||||||
@@ -48,12 +45,6 @@ EXTRA_RSYNC_OPTS = {
|
|||||||
"update": "--update",
|
"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 -----
|
# ----- Error Codes -----
|
||||||
E_FAILED_PROCESS = 1
|
E_FAILED_PROCESS = 1
|
||||||
@@ -119,43 +110,6 @@ def parse_cmdline_arguments(**kwargs):
|
|||||||
return parser.parse_args(**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 ==========
|
# ========== Main Script ==========
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
args = parse_cmdline_arguments()
|
args = parse_cmdline_arguments()
|
||||||
@@ -178,24 +132,24 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
curr_snapshot = None
|
curr_snapshot = None
|
||||||
|
|
||||||
try:
|
with config.merge_include_files() as include_file, config.merge_exclude_files() as exclude_file:
|
||||||
with merge_files(*INCLUDE_PATHS) as include_file:
|
try:
|
||||||
curr_snapshot = repo.create_snapshot(args.name)
|
curr_snapshot = repo.create_snapshot(args.name)
|
||||||
rsync(
|
rsync(
|
||||||
*rsync_opts,
|
*rsync_opts,
|
||||||
*FILE_OPTS,
|
f"--exclude-from={exclude_file}",
|
||||||
f"--files-from={include_file}",
|
f"--files-from={include_file}",
|
||||||
*link_dests,
|
*link_dests,
|
||||||
"/",
|
"/",
|
||||||
str(curr_snapshot.path),
|
str(curr_snapshot.path),
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
syslog.critical(e)
|
syslog.critical(e)
|
||||||
exit(E_INVALID_SNAPSHOT_NAME)
|
exit(E_INVALID_SNAPSHOT_NAME)
|
||||||
except CalledProcessError as e:
|
except CalledProcessError as e:
|
||||||
syslog.critical("Backup process failed")
|
syslog.critical("Backup process failed")
|
||||||
syslog.critical(f"Failing command: {e.cmd}")
|
syslog.critical(f"Failing command: {e.cmd}")
|
||||||
exit(E_FAILED_PROCESS)
|
exit(E_FAILED_PROCESS)
|
||||||
|
|
||||||
snapshot_symlink = repo.path / "current"
|
snapshot_symlink = repo.path / "current"
|
||||||
|
|
||||||
|
108
rbackup/config.py
Normal file
108
rbackup/config.py
Normal 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
|
Reference in New Issue
Block a user