#!/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=1 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 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 = ["--sign-key", "AF5834A6", "--encrypt-key", "AF5834A6", "--gpg-options", "--secret-keyring ./duplicity.sec --keyring ./duplicity.pub"] args = ["--archive-dir", "cache", "--log-file", "/var/log/duplicity.log"] + gpg_args backup_args = args + ["--verbosity", "notice", "--full-if-older-than", "30D", "--num-retries", "3", "--asynchronous-upload", "--volsize", "250", "--include-globbing-filelist", "/dev/stdin", "--exclude", "**", "/", BACKUP_PATH] env = { "PASSPHRASE" => File.read("pgp-passphrase") } sh("find", "cache", "-type", "f", "-name", "lockfile.lock", "-exec", "rm -f {} ;") sh("duplicity", env, "cleanup", "--force", BACKUP_PATH, *args) stdin, stdout, stderr = Open3.popen3(env, "duplicity", *backup_args) backup_paths.each do |path| stdin.puts(path) puts(path) end stdin.close puts stdout.read puts stderr.read sh("duplicity", env, "remove-older-than", "30D", "--force", BACKUP_PATH, *args) sh("duplicity", env, "remove-all-inc-of-but-n-full", FULL_BACKUP_COUNT.to_s, "--force", BACKUP_PATH, *args) end