#!/usr/bin/env ruby require 'json' require 'pathname' require 'fileutils' require 'optparse' 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") BACKUP_LOCATIONS = %w{home srv etc usr/local} CONFIG_PATH="/etc/lxc/container.json" BACKUP_PATH="/backup" #ZFS_DATASET="backup/rsync" 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 module Utils def self.sh(cmd, *args) pretty_args = args.map {|arg| "'#{arg}'"} puts ([cmd] + pretty_args).join(" ") system(cmd, *args) end def self.backup(remote_path, local_path) sh RSYNC_CMD, *RSYNC_ARGS, remote_path, local_path end 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 backup_paths = BACKUP_LOCATIONS if @backup_paths.is_a?(Array) backup_paths += @backup_paths end backup_paths.each do |relative_path| backup_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 def run_backup_scripts if @backup_scripts.is_a?(Array) @backup_scripts.each do |script| backup_script(script) end 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 = backupname.gsub("/", "") FileUtils.mkdir_p(backupname) puts "cd #{backupname}" Dir.chdir(backupname) do Utils.sh(command) end end def empty_directory?(path) return false unless Dir.exists?(path) return Dir.entries(path).size <= 2 # - [".", ".."] end end options = {} OptionParser.new do |opts| opts.on("-l", "--label [LABEL]", String, "daily, hourly") do |label| options[:label] = label end opts.on("-k", "--keep [NUMBER]", Integer, "Number of backups to keep") do |keep| options[:keep] = keep end end.parse! puts "cd #{BACKUP_PATH}" Dir.chdir(BACKUP_PATH) do BACKUP_LOCATIONS.each do |location| FileUtils.mkdir_p(location) Utils.backup("/" + location, "localhost") end config = load_config config["network"].each do |container, data| next if data["lxc"] == false container = Container.new(container, data["backup-paths"], data["backup-scripts"]) container.backup container.run_backup_scripts end #args = ["--label", options[:label], "--keep", options[:keep].to_s, ZFS_DATASET] #Utils.sh(ZFS_SNAPSHOT_CMD, *args) end