new backup script, see #11

This commit is contained in:
Joshua Tauberer 2014-06-03 20:21:17 +00:00
parent 51dd2ed70b
commit 89730bd643
4 changed files with 141 additions and 6 deletions

54
management/backup.py Executable file
View file

@ -0,0 +1,54 @@
#!/usr/bin/python3
# This script performs a backup of all user data:
# 1) System services are stopped while a copy of user data is made.
# 2) An incremental backup is made using rdiff-backup into the
# directory STORAGE_ROOT/backup/rdiff-history. This directory
# will contain the latest files plus a complete history for
# all prior backups.
# 3) The stopped services are restarted.
# 4) The backup directory is compressed into a single file using tar.
# 5) That file is encrypted with a long password stored in backup/secret_key.txt.
import os, os.path, subprocess
from utils import exclusive_process, load_environment
env = load_environment()
exclusive_process("backup")
# Ensure the backup directory exists.
backup_dir = os.path.join(env["STORAGE_ROOT"], 'backup')
rdiff_backup_dir = os.path.join(backup_dir, 'rdiff-history')
os.makedirs(backup_dir, exist_ok=True)
# Stop services.
subprocess.check_call(["service", "dovecot", "stop"])
subprocess.check_call(["service", "postfix", "stop"])
# Update the backup directory which stores increments.
try:
subprocess.check_call([
"rdiff-backup",
"--exclude", backup_dir,
env["STORAGE_ROOT"],
rdiff_backup_dir])
except subprocess.CalledProcessError:
pass
# Start services.
subprocess.check_call(["service", "dovecot", "start"])
subprocess.check_call(["service", "postfix", "start"])
# Tar the rdiff-backup directory into a single file encrypted using the backup private key.
os.system(
"tar -zcC %s . | openssl enc -aes-256-cbc -a -salt -in /dev/stdin -out %s -pass file:%s"
%
( rdiff_backup_dir,
os.path.join(backup_dir, "latest.tgz.enc"),
os.path.join(backup_dir, "secret_key.txt"),
))
# The backup can be decrypted with:
# openssl enc -d -aes-256-cbc -a -in latest.tgz.enc -out /dev/stdout -pass file:secret_key.txt | tar -z

View file

@ -5,13 +5,11 @@ import os, os.path
from flask import Flask, request, render_template
app = Flask(__name__)
# Load settings from /etc/mailinabox.conf.
env = { }
for line in open("/etc/mailinabox.conf"): env.setdefault(*line.strip().split("=", 1))
env["CONF_DIR"] = os.path.join(os.path.dirname(__file__), "../conf")
import utils
from mailconfig import get_mail_users, add_mail_user, set_mail_password, remove_mail_user, get_mail_aliases, get_mail_domains, add_mail_alias, remove_mail_alias
env = utils.load_environment()
@app.route('/')
def index():
return render_template('index.html')

77
management/utils.py Normal file
View file

@ -0,0 +1,77 @@
def load_environment():
# Load settings from /etc/mailinabox.conf.
import os.path
env = { }
for line in open("/etc/mailinabox.conf"): env.setdefault(*line.strip().split("=", 1))
env["CONF_DIR"] = os.path.join(os.path.dirname(__file__), "../conf")
return env
def exclusive_process(name):
# Ensure that a process named `name` does not execute multiple
# times concurrently.
import os, sys, atexit
pidfile = '/var/run/mailinabox-%s.pid' % name
mypid = os.getpid()
# Attempt to get a lock on ourself so that the concurrency check
# itself is not executed in parallel.
with open(__file__, 'r+') as flock:
# Try to get a lock. This blocks until a lock is acquired. The
# lock is held until the flock file is closed at the end of the
# with block.
os.lockf(flock.fileno(), os.F_LOCK, 0)
# While we have a lock, look at the pid file. First attempt
# to write our pid to a pidfile if no file already exists there.
try:
with open(pidfile, 'x') as f:
# Successfully opened a new file. Since the file is new
# there is no concurrent process. Write our pid.
f.write(str(mypid))
atexit.register(clear_my_pid, pidfile)
return
except FileExistsError:
# The pid file already exixts, but it may contain a stale
# pid of a terminated process.
with open(pidfile, 'r+') as f:
# Read the pid in the file.
existing_pid = None
try:
existing_pid = int(f.read().strip())
except ValueError:
pass # No valid integer in the file.
# Check if the pid in it is valid.
if existing_pid:
if is_pid_valid(existing_pid):
print("Another %s is already running (pid %d)." % (name, existing_pid), file=sys.stderr)
sys.exit(1)
# Write our pid.
f.seek(0)
f.write(str(mypid))
f.truncate()
atexit.register(clear_my_pid, pidfile)
def clear_my_pid(pidfile):
import os
os.unlink(pidfile)
def is_pid_valid(pid):
"""Checks whether a pid is a valid process ID of a currently running process."""
# adapted from http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid
import os, errno
if pid <= 0: raise ValueError('Invalid PID.')
try:
os.kill(pid, 0)
except OSError as err:
if err.errno == errno.ESRCH: # No such process
return False
elif err.errno == errno.EPERM: # Not permitted to send signal
return True
else: # EINVAL
raise
else:
return True

View file

@ -2,7 +2,13 @@
source setup/functions.sh
apt_install python3-flask links
apt_install python3-flask links rdiff-backup
# Create a backup directory and a random key for encrypting backups.
mkdir -p $STORAGE_ROOT/backup
if [ ! -f $STORAGE_ROOT/backup/secret_key.txt ]; then
openssl rand -base64 2048 > $STORAGE_ROOT/backup/secret_key.txt
fi
# Link the management server daemon into a well known location.
rm -f /usr/bin/mailinabox-daemon