├── README ├── bupper.yml.example ├── README.md └── bin └── bupper /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /bupper.yml.example: -------------------------------------------------------------------------------- 1 | --- 2 | global: 3 | log_dir: '/var/log/bupper' 4 | log_level: 'debug' 5 | profiles: 6 | root_home: 7 | source: 8 | - '/root' 9 | remote: true 10 | destination: 'server:/var/lib/backup' 11 | backup_exclude: 12 | - 'src1' 13 | temp: 14 | source: 15 | - '/tmp' 16 | - '/var/tmp' 17 | pre_backup_commands: 18 | - '/usr/local/bin/pre_backup.sh' 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bupper - a bup backup profile manager 2 | 3 | ## Introduction 4 | 5 | bupper is a profile manager for [bup](https://github.com/bup/bup), the backup utility based on git. It helps you to create backup profiles with pre- and post-backup tasks, excludes, backup destinations and much more. 6 | 7 | ## Installation 8 | 9 | * Install [bup](https://github.com/bup/bup) > 0.25 10 | Ubuntu 14.04 has already this version shipped: `apt-get install bup` 11 | * Optional: Install `sshfs`, needed for restoring from remote server 12 | * `git clone https://github.com/tobru/bupper.git && cd bupper && ln -s $(pwd)/bin/bupper /usr/local/bin/bupper` 13 | 14 | ## Configuration 15 | 16 | The configuration is done in `/etc/bupper.yml` or in the config file you specify with `--config `. Example: 17 | 18 | ``` yaml 19 | --- 20 | global: 21 | log_dir: '/var/log/bupper' 22 | log_level: 'debug' 23 | profiles: 24 | profile_local: 25 | source: 26 | - '/etc' 27 | bup_dir: '/var/lib/backup/bup' 28 | pre_backup_commands: 29 | - '/usr/local/bin/pre_backup.sh' 30 | post_backup_commands: 31 | - '/usr/local/bin/post_backup.sh' 32 | backup_exclude: 33 | - 'passwd' 34 | - 'shadow' 35 | profile_remote: 36 | source: '/tmp' 37 | remote: true 38 | destination: 'server:/var/lib/backup' 39 | ``` 40 | 41 | ### Detailed configuration parameter description 42 | 43 | **global** 44 | 45 | * *log_dir* (string): path where the log will be saved 46 | * *log_level* (string): choose one of: debug, info, warn, error, fatal, unknown 47 | 48 | **profiles** 49 | 50 | The profile name should only contain letters and underscores. 51 | 52 | * *source* (array): source directories for the backup 53 | * *bup_dir* (string): bup working directory, defaults to /root/.bup 54 | * *remote* (boolean): is the destination local or remote 55 | * *destination* (string): destination for remote backups 56 | * *backup_exclude* (array): exclude directories or files 57 | * *pre_backup_commands* (array): commands to execute before the backup 58 | * *post_backup_commands* (array): commands to execute after the backup 59 | 60 | ## Usage 61 | 62 | To use bup, you need to initialize a local repository: `bup init`. 63 | If you're using `bup_dir` in the configuration file to specify the working directory, use `BUP_DIR= bup init` 64 | The default for `bup_dir` is `/root/.bup` 65 | 66 | ### Backup 67 | 68 | This runs `bup index` and `bup save` and the specified pre- and post-backup scripts (if there are any) 69 | 70 | * `bupper backup -p ` 71 | * `bupper backup -p all` 72 | 73 | ### Restore 74 | 75 | It just outputs some help text, but does nothing by itself. 76 | 77 | * `bupper restore -p ` 78 | * `bupper restore -p all` 79 | 80 | ### Other 81 | 82 | * `bupper -h`: help 83 | 84 | ### Parameter 85 | 86 | * `-c | --config`: Path to the config file (Default: `/etc/bupper.yml` 87 | * `-p | --profile`: Name of the profile or `all` (Default: all) 88 | 89 | ## The MIT License (MIT) 90 | 91 | ``` 92 | Copyright (c) 2014 Tobias BRunner 93 | 94 | Permission is hereby granted, free of charge, to any person obtaining a copy 95 | of this software and associated documentation files (the "Software"), to deal 96 | in the Software without restriction, including without limitation the rights 97 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 98 | copies of the Software, and to permit persons to whom the Software is 99 | furnished to do so, subject to the following conditions: 100 | 101 | The above copyright notice and this permission notice shall be included in all 102 | copies or substantial portions of the Software. 103 | 104 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 105 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 106 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 107 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 108 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 109 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 110 | SOFTWARE. 111 | ``` 112 | 113 | ## Addendum 114 | 115 | This project was done by following the [Readme Driven Development](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html) pattern. 116 | It's my first Ruby project, so bear with me for the ugliness. 117 | -------------------------------------------------------------------------------- /bin/bupper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'yaml' 4 | require 'net/smtp' 5 | require 'optparse' 6 | require 'open3' 7 | require 'logger' 8 | require 'os' 9 | require 'ptools' 10 | 11 | options = {} 12 | 13 | opt_parser = OptionParser.new do |opt| 14 | opt.banner = 'Usage: bupper COMMAND [OPTIONS]' 15 | opt.separator '' 16 | opt.separator 'Commands' 17 | opt.separator ' backup: run backup for the specified profile, or all' 18 | opt.separator ' restore: initialize all the things to restore a backup (mainly: mount sshfs)' 19 | opt.separator '' 20 | opt.separator 'Options' 21 | 22 | options[:profile] = 'all' 23 | opt.on('-p','--profile PROFILE','name of the profile as specified in the configuration file, defaults to all') do |profile| 24 | options[:profile] = profile 25 | end 26 | 27 | options[:config] = '/etc/bupper.yml' 28 | opt.on('-c','--config FILENAME','filename of the config file, including the path') do |config| 29 | options[:config] = config 30 | end 31 | 32 | opt.on('-h','--help','help') do 33 | puts opt_parser 34 | end 35 | end 36 | 37 | opt_parser.parse! 38 | action = ARGV[0] 39 | 40 | # load and parse configuration 41 | if File.exists?(options[:config]) 42 | configuration = YAML.load_file(options[:config]) 43 | else 44 | puts "Configuration file #{options[:config]} not found" 45 | exit 1 46 | end 47 | 48 | # setup logging 49 | if not File.exist?(configuration['global']['log_dir']) 50 | puts "Log directory #{configuration['global']['log_dir']} does not exist" 51 | exit 1 52 | end 53 | class MultiIO 54 | def initialize(*targets) 55 | @targets = targets 56 | end 57 | 58 | def write(*args) 59 | @targets.each {|t| t.write(*args)} 60 | end 61 | 62 | def close 63 | @targets.each(&:close) 64 | end 65 | end 66 | log_file = File.open(configuration['global']['log_dir'] + '/bupper.log', 'a') 67 | @log = Logger.new MultiIO.new(STDOUT, log_file) 68 | case configuration['global']['log_level'] 69 | when 'debug' then @log.level = Logger::DEBUG 70 | when 'info' then @log.level = Logger::INFO 71 | when 'warn' then @log.level = Logger::WARN 72 | when 'error' then @log.level = Logger::ERROR 73 | when 'fatal' then @log.level = Logger::FATAL 74 | when 'unknown' then @log.level = Logger::UNKNOWN 75 | end 76 | 77 | def run_command(command) 78 | @log.info('running command: ' + command) 79 | if system(command) 80 | @log.info('command successful') 81 | return true 82 | else 83 | @log.error('command failed') 84 | return false 85 | end 86 | end 87 | 88 | def backup(name, profile) 89 | @log.info('action: backup. profile: ' + name) 90 | 91 | # run pre_backup_commands if configured 92 | profile['pre_backup_commands'].each do |command| run_command(command) 93 | end if profile.has_key?('pre_backup_commands') 94 | 95 | # run bup index 96 | index_status = bup_index(name, profile) 97 | 98 | # run bup save if indexing was successfull 99 | bup_save(name, profile) if index_status 100 | 101 | # run post_backup_commands if configured 102 | profile['post_backup_commands'].each do |command| run_command(command) 103 | end if profile.has_key?('post_backup_commands') 104 | end 105 | 106 | def bup_index(name, profile) 107 | @log.info('running bup index for profile ' + name) 108 | 109 | # define bup_dir 110 | if profile.has_key?('bup_dir') 111 | bup_dir = profile['bup_dir'] 112 | else 113 | bup_dir = '/root/.bup' 114 | end 115 | 116 | # define command to run 117 | excludes = '' 118 | profile['backup_exclude'].each do |exclude| excludes << '--exclude=' + exclude + ' ' end if profile.has_key?('backup_exclude') 119 | profile['backup_exclude_rx'].each do |exclude| excludes << '--exclude-rx=' + exclude + ' ' end if profile.has_key?('backup_exclude_rx') 120 | 121 | command = "bup index -x -f #{bup_dir}/index_#{name} #{excludes} #{profile['source'].join(' ')}" 122 | 123 | @log.debug("BUP_DIR=#{bup_dir}") 124 | @log.debug(command) 125 | if system({'BUP_DIR' => bup_dir}, command) 126 | @log.info('indexing successful for profile ' + name) 127 | return true 128 | else 129 | @log.error('indexing failed for profile ' + name) 130 | return false 131 | end 132 | end 133 | 134 | def bup_save(name, profile) 135 | @log.info('running bup save for profile ' + name) 136 | 137 | # define bup_dir 138 | if profile.has_key?('bup_dir') 139 | bup_dir = profile['bup_dir'] 140 | else 141 | bup_dir = '/root/.bup' 142 | end 143 | 144 | # decide if the destination is remote or not 145 | if profile.has_key?('remote') then remote = profile['remote'] else remote = false end 146 | if remote 147 | command = "bup save -r #{profile['destination']} -n #{name} -f #{bup_dir}/index_#{name} " + profile['source'].join(' ') 148 | else 149 | command = "bup save -n #{name} -f #{bup_dir}/index_#{name} " + profile['source'].join(' ') 150 | end 151 | 152 | @log.debug("BUP_DIR=#{bup_dir}") 153 | @log.debug(command) 154 | if system({'BUP_DIR' => bup_dir}, command) 155 | @log.info('saving successful for profile ' + name) 156 | return true 157 | else 158 | @log.error('saving failed for profile ' + name) 159 | return false 160 | end 161 | end 162 | 163 | def restore_help(name, profile) 164 | 165 | # define bup_dir 166 | if profile.has_key?('bup_dir') 167 | bup_dir = profile['bup_dir'] 168 | else 169 | bup_dir = '/root/.bup' 170 | end 171 | 172 | puts "" 173 | puts "Restore help (profile #{name})" 174 | puts "------------" 175 | 176 | if profile.has_key?('remote') then remote = profile['remote'] else remote = false end 177 | if remote 178 | puts "This profile has it's data saved on a remote server: #{profile['destination']}" 179 | puts "" 180 | puts "You need to mount the remote directory with sshfs:" 181 | puts "" 182 | 183 | unless File.which 'sshfs' 184 | puts " sshfs is not installed!" 185 | puts " apt-get install sshfs" if OS.linux? 186 | puts " Download pkg from https://github.com/osxfuse/sshfs/releases/latest" if OS.mac? 187 | puts "" 188 | end 189 | 190 | puts " mkdir /mnt/#{name}" unless File.exist?("/mnt/#{name}") 191 | puts " sshfs #{profile['destination']} /mnt/#{name}" 192 | puts "" 193 | puts "Now use the bup commands to restore files:" 194 | puts "" 195 | puts " BUP_DIR=/mnt/#{name} bup ls" 196 | puts " BUP_DIR=/mnt/#{name} bup restore" 197 | puts "" 198 | puts "When finished:" 199 | puts " umount /mnt/#{name}" 200 | puts "" 201 | else 202 | puts "This profile has it's data saved local on this server" 203 | puts "" 204 | puts "Use the bup commands to restore files:" 205 | puts "" 206 | puts " BUP_DIR=#{bup_dir} bup ls" 207 | puts " BUP_DIR=#{bup_dir} bup restore" 208 | puts "" 209 | end 210 | 211 | end 212 | 213 | # do the action 214 | case action 215 | when 'backup' 216 | if options[:profile].match('all') 217 | configuration['profiles'].each do |name, profile| 218 | if profile.has_key?('enabled') then enabled = profile['enabled'] else enabled = true end 219 | backup(name, profile) if enabled 220 | @log.info("profile #{name} is disabled for 'all' backup. skipping.") if not enabled 221 | end 222 | elsif configuration['profiles'].has_key?(options[:profile]) 223 | backup(options[:profile], configuration['profiles'][options[:profile]]) 224 | else 225 | @log.fatal('profile ' + options[:profile] + ' not found in configuration') 226 | exit 1 227 | end 228 | when 'restore' 229 | if options[:profile].match('all') 230 | configuration['profiles'].each do |name, profile| 231 | restore_help(name, profile) 232 | end 233 | elsif configuration['profiles'].has_key?(options[:profile]) 234 | restore_help(options[:profile], configuration['profiles'][options[:profile]]) 235 | else 236 | @log.fatal('profile ' + options[:profile] + ' not found in configuration') 237 | exit 1 238 | end 239 | else 240 | puts opt_parser 241 | end 242 | 243 | # cleanup 244 | @log.close 245 | --------------------------------------------------------------------------------