|
| 1 | +#!/usr/bin/php |
| 2 | +<?php |
| 3 | +/* |
| 4 | + * rsnapshot-once |
| 5 | + * Copyright (C) 2013 Philipp C. Heckel <philipp.heckel@gmail.com> |
| 6 | + * |
| 7 | + * Original blog post at: |
| 8 | + * http://blog.philippheckel.com/2013/06/28/script-run-rsnapshot-backups-only-once-and-rollback-failed-backups-using-rsnapshot-once/ |
| 9 | + * |
| 10 | + * This program is free software: you can redistribute it and/or modify |
| 11 | + * it under the terms of the GNU General Public License as published by |
| 12 | + * the Free Software Foundation, either version 3 of the License, or |
| 13 | + * (at your option) any later version. |
| 14 | + * |
| 15 | + * This program is distributed in the hope that it will be useful, |
| 16 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 17 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 18 | + * GNU General Public License for more details. |
| 19 | + * |
| 20 | + * You should have received a copy of the GNU General Public License |
| 21 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 22 | + */ |
| 23 | + |
| 24 | +$opts = getopt("c:"); |
| 25 | +// General failure |
| 26 | +if (!$opts) { |
| 27 | + die( |
| 28 | + "Usage:\n" |
| 29 | + . " {$argv[0]} [-c cfgfile] (daily|weekly|monthly)\n\n" |
| 30 | + |
| 31 | + ."Description:\n" |
| 32 | + ." rsnapshot-once is a wrapper for rsnapshot to ensure that daily, weekly\n" |
| 33 | + ." and monthly tasks are run only once in the respective time period, i.e.\n" |
| 34 | + ." it ensures that 'weekly' backups are executed only once a week,\n" |
| 35 | + ." regardless how often rsnapshot-timer is called.\n\n" |
| 36 | + ." rsnapshot-once accepts the same parameters as rsnapshot and uses the\n" |
| 37 | + ." same config file. It does not need any additional configuration.\n\n" |
| 38 | + |
| 39 | + ."Example (crontab):\n" |
| 40 | + ." # Job to run every hour, rsnapshot-once makes sure it only runs once a day.\n" |
| 41 | + ." 0 * * * * {$argv[0]} -c /home/user/.rsnapshot/rsnapshot.home.conf daily\n" |
| 42 | + ); |
| 43 | +} |
| 44 | +//print_r($opts); |
| 45 | +// -c (Config file) |
| 46 | +if (!isset($opts['c'])) { |
| 47 | + $opts['c'] = "/etc/rsnapshot.conf"; |
| 48 | +} |
| 49 | +else if (isset($opts['c']) && !file_exists($opts['c'])) { |
| 50 | + die("Config file {$opts['c']} does not exist.\n"); |
| 51 | +} |
| 52 | +$cfgfile = $opts['c']; |
| 53 | +// Read logfile |
| 54 | +$logfile = trim(`cat '{$opts['c']}' | grep '^logfile'`); |
| 55 | +if (!preg_match('!^logfile\t(.+)$!', $logfile, $m)) { |
| 56 | + die("Config option 'logfile' not found in config file.\n"); |
| 57 | +} |
| 58 | +$logfile = $m[1]; |
| 59 | +logft("## STARTING BACKUP ######################\n"); |
| 60 | +$xargs = $argv; array_shift($xargs); |
| 61 | +logft("\$ ".basename($argv[0])." '".join("' '", $xargs)."'\n"); |
| 62 | +logft("Config read from: $cfgfile\n"); |
| 63 | +logft("- logfile = $logfile\n"); |
| 64 | +// Read snapshot_root |
| 65 | +$snapshot_root = trim(`cat '{$opts['c']}' | grep '^snapshot_root'`); |
| 66 | +if (!preg_match('!^snapshot_root\t(.+)$!', $snapshot_root, $m)) { |
| 67 | + logft("Config option 'snapshot_root' not found in config file. EXITING.\n"); |
| 68 | + exit; |
| 69 | +} |
| 70 | +$snapshot_root = $m[1]; |
| 71 | +logft("- snapshot_root = $snapshot_root\n"); |
| 72 | +if (!preg_match('!/$!', $snapshot_root)) { |
| 73 | + logft("Invalid config. 'snapshot_root' has no trailing slash. EXITING.\n"); |
| 74 | + logft("## BACKUP ABORTED #######################\n\n"); |
| 75 | + exit; |
| 76 | +} |
| 77 | +// Other argument (weekly, daily, monthly) |
| 78 | +$jobname = $argv[count($argv)-1]; |
| 79 | +if (!in_array($jobname, array("daily", "weekly", "monthly"))) { |
| 80 | + logft("Jobname must be 'daily', 'weekly' or 'monthly'. EXITING.\n"); |
| 81 | + logft("## BACKUP ABORTED #######################\n\n"); |
| 82 | + exit; |
| 83 | +} |
| 84 | +// Check pid file |
| 85 | +$pidfile = "{$snapshot_root}.rsnapshot-once.pid"; |
| 86 | +logft("Checking rsnapshot-once pidfile at $pidfile ... "); |
| 87 | +if (file_exists($pidfile)) { |
| 88 | + $pidfilepid = trim(file_get_contents($pidfile)); |
| 89 | + if (file_exists("/proc/$pidfilepid")) { |
| 90 | + logf("Exists. PID $pidfilepid still running. ABORTING.\n"); |
| 91 | + logft("## BACKUP ABORTED #######################\n\n"); |
| 92 | + exit; |
| 93 | + } |
| 94 | + else { |
| 95 | + logf("Exists. PID $pidfilepid not running. Script crashed before.\n"); |
| 96 | + $sorted_backups = glob("{$snapshot_root}$jobname.*"); |
| 97 | + echo ": ".join($sorted_backups); |
| 98 | + exit; |
| 99 | + natsort($sorted_backups); |
| 100 | + if (count($sorted_backups) == 0) { |
| 101 | + logft("No previous backups found. No cleanup necessary.\n"); |
| 102 | + } |
| 103 | + else { |
| 104 | + logft("Cleaning up unfinished backup ...\n"); |
| 105 | + $first_backup = array_shift($sorted_backups); |
| 106 | + logft("- Deleting $first_backup ... "); |
| 107 | + |
| 108 | + // Double check before deleting (!) |
| 109 | + if (!isset($jobname) || empty($jobname) || !file_exists($first_backup) || strlen($first_backup) < 2 || !preg_match('/^(.+)\.(\d+)$/', $first_backup)) { |
| 110 | + logf("Script security issue. EXITING.\n"); |
| 111 | + logft("## BACKUP ABORTED #######################\n\n"); |
| 112 | + exit; |
| 113 | + } |
| 114 | + else { |
| 115 | + // Delete! |
| 116 | + $slashed_first_backup = addslashes($first_backup); |
| 117 | + `rm -rf '$slashed_first_backup'`; |
| 118 | + logf("DONE\n"); |
| 119 | + } |
| 120 | + |
| 121 | + while (count($sorted_backups) > 0) { |
| 122 | + $nth_backup = array_shift($sorted_backups); |
| 123 | + |
| 124 | + if (preg_match('/^(.+)\.(\d+)$/', $nth_backup, $m)) { |
| 125 | + $n_minus_1th_backup = $m[1].".".($m[2]-1); |
| 126 | + logft("- Moving $nth_backup to $n_minus_1th_backup ... "); |
| 127 | + rename($nth_backup, $n_minus_1th_backup); |
| 128 | + logf("DONE\n"); |
| 129 | + } |
| 130 | + } |
| 131 | + } |
| 132 | + } |
| 133 | +} |
| 134 | +else { |
| 135 | + logf("Does not exist. Last backup was clean.\n"); |
| 136 | +} |
| 137 | +logft("Checking delays (minimum 15 minutes since startup/wakeup) ...\n"); |
| 138 | +// Backup delay (uptime/resumetime + 15 minutes) |
| 139 | +$uptime_minutes = 0; |
| 140 | +$uptime_minutes = strtok(exec("cat /proc/uptime"), ".")/60; |
| 141 | +if ($uptime_minutes) { |
| 142 | + if ($uptime_minutes < 15) { |
| 143 | + logft("- Computer uptime is ".sprintf("%.1f", $uptime_minutes)." minutes. NOT ENOUGH. EXITING.\n"); |
| 144 | + logft("## BACKUP ABORTED #######################\n\n"); |
| 145 | + exit; |
| 146 | + } |
| 147 | + else { |
| 148 | + logft("- Computer uptime is ".sprintf("%.1f", $uptime_minutes)." minutes. THAT'S OKAY.\n"); |
| 149 | + } |
| 150 | +} |
| 151 | +// Get time of resume |
| 152 | +// http://unix.stackexchange.com/questions/22140/determine-time-of-last-suspend-to-ram |
| 153 | +$wakeup_minutes = 0; |
| 154 | +$wakeup_date = trim(`egrep 'Running hooks for (resume|thaw)' /var/log/pm-suspend.log | tail -n 1 | sed 's/^\(.*\):.*$/\\1/'`); |
| 155 | +if (isset($wakeup_date) && $wakeup_date) { |
| 156 | + $wakeup_minutes = (time() - intval(`date --date="$wakeup_date" +%s`))/60; |
| 157 | + if ($wakeup_minutes) { |
| 158 | + if ($wakeup_minutes < 15) { |
| 159 | + logft("- Computer resume time is ".sprintf("%.1f", $wakeup_minutes)." minutes. NOT ENOUGH. EXITING.\n"); |
| 160 | + logft("## BACKUP ABORTED #######################\n\n"); |
| 161 | + exit; |
| 162 | + } |
| 163 | + else { |
| 164 | + logft("- Computer resume time is ".sprintf("%.1f", $wakeup_minutes)." minutes. THAT'S OKAY.\n"); |
| 165 | + } |
| 166 | + } |
| 167 | +} |
| 168 | +// Get date of newest folder (e.g. weekly.0, daily.0, monthly.0) |
| 169 | +// to figure out if the job needs to run |
| 170 | +$newest_backup_folder = "{$snapshot_root}{$jobname}.0"; |
| 171 | +if (!file_exists($newest_backup_folder)) { |
| 172 | + logft("No backup exists for job '$jobname' at '$newest_backup_folder'.\n"); |
| 173 | + $job_needs_to_run = true; |
| 174 | +} |
| 175 | +else { |
| 176 | + $backuptime = filemtime($newest_backup_folder); |
| 177 | + logft("Newest backup for '$jobname' at $newest_backup_folder was at ".date("d/M/Y, H:i:s", $backuptime).".\n"); |
| 178 | + if ($jobname == "daily") { |
| 179 | + $job_needs_to_run = time() - $backuptime > 23*60*60; |
| 180 | + $text_between_runs = sprintf("%.1f", (time() - $backuptime)/60/60)." hour(s)"; |
| 181 | + $text_min_time = "23 hours"; |
| 182 | + } |
| 183 | + else if ($jobname == "weekly") { |
| 184 | + $job_needs_to_run = time() - $backuptime > 6.5*24*60*60; |
| 185 | + $text_between_runs = sprintf("%.1f", (time() - $backuptime)/60/60/24)." day(s)"; |
| 186 | + $text_min_time = "6.5 days"; |
| 187 | + } |
| 188 | + else if ($jobname == "monthly") { |
| 189 | + $job_needs_to_run = time() - $backuptime > 29*24*60*60; |
| 190 | + $text_between_runs = sprintf("%.1f", (time() - $backuptime)/60/60/24)." day(s)"; |
| 191 | + $text_min_time = "29 days"; |
| 192 | + } |
| 193 | + else { |
| 194 | + logft("Error: This should not happen. ERROR.\n"); |
| 195 | + exit; |
| 196 | + } |
| 197 | + if (!$job_needs_to_run) { |
| 198 | + logft("Job does NOT need to run. Last run is only $text_between_runs ago (min. is $text_min_time). EXITING.\n"); |
| 199 | + logft("## BACKUP ABORTED #######################\n\n"); |
| 200 | + exit; |
| 201 | + } |
| 202 | + else { |
| 203 | + logft("Last run is $text_between_runs ago (min. is $text_min_time).\n"); |
| 204 | + } |
| 205 | +} |
| 206 | +logft("Writing rsnapshot-once pidfile (PID ".getmypid().") to ".$pidfile.".\n"); |
| 207 | +file_put_contents($pidfile, getmypid()); |
| 208 | +logft("NOW RUNNING JOB: "); |
| 209 | +array_shift($argv); |
| 210 | +$escaped_pidfile = addslashes($pidfile); |
| 211 | +$cmd = "rsnapshot '".join("' '", $argv)."' ".'2>&1'; |
| 212 | +logf("$cmd\n"); |
| 213 | +$exitcode = -1; |
| 214 | +$configerror = false; |
| 215 | +$output = array(); |
| 216 | +exec($cmd, $output, $exitcode); |
| 217 | +foreach ($output as $outline) { |
| 218 | + logft(" rsnapshot says: $outline\n"); |
| 219 | + |
| 220 | + if (preg_match("/rsnapshot encountered an error/", $outline)) { |
| 221 | + $configerror = true; |
| 222 | + } |
| 223 | +} |
| 224 | +if ($configerror) { |
| 225 | + logft("Exiting rsnapshot-once, because error in rsnapshot run detected.\n"); |
| 226 | + logft("Removing rsnapshot-once pidfile at ".$pidfile." (CLEAN EXIT).\n"); |
| 227 | + unlink($pidfile); |
| 228 | + |
| 229 | + logft("## BACKUP ABORTED #######################\n"); |
| 230 | + exit; |
| 231 | +} |
| 232 | +// pidfile should NOT exist if exit was clean |
| 233 | +if ($exitcode == 1) { // 1 means 'fatal error' in rsnapshot terminology |
| 234 | + logft("No clean exit. Backup aborted. Cleanup necessary on next run (DIRTY EXIT).\n"); |
| 235 | + logft("## BACKUP ABORTED #######################\n"); |
| 236 | + exit; |
| 237 | +} |
| 238 | +logft("Removing rsnapshot-once pidfile at ".$pidfile." (CLEAN EXIT).\n"); |
| 239 | +unlink($pidfile); |
| 240 | +logft("Rotating log ...\n"); |
| 241 | +logrotate(); |
| 242 | +logft("## BACKUP COMPLETE ######################\n\n"); |
| 243 | +#### FUNCTIONS ############################################################# |
| 244 | +// log with time |
| 245 | +function logft($s) { |
| 246 | + logf("[".date("d/M/Y:H:i:s")."/rsnapshot-once] ".$s); |
| 247 | +} |
| 248 | +// log without time |
| 249 | +function logf($s) { |
| 250 | + echo $s; |
| 251 | + |
| 252 | + if (isset($GLOBALS['logfile'])) { |
| 253 | + file_put_contents(trim($GLOBALS['logfile']), $s, FILE_APPEND | LOCK_EX); |
| 254 | + } |
| 255 | +} |
| 256 | +// rotate log |
| 257 | +function logrotate() { |
| 258 | + $logfile = $GLOBALS['logfile']; |
| 259 | + if (isset($GLOBALS['logfile'])) { |
| 260 | + `tail -n 1000 $logfile > $logfile.tmp`; |
| 261 | + `mv $logfile.tmp $logfile`; |
| 262 | + } |
| 263 | +} |
| 264 | +?> |
0 commit comments