use duplicity

This commit is contained in:
root 2014-10-21 19:36:41 +02:00
parent 5b9939ce8e
commit c148ae99df
1 changed files with 68 additions and 57 deletions

View File

@ -2,37 +2,27 @@
require 'json' require 'json'
require 'pathname' require 'pathname'
require 'fileutils' require 'fileutils'
require 'optparse' require 'open3'
RSYNC_CMD="rsync"
RSYNC_ARGS=["--archive", "--delete", "--numeric-ids"]
ZFS_SNAPSHOT_CMD="zfs-auto-snapshot"
# --quiet --syslog --label=daily --keep=31 backup/rsync"
LXC_PATH=Pathname.new("/data/containers") LXC_PATH=Pathname.new("/data/containers")
BACKUP_LOCATIONS = %w{home srv etc usr/local} BACKUP_LOCATIONS = %w{home srv etc usr/local}
CONFIG_PATH="/etc/lxc/container.json" CONFIG_PATH="/etc/lxc/container.json"
DUPLICITY_PATH= Pathname.new("/data/duplicity/")
BACKUP_PATH="/backup" BACKUP_PATH="file:///mnt/backup/duplicity"
#ZFS_DATASET="backup/rsync" FULL_BACKUP_COUNT=2
def load_config def load_config
return JSON.load(File.open(CONFIG_PATH)) return JSON.load(File.open(CONFIG_PATH))
rescue SystemCallError => e rescue SystemCallError => e
abort "failed to open configuration '#{CONFIG_PATH}', #{e}" abort "failed to open configuration '#{CONFIG_PATH}', #{e}"
rescue JSON::ParserError => e rescue JSON::ParserError => e
abort "failed to parse configuration '#{CONFIG_PATH}', #{e}" abort "failed to parse configuration '#{CONFIG_PATH}', #{e}"
end end
module Utils def sh(cmd, env={}, *args)
def self.sh(cmd, *args) pretty_args = args.map {|arg| "'#{arg}'"}
pretty_args = args.map {|arg| "'#{arg}'"} puts ([cmd] + pretty_args).join(" ")
puts ([cmd] + pretty_args).join(" ") system(env, cmd, *args)
system(cmd, *args)
end
def self.backup(remote_path, local_path)
sh RSYNC_CMD, *RSYNC_ARGS, remote_path, local_path
end
end end
class Container class Container
@ -42,26 +32,22 @@ class Container
@backup_scripts = backup_scripts @backup_scripts = backup_scripts
@path = LXC_PATH.join(name, "rootfs") @path = LXC_PATH.join(name, "rootfs")
end end
def backup def backup_paths
backup_paths = BACKUP_LOCATIONS paths = BACKUP_LOCATIONS
if @backup_paths.is_a?(Array) if @backup_paths.is_a?(Array)
backup_paths += @backup_paths paths += @backup_paths
end end
backup_paths.each do |relative_path| paths.map do |relative_path|
backup_path = @path.join(relative_path) @path.join(relative_path)
local_path = Pathname.new(@name).join(relative_path)
FileUtils.mkdir_p(local_path)
if File.exists?(backup_path) && !empty_directory?(backup_path)
Utils.backup(backup_path.to_s, File.dirname(local_path))
end
end end
end end
def run_backup_scripts def run_backup_scripts
if @backup_scripts.is_a?(Array) if @backup_scripts.is_a?(Array)
@backup_scripts.each do |script| @backup_scripts.map do |script|
backup_script(script) backup_script(script)
end end
else
[]
end end
end end
@ -78,12 +64,13 @@ class Container
if backupname.nil? if backupname.nil?
abort("backupname not set for backup-scripts for container '#{@name}'") abort("backupname not set for backup-scripts for container '#{@name}'")
end end
backupname = backupname.gsub("/", "") backupname = DUPLICITY_PATH.join(backupname.gsub("/", ""))
FileUtils.mkdir_p(backupname) FileUtils.mkdir_p(backupname)
puts "cd #{backupname}" puts "cd #{backupname}"
Dir.chdir(backupname) do Dir.chdir(backupname) do
Utils.sh(command) sh(command)
end end
backupname
end end
def empty_directory?(path) def empty_directory?(path)
@ -92,29 +79,53 @@ class Container
end end
end end
options = {} if ARGV.size > 0
OptionParser.new do |opts| if ["full", "incr"].include?(ARGV.first)
opts.on("-l", "--label [LABEL]", String, "daily, hourly") do |label| action = ARGV.first
options[:label] = label else
$stderr.puts "action must be full or incr"
$stderr.puts "USAGE #{File.basename($0)} [full|incr]"
exit 1
end end
opts.on("-k", "--keep [NUMBER]", Integer, "Number of backups to keep") do |keep| else
options[:keep] = keep action = "incr"
end end
end.parse!
Dir.chdir(DUPLICITY_PATH) do
puts "cd #{BACKUP_PATH}" config = load_config
Dir.chdir(BACKUP_PATH) do backup_paths = BACKUP_LOCATIONS.map do |location|
BACKUP_LOCATIONS.each do |location| "/#{location}"
FileUtils.mkdir_p(location) end
Utils.backup("/" + location, "localhost") config["network"].each do |container, data|
end next if data["lxc"] == false
config = load_config container = Container.new(container, data["backup-paths"], data["backup-scripts"])
config["network"].each do |container, data| backup_paths += container.backup_paths
next if data["lxc"] == false backup_paths += container.run_backup_scripts
container = Container.new(container, data["backup-paths"], data["backup-scripts"]) end
container.backup gpg_args = ["--gpg-options", "--secret-keyring ./duplicity.sec --keyring ./duplicity.pub"]
container.run_backup_scripts args = gpg_args + ["--verbosity", "notice",
end "--encrypt-key", "AF5834A6",
#args = ["--label", options[:label], "--keep", options[:keep].to_s, ZFS_DATASET] "--sign-key", "AF5834A6",
#Utils.sh(ZFS_SNAPSHOT_CMD, *args) "--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 end