Browse Source

backup script (local only)

master
n0m1s 4 years ago
commit
3eaf7cd339
Signed by: nomis GPG Key ID: BC0454CAD76FE803
15 changed files with 355 additions and 0 deletions
  1. +0
    -0
      backup/__init__.py
  2. BIN
      backup/__pycache__/__init__.cpython-310.pyc
  3. BIN
      backup/__pycache__/exceptions.cpython-310.pyc
  4. BIN
      backup/__pycache__/repo.cpython-310.pyc
  5. BIN
      backup/__pycache__/server.cpython-310.pyc
  6. +0
    -0
      backup/detail/__init__.py
  7. BIN
      backup/detail/__pycache__/__init__.cpython-310.pyc
  8. BIN
      backup/detail/__pycache__/server_local.cpython-310.pyc
  9. +26
    -0
      backup/detail/server_local.py
  10. +16
    -0
      backup/exceptions.py
  11. +162
    -0
      backup/repo.py
  12. +62
    -0
      backup/server.py
  13. +11
    -0
      conf/backups/test.yml
  14. +3
    -0
      conf/servers/localhost.yml
  15. +75
    -0
      run_backup.py

+ 0
- 0
backup/__init__.py View File


BIN
backup/__pycache__/__init__.cpython-310.pyc View File


BIN
backup/__pycache__/exceptions.cpython-310.pyc View File


BIN
backup/__pycache__/repo.cpython-310.pyc View File


BIN
backup/__pycache__/server.cpython-310.pyc View File


+ 0
- 0
backup/detail/__init__.py View File


BIN
backup/detail/__pycache__/__init__.cpython-310.pyc View File


BIN
backup/detail/__pycache__/server_local.cpython-310.pyc View File


+ 26
- 0
backup/detail/server_local.py View File

@ -0,0 +1,26 @@
from pathlib import Path
class LocalServer:
"""
Backup server on localhost
"""
def __init__(self, params: dict):
"""
Constructor
"""
self.path = Path(params["path"])
def repo_path(self, repo_name: str) -> str:
"""
Returns a full path to a given repo
"""
return str( self.path / repo_name )
@staticmethod
def is_accessible() -> bool:
"""
Tests the connection to the server.
Since this is localhost, always return True
"""
return True

+ 16
- 0
backup/exceptions.py View File

@ -0,0 +1,16 @@
"""
Custom errors
"""
from subprocess import CompletedProcess
class BorgError(RuntimeError):
"""
Exception happened during borg command
"""
def __init__( self, command: str, run: CompletedProcess ):
super().__init__(self)
self.command = command
self.stdout = str(run.stdout)
self.stderr = str(run.stderr)

+ 162
- 0
backup/repo.py View File

@ -0,0 +1,162 @@
"""
Declares a borg repository
"""
import subprocess
from typing import List, Optional
from datetime import datetime
import json
import yaml
from backup.server import Server
from backup.exceptions import BorgError
class Repo:
"""
Interface to a borg repository
"""
def __init__(self, name: str, dirs: List[str]):
"""
Constructor.
Parameters:
- name: name of the repo. No white spaces allowed.
- dirs: list of directories to backup
"""
self.name = name
self.dirs = dirs
self.exclude_patterns: Optional[ List[str] ] = None
self.append_only = False
self.prune = True
self.keep_daily = 7
self.keep_weekly = 4
self.keep_monthly = 6
def set_exclude_patterns(self, exclude_patterns: Optional[ List[str] ]):
"""
Sets exclude patterns
"""
self.exclude_patterns = exclude_patterns
def set_append_only(self, append_only: bool):
"""
Sets (or remove) the "append_only" flag on the repo
"""
self.append_only = append_only
def set_pruning(self,
do_prune: bool,
*,
keep_daily: int = 7,
keep_weekly: int = 4,
keep_monthly: int = 6):
"""
Sets up the pruning strategy.
Parameters:
- do_prune: if True, prunes periodically the repo
- keep_daily: number of backups to keep daily (not used if do_prune is False)
- keep_weekly: number of backups to keep weekly (not used if do_prune is False)
- keep_monthly: number of backups to keep monthly (not used if do_prune is False)
"""
def exists( self, server: Server ) -> bool:
"""
Checks if the repo exists on the server
"""
repo_path = server.repo_path(self.name)
run = subprocess.run([
"borg",
"check",
repo_path
], check=False)
return run.returncode == 0
def init_or_config( self, server: Server ):
"""
Inits the repository (if not already done),
changes its configuration otherwise
"""
repo_path = server.repo_path(self.name)
if not self.exists( server ):
run = subprocess.run([
"borg", "init",
repo_path,
"--encryption", "none"
], check=False, capture_output=True)
if run.returncode != 0:
raise BorgError("init", run)
# configure append_only
run = subprocess.run([
"borg", "config",
repo_path,
"append_only", ("1" if self.append_only else "0")
], check=False, capture_output=True)
if run.returncode != 0:
raise BorgError("config (append_only)", run)
def backup( self, server: Server ) -> dict:
"""
Creates a backup archive.
Returns a dict of stats about the archive.
"""
cli = ["borg", "create",
"--stats", "--json",
"--compression", "lz4",
"--exclude-caches"]
if self.exclude_patterns is not None:
for pattern in self.exclude_patterns:
cli.append("--exclude")
cli.append(pattern)
repo_path = server.repo_path(self.name)
archive_name = datetime.now().isoformat()
cli.append(f"{ repo_path }::{ archive_name }")
for directory in self.dirs:
cli.append(directory)
run = subprocess.run(cli, check=False, capture_output=True)
if run.returncode != 0:
raise BorgError("create", run)
return json.loads(run.stdout)
def load_repo_from_dict( params: dict ) -> Repo:
"""
Creates a Repo from a parameters dict
"""
repo = Repo( params["name"], params["dirs"] )
# replace defaults
if "exclude_patterns" in params:
repo.set_exclude_patterns( params["exclude_patterns"] )
if "append_only" in params:
repo.set_append_only( params["append_only"] )
if "prune" in params:
if params["prune"] is dict:
repo.set_pruning(True,
keep_daily = params["prune"]["keep_daily"],
keep_weekly = params["prune"]["keep_weekly"],
keep_monthly = params["prune"]["keep_monthly"])
else:
repo.set_pruning(False)
return repo
def load_repo_from_file( filename: str ) -> Repo:
"""
Creates a Repo from a yaml config file
"""
with open( filename, 'r', encoding="UTF-8" ) as file:
params = yaml.safe_load( file )
return load_repo_from_dict( params )

+ 62
- 0
backup/server.py View File

@ -0,0 +1,62 @@
"""
Declares the Borg backup server class, and functions to create servers from file
"""
import yaml
from backup.detail.server_local import LocalServer
class Server:
"""
Interface for abstracting multiple types of Borg servers.
"""
def __init__(self, server_type: str, params: dict):
"""
Constructor.
Parameters:
type: server type
params: parameters
"""
if server_type == "local":
self.server = LocalServer(params)
else:
raise AssertionError("Unknown server type")
def repo_path(self, repo_name: str) -> str:
"""
Returns a full path to a given repo
"""
return self.server.repo_path( repo_name )
def is_accessible(self) -> bool:
"""
Tests the connection to the server.
Returns:
- True if the server is ready to be connected to
- False otherwise
"""
return self.server.is_accessible()
def load_server_from_dict( params: dict ) -> Server:
"""
Creates a Server from a parameters dict
"""
assert "type" in params
server_type = params["type"]
if server_type == "local":
return Server("local", params)
raise AssertionError("Unknown server type")
def load_server_from_file( filename: str ) -> Server:
"""
Creates a Server from a yaml config file
"""
with open( filename, 'r', encoding="UTF-8" ) as file:
params = yaml.safe_load( file )
return load_server_from_dict( params )

+ 11
- 0
conf/backups/test.yml View File

@ -0,0 +1,11 @@
name: test
append_only: false
dirs:
- '/tmp/backup/src'
exclude_patterns:
- '/home/*/.cache/*'
- '/var/tmp/*'
prune:
keep_daily: 7
keep_weekly: 4
keep_monthly: 6

+ 3
- 0
conf/servers/localhost.yml View File

@ -0,0 +1,3 @@
name: "localhost"
type: local
path: "/tmp/backup/tgt"

+ 75
- 0
run_backup.py View File

@ -0,0 +1,75 @@
#!/bin/python3
"""
Automated backup script.
"""
import argparse
from pathlib import Path
import os
from typing import (
Callable,
List
)
import yaml
from backup.server import Server, load_server_from_file
from backup.repo import Repo, load_repo_from_file
#############
# Arguments #
#############
parser = argparse.ArgumentParser(description="Automated backup script")
parser.add_argument('--config_dir',
metavar="DIR",
default="./conf",
help="Directory for configuration files")
args = parser.parse_args()
#############
# Functions #
#############
def list_yml_files_in_dir( directory: Path ) -> List[str]:
"""
Lists the YAML files in a given directory.
"""
files: List[str] = []
with os.scandir( directory ) as iterator:
for entry in iterator:
if not entry.is_file():
continue
if entry.name.startswith('.'):
continue
if not entry.name.endswith('.yml'):
continue
files.append(entry.path)
return files
##########
# Script #
##########
config_dir = Path(args.config_dir)
# load servers
server_dir = config_dir / "servers"
servers: List[Server] = []
for server_file in list_yml_files_in_dir( server_dir ):
servers.append( load_server_from_file( server_file ) )
# load repos
repos_dir = config_dir / "backups"
repos: List[Repo] = []
for repo_file in list_yml_files_in_dir( repos_dir ):
repos.append( load_repo_from_file( repo_file ) )
# init backups
for server in servers:
for repo in repos:
repo.init_or_config( server )
print(repo.backup( server ))

Loading…
Cancel
Save