server-scripts/backup-container

132 lines
3.5 KiB
Ruby
Executable File

#!/usr/bin/env ruby
require 'json'
require 'pathname'
require 'fileutils'
require 'open3'
LXC_PATH=Pathname.new("/data/containers")
BACKUP_LOCATIONS = %w{home srv etc usr/local}
CONFIG_PATH="/etc/lxc/container.json"
DUPLICITY_PATH= Pathname.new("/data/duplicity/")
BACKUP_PATH="file:///mnt/backup/duplicity"
FULL_BACKUP_COUNT=2
def load_config
return JSON.load(File.open(CONFIG_PATH))
rescue SystemCallError => e
abort "failed to open configuration '#{CONFIG_PATH}', #{e}"
rescue JSON::ParserError => e
abort "failed to parse configuration '#{CONFIG_PATH}', #{e}"
end
def sh(cmd, env={}, *args)
pretty_args = args.map {|arg| "'#{arg}'"}
puts ([cmd] + pretty_args).join(" ")
system(env, cmd, *args)
end
class Container
def initialize(name, backup_paths, backup_scripts)
@name = name
@backup_paths = backup_paths
@backup_scripts = backup_scripts
@path = LXC_PATH.join(name, "rootfs")
end
def backup_paths
paths = BACKUP_LOCATIONS
if @backup_paths.is_a?(Array)
paths += @backup_paths
end
paths.map do |relative_path|
@path.join(relative_path)
end
end
def run_backup_scripts
if @backup_scripts.is_a?(Array)
@backup_scripts.map do |script|
backup_script(script)
end
else
[]
end
end
private
def backup_script(script)
unless script.is_a?(Hash)
abort("backup-scripts: Expected an Object, got #{script.class}")
end
command = script["command"]
if command.nil?
abort("command not set for backup-scripts for container '#{@name}'")
end
backupname = script["backupname"]
if backupname.nil?
abort("backupname not set for backup-scripts for container '#{@name}'")
end
backupname = DUPLICITY_PATH.join(backupname.gsub("/", ""))
FileUtils.mkdir_p(backupname)
puts "cd #{backupname}"
Dir.chdir(backupname) do
sh(command)
end
backupname
end
def empty_directory?(path)
return false unless Dir.exists?(path)
return Dir.entries(path).size <= 2 # - [".", ".."]
end
end
if ARGV.size > 0
if ["full", "incr"].include?(ARGV.first)
action = ARGV.first
else
$stderr.puts "action must be full or incr"
$stderr.puts "USAGE #{File.basename($0)} [full|incr]"
exit 1
end
else
action = "incr"
end
Dir.chdir(DUPLICITY_PATH) do
config = load_config
backup_paths = BACKUP_LOCATIONS.map do |location|
"/#{location}"
end
config["network"].each do |container, data|
next if data["lxc"] == false
container = Container.new(container, data["backup-paths"], data["backup-scripts"])
backup_paths += container.backup_paths
backup_paths += container.run_backup_scripts
end
gpg_args = ["--gpg-options", "--secret-keyring ./duplicity.sec --keyring ./duplicity.pub"]
args = gpg_args + ["--verbosity", "notice",
"--encrypt-key", "AF5834A6",
"--sign-key", "AF5834A6",
"--full-if-older-than", "60D",
"--num-retries", "3",
"--asynchronous-upload",
"--volsize", "250",
"--include-globbing-filelist", "/dev/stdin",
"--archive-dir", "cache",
"--log-file", "/var/log/duplicity.log",
action, "/", BACKUP_PATH]
env = { "PASSPHRASE" => File.read("pgp-passphrase") }
stdin, stdout, stderr = Open3.popen3(env, "duplicity", *args)
if action == "full"
sh("duplicity", env, "remove-all-but-n-full", FULL_BACKUP_COUNT.to_s, BACKUP_PATH, *gpg_args)
end
backup_paths.each do |path|
stdin.puts("+ #{path}")
puts("+ #{path}")
end
stdin.puts("- **")
puts("- **")
stdin.close
puts stdout.read
puts stderr.read
end