Last active
April 5, 2017 21:08
-
-
Save anyheck/b5f53e5f12df1e988c02d9cbf369d9f0 to your computer and use it in GitHub Desktop.
ZFS Backup
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Number of DAYS to keep the daily backups | |
keep-daily 7 | |
# Number of DAYS to keep the weekly backups -- note this is NOT the | |
# number of weeks to keep them | |
keep-weekly 42 | |
# The three letter abbreviation of the day to consider a weekly backup | |
weekly-backup-day Sun | |
default-protocol rsh | |
# Show a status line when a backup starts, set to yes to enable | |
show-start-line no | |
# The path to the find command on remote the remote hosts | |
# (used for globbing paths) | |
find-path /usr/bin/find | |
# The path to rsync on remote the remote hosts | |
rsync-path /usr/local/bin/rsync | |
# Whether rsync should use compression or not (valid: 'yes' or 'no') | |
compression yes | |
# Each protocol must be defined with the path to it's binary | |
#protcol-path name=/path/to/program | |
protocol-path rsh=/usr/bin/rsh | |
protocol-path ssh=/usr/bin/ssh | |
#debug yes # comment out this line to disable debugging | |
# | |
# Sample host entry: | |
# | |
# host hostname.domain { | |
# | |
# # The thread-id assigns this host to a specific thread. Each thread runs | |
# # in parallel, but each host in the thread is backed up serially. This | |
# # allows long running rsync's to be separate from other hosts and allows | |
# # utilizing the bandwidth capabilities for separate WAN links at the same | |
# # time. | |
# thread-id any-thread-id | |
# | |
# # The protocol statement defines the protocol to use for this host The | |
# # protocol must be defined using the protocol-path global statement. If | |
# # protocol is omitted, the global default-protocol option is used. | |
# protocol rsh | |
# | |
# # The path statement defines a path to be backed up from the host | |
# path /path/to/dir | |
# | |
# # Using a '*' at the end of the path will have the backup program used | |
# # the defined (or default) protocol to connect to the host and get a list | |
# # of directories based on the glob. This is useful for directories that | |
# # contain very large numbers of files, which causes rsync to use more | |
# # system resources. | |
# # The command used to glob is: | |
# # find /some/other/path/* -type d -maxdepth 0 | |
# # | |
# # Note that this will not include hidden directories or any files in the | |
# # specified path | |
# path /some/other/path/* | |
# | |
# # This format for the path statement allows you to relocate a remote path | |
# # to another location on the backup server. This is useful for backing | |
# # up ZFS snapshots of a filesystem or other preporatory directories | |
# # created by a pre-script | |
# path /remote/path/=/local/path | |
# path /path/.zfs/snapshot/snapname=/path | |
# | |
# # The exclude statement defines an exclude regular expression. The | |
# # pattern excluded is excluded from ALL paths defined for host. | |
# # This path is relative to ALL paths specified with the "path" | |
# # configuration. For example, if you define: | |
# # | |
# # path /foo | |
# # exclude /bar | |
# # | |
# # This will recursively exclude /foo/bar from the backup. | |
# exclude /exclude-dir | |
# | |
# } | |
############################################################################# | |
# home directories go into it's own thread because it takes forever | |
host homedirnfs.mydomain.com { | |
thread-id homedirs | |
path /home/* | |
} | |
############################################################################# | |
# Remote Site 1 | |
host svn.mydomain.com { | |
thread-id remote-site-1 | |
path /svnroot-backup=/svnroot | |
} | |
host devserver.mydomain.com { | |
thread-id remote-site-1 | |
path /http | |
path /usr/local/etc | |
exclude /static | |
exclude /backup* | |
} | |
host nameserver1.mydomain.com { | |
thread-id remote-site-1 | |
path /usr/local/etc | |
} | |
host nameserver2.mydomain.com { | |
thread-id remote-site-1 | |
path /usr/local/etc | |
} | |
host ftpserver.mydomain.com { | |
thread-id remote-site-1 | |
path /usr/local/etc | |
path /ftp/* | |
} | |
############################################################################# | |
# Remote Site 2 | |
host mysql.mydomain.com { | |
thread-id remote-site-2 | |
path /usr/local/etc | |
path /mysql/.zfs/snapshot/backup=/mysql | |
pre-script /usr/local/bin/snapshot-db tank/mysql@backup | |
exclude /master-log.* | |
force-checksum yes # this is HIGHLY recommended for MySQL backups | |
} | |
host server1.mydomain.com { | |
thread-id remote-site-2 | |
path /usr/local/etc | |
} | |
host server2.mydomain.com { | |
thread-id remote-site-2 | |
path /usr/local/etc | |
} | |
host server3.mydomain.com { | |
thread-id remote-site-2 | |
path /usr/local/etc | |
} | |
host solrlucene.mydomain.com { | |
thread-id remote-site-2 | |
path /usr/local/etc | |
path /solr | |
exclude /*/solr/data/* | |
exclude /*/solr/logs/* | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/local/bin/php | |
<? | |
// :set cindent et sts=4 sw=4 | |
/* | |
* ypass-backup: Backup script for Solaris 10 with ZFS and rsync | |
* | |
* Copyright (c) 2009 Eric Kilfoil ([email protected]) | |
* http://www.ypass.net/solaris/zfsbackup/ | |
* | |
* This program is free software: you can redistribute it and/or modify | |
* it under the terms of the GNU General Public License as published by | |
* the Free Software Foundation, either version 3 of the License, or | |
* (at your option) any later version. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU General Public License for more details. | |
* | |
* You should have received a copy of the GNU General Public License | |
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |
*/ | |
define(CONFIG_FILE, "/usr/local/etc/backup.conf"); | |
// This is the name of the zfs filesystem to snapshot | |
// This should move to the config file | |
define(BACKUP_ZFS_NAME, "tank/backup"); | |
// This is the mountpoint for the above zfs dataset | |
// This should move to the config file | |
define(BACKUP_DIR, "/backup/"); | |
define(VERSION, '1.0'); | |
$config = array(); | |
$thread_id_list = array(); | |
$debug = FALSE; | |
$compression = FALSE; | |
$opt_debug = $opt_thread = $opt_host = $opt_path = NULL; | |
function display_status($step) | |
{ | |
$dots = ".............................................................................."; | |
echo substr($step . $dots, 0, 65) . " "; | |
} | |
function display_result($resultcode, $resulttext = NULL)/*{{{*/ | |
{ | |
if ($resultcode) { | |
echo ($resulttext === NULL ? "done" : $resulttext) . "\n"; | |
} else { | |
echo ($resulttext === NULL ? "failed" : $resulttext) . "\n"; | |
} | |
} | |
function status_line($line) { | |
echo date("H:i:s") . " $line\n"; | |
} | |
// Remove any double slashes from the path | |
function normalize_path($path) { | |
while (preg_match('%//%', $path)) { | |
$path = preg_replace('%//%', '/', $path); | |
} | |
return($path); | |
} | |
// Read in the configuration file and populate the global $config array | |
function read_config($config_file) { | |
global $config, $thread_id_list; | |
if (! file_exists($config_file)) { | |
echo "error: config file not found at $config_file\n"; | |
echo "edit this script and set the location of CONFIG_FILE at the top\n"; | |
exit(1); | |
} | |
// normalize the path and remove any trailing slashes | |
$config['backup-dir'] = preg_replace('%/$%', '', normalize_path(BACKUP_DIR)); | |
$cfglines = file($config_file); | |
$inhostblock = FALSE; | |
$linectr = 0; | |
for ($ctr = 0; $ctr < count($cfglines); $ctr++) { | |
$l = $cfglines[$ctr]; | |
$linectr++; | |
// skip blank lines and comments | |
if (trim($l) == "") { | |
continue; | |
} | |
if (preg_match('/^\s*#.*$/', $l)) { | |
continue; | |
} | |
// make sure we're in the global context and this line isn't a | |
// hostblock definition | |
if (!$inhostblock && ! preg_match('/^\s*host\s+([^{]+){\s+$/', $l, $regs)) { | |
// parse a global config option | |
if (preg_match('/^\s*(\S+)\s+([^#]+)/', $l, $regs)) { | |
$option = trim($regs[1]); | |
$value = trim($regs[2]); | |
switch ($option) { | |
case 'default-protocol': | |
$config['default-protocol'] = $value; | |
break; | |
case 'keep-daily': | |
$config['keep-daily'] = $value; | |
break; | |
case 'keep-weekly': | |
$config['keep-weekly'] = $value; | |
break; | |
case 'weekly-backup-day': | |
$config['weekly-backup-day'] = $value; | |
break; | |
case 'show-start-line': | |
$config['show-start-line'] = $value; | |
break; | |
case 'find-path': | |
$config['find-path'] = $value; | |
break; | |
case 'rsync-path': | |
$config['rsync-path'] = $value; | |
break; | |
case 'compression': | |
if ($value != 'yes' && $value != 'no') { | |
display_result(FALSE); | |
echo "error: valid options for compression are 'yes' and 'no' (line $linectr)\n"; | |
exit(1); | |
} | |
$config['compression'] = $value; | |
$GLOBALS['compression'] = TRUE; | |
break; | |
case 'debug': | |
$config['debug'] = 'yes'; | |
$GLOBALS['debug'] = TRUE; | |
break; | |
case 'protocol-path': | |
if (! preg_match('/(.+)=(.+)/', $value, $regs)) { | |
display_result(FALSE); | |
echo "error: bad protocol-path format: name=path (ie. rsh=/usr/bin/rsh) (line $linectr)\n"; | |
exit(1); | |
} | |
$config['protocols'][$regs[1]]['path'] = trim($regs[2]); | |
break; | |
default: | |
display_result(FALSE); | |
echo "error: unknown global option '" . trim($regs[2]) . "' on line $linectr\n"; | |
exit(1); | |
break; | |
} | |
} else { | |
display_result(FALSE); | |
echo "error: expected host block start\n"; | |
echo "error parsing line $linectr: $l"; | |
exit(1); | |
} | |
} else if ($inhostblock && preg_match('/^\s*host\s+([^{]+){\s+$/', $l, $regs)) { | |
// not able to have a host def inside of another host | |
// definition. Probably forgot a closing brace. | |
display_result(FALSE); | |
echo "error: previous hostblock not ended at line $linectr: $l"; | |
exit(1); | |
} else if (preg_match('/^\s*host\s+([^{]+){\s+$/', $l, $regs)) { | |
// start a new host block | |
$inhostblock = TRUE; | |
$hostname = trim($regs[1]); | |
} else if (preg_match('/^\s*}\s+/', $l)) { | |
// end a new host block | |
$inhostblock = FALSE; | |
$hostname = ''; | |
} else { | |
// we're inside of a hostblock, make sure the syntax is correct | |
// and set host options | |
if (preg_match('/^\s*(\S+)\s+([^#]+)/', $l, $regs)) { | |
$option = trim($regs[1]); | |
$value = trim($regs[2]); | |
switch ($option) { | |
case 'thread-id': | |
if ($config['hosts'][$hostname]['thread-id'] != "") { | |
display_result(FALSE); | |
echo "error: attempting to reset thread-id to '$value' on line $linectr\n"; | |
exit(0); | |
} | |
$config['hosts'][$hostname]['thread-id'] = $value; | |
if (! array_key_exists($value, $thread_id_list)) { | |
$thread_id_list[$value] = 0; | |
} | |
break; | |
case 'protocol': | |
if ($config['hosts'][$hostname]['protocol'] != "") { | |
display_result(FALSE); | |
echo "error: attempting to reset protocol to '$value' on line $linectr\n"; | |
exit(0); | |
} | |
$config['hosts'][$hostname]['protocol'] = $value; | |
break; | |
case 'force-checksum': | |
if ($config['hosts'][$hostname]['force-checksum'] != "") { | |
display_result(FALSE); | |
echo "error: attempting to specify force-checksum twice on line $linectr\n"; | |
exit(0); | |
} | |
if ($value != "yes" && $value != "no") { | |
display_result(FALSE); | |
echo "error: invalid value for force-checksum on line $linectr\n"; | |
exit(0); | |
} | |
$config['hosts'][$hostname]['force-checksum'] = $value; | |
break; | |
case 'path': | |
if (is_array($config['hosts'][$hostname]['path']) && in_array($value, $config['hosts'][$hostname]['path'])) { | |
display_result(FALSE); | |
echo "error: path '$value' listed twice on line $linectr\n"; | |
exit(0); | |
} | |
$config['hosts'][$hostname]['path'][] = $value; | |
break; | |
case 'exclude': | |
if (is_array($config['hosts'][$hostname]['exclude']) && in_array($value, $config['hosts'][$hostname]['exclude'])) { | |
display_result(FALSE); | |
echo "error: exclude path '$value' listed twice on line $linectr\n"; | |
exit(0); | |
} | |
$config['hosts'][$hostname]['exclude'][] = $value; | |
break; | |
case 'pre-script': | |
$config['hosts'][$hostname]['pre-script'][] = $value; | |
break; | |
case 'post-script': | |
$config['hosts'][$hostname]['post-script'][] = $value; | |
break; | |
default: | |
display_result(FALSE); | |
echo "error: unknown option on $linectr: $l"; | |
exit(1); | |
break; | |
} | |
} else { | |
display_result(FALSE); | |
echo "error: cannot parse line $linectr: $l"; | |
exit(1); | |
break; | |
} | |
} | |
} | |
} | |
// this function goes through the config array and makes sure all of the | |
// required global variables are set in the array, performs a sanity check | |
// on the host configuration and globs any paths specified in path | |
// configuration statements | |
function check_config() { | |
global $config; | |
if (! isset($config['keep-daily'])) { | |
echo "warning: keep-daily not defined in the configuration file\n"; | |
echo "defaulting keep-daily to 7\n"; | |
$config['keep-daily'] = 7; | |
} | |
if (! isset($config['keep-weekly'])) { | |
echo "warning: keep-weekly not defined in the configuration file\n"; | |
echo "defaulting keep-weekly to 42\n"; | |
$config['keep-weekly'] = 42; | |
} | |
if (! isset($config['weekly-backup-day'])) { | |
echo "warning: weekly-backup-day not defined in the configuration file\n"; | |
echo "defaulting weekly-backup-day to Sun\n"; | |
$config['weekly-backup-day'] = 'Sun'; | |
} | |
if (! isset($config['find-path'])) { | |
echo "warning: find-path not defined in the configuration file\n"; | |
echo "defaulting find-path to /usr/local/bin/find\n"; | |
$config['find-path'] = '/usr/bin/find'; | |
} | |
if (! isset($config['rsync-path'])) { | |
echo "warning: rsync-path not defined in the configuration file\n"; | |
echo "defaulting rsync-path to /usr/local/bin/rsync\n"; | |
$config['rsync-path'] = '/usr/local/bin/rsync'; | |
} | |
if (! isset($config['default-protocol'])) { | |
display_result(FALSE); | |
echo "error: default-protocol not defined in the configuration file\n"; | |
exit(1); | |
} | |
foreach ($config['hosts'] as $hostname => $host) { | |
if (! isset($host['protocol'])) { | |
$config['hosts'][$hostname]['protocol'] = $config['default-protocol']; | |
} | |
if (! is_array($host['path'])) { | |
display_result(FALSE); | |
echo "error: $hostname has no backup paths defined\n"; | |
exit(1); | |
} | |
if (! is_array($config['protocols'][$config['hosts'][$hostname]['protocol']])) { | |
display_result(FALSE); | |
echo "error: $hostname has unknown protocol '" . $host['protocol'] . "'\n"; | |
echo "All protocols must be defined with the global protocol-path statement\n"; | |
exit(1); | |
} | |
foreach ($host['path'] as $i => $path) { | |
if ($path[strlen($path)-1] == '*') { | |
$command = $config['protocols'][$config['hosts'][$hostname]['protocol']]['path'] . " " . $hostname . " '" . $config['find-path'] . " $path -type d -maxdepth 0'"; | |
$output = array(); | |
exec($command, $output, $ret); | |
if ($ret != 0) { | |
display_result(FALSE); | |
echo "error: could not glob path '$path' for host $hostname\n"; | |
exit(1); | |
} | |
$path[strlen($path)-1] = ''; | |
foreach ($output as $subdir) { | |
$config['hosts'][$hostname]['path'][] = $subdir; | |
} | |
unset($config['hosts'][$hostname]['path'][$i]); | |
} | |
} | |
} | |
} | |
// This function actually performs the backup for a specific thread. This | |
// is called by a child after is forked off to handle the thread. | |
function run_thread($id) { | |
global $config, $debug, $opt_host, $opt_path; | |
foreach ($config['hosts'] as $hostname => $host) { | |
if ($host['thread-id'] != $id) { | |
// skip hosts not in the specified thread | |
continue; | |
} | |
if ($opt_host && $opt_host != $hostname) { | |
continue; | |
} | |
if ($host['force-checksum'] == 'yes') { | |
$checksum = TRUE; | |
} else { | |
$checksum = FALSE; | |
} | |
// loop through each path, build an rsync command, and execute | |
foreach ($host['path'] as $path) { | |
if (preg_match('/^(.*)=(.*)$/', $path, $regs)) { | |
$remotepath = $regs[1]; | |
$localpath = $regs[2]; | |
} else { | |
$remotepath = $path; | |
$localpath = $path; | |
} | |
if ($opt_path && $opt_path != $remotepath) { | |
continue; | |
} | |
if ( $config['show-start-line'] == 'yes' || | |
$config['show-start-line'] == 'y' || | |
$config['show-start-line'] == 'on') { | |
status_line("[$id] Starting backup of $hostname:$remotepath"); | |
} | |
$rsync_path = $config['rsync-path']; | |
$protocol_path = $config['protocols'][$host['protocol']]['path']; | |
$excludeparams=''; | |
// create a --exclude param for each excluded path | |
if (is_array($host['exclude'])) { | |
foreach ($host['exclude'] as $ex) { | |
$excludeparams="$excludeparams --exclude=$ex"; | |
} | |
} | |
// if the path we're backing up to does exist, create it | |
// locally (and recursively) | |
if (! file_exists("/" . $config['backup-dir'] . "/$hostname/$localpath")) { | |
mkdir("/" . $config['backup-dir'] . "/$hostname/$localpath", 0755, TRUE); | |
} | |
$starttime = time(); | |
// if there's a pre-script for the host, run it | |
if (is_array($host['pre-script'])) { | |
$scriptfailed = FALSE; | |
status_line("[$id] Executing pre-scripts on $hostname"); | |
foreach ($host['pre-script'] as $scriptname) { | |
$command = $config['protocols'][$config['hosts'][$hostname]['protocol']]['path'] . " " . $hostname . " '$scriptname'"; | |
$output = array(); | |
if ($debug) { | |
echo $command . "\n"; | |
} else { | |
exec("truss -f -o /tmp/e " . $command, $output, $ret); | |
} | |
if ($ret != 0) { | |
echo "error: pre-script failed execution -- skipping backup\n"; | |
echo "---------[ Error Output ]--------------------------------------\n"; | |
echo implode("\n", $output) . "\n\n"; | |
$scriptfailed = TRUE; | |
break; | |
} | |
} | |
if ($scriptfailed) { | |
continue; | |
} | |
} | |
// put our actual rsync command together | |
$command = "$rsync_path -av " . ($checksum ? " -c " : "") . " " . ($config['compression'] ? " -z " : "") . " --rsync-path=$rsync_path --rsh=$protocol_path --exclude=/lost+found $excludeparams --delete $hostname:$remotepath/ /" . $config['backup-dir'] . "/$hostname/$localpath/ 2>&1"; | |
if ($debug) { | |
echo $command . "\n"; | |
} else { | |
$output=''; | |
exec($command, $output, $ret); | |
$endtime = time(); | |
$runtime = sprintf("%2.2lf min", ($endtime - $starttime) / 60); | |
$speedline = $output[count($output)-2]; | |
$sizeline = $output[count($output)-1]; | |
// when the command finishes, parse the output of rsync and | |
// build a string for the status line | |
if (preg_match('/^.*sent\s+(\d+)\s+bytes\s+received\s+(\d+)\s+bytes\s+(\d+).*$/', $speedline, $regs)) { | |
$xmitbytes = $regs[2]; | |
if ($xmitbytes > 1024*1024*1024) { | |
$xmitbytes = sprintf("%2.2lfGB", ($xmitbytes/1024/1024/1024)); | |
} else if ($xmitbytes > 1024*1024) { | |
$xmitbytes = sprintf("%2.2lfMB", ($xmitbytes/1024/1024)); | |
} else if ($xmitbytes > 1024) { | |
$xmitbytes = sprintf("%2.2lfKB", ($xmitbytes/1024)); | |
} else { | |
$xmitbytes = sprintf("%dB", ($xmitbytes)); | |
} | |
$speed = sprintf("%2.2lfKbps", $regs[3] / 1024 * 8); | |
} else { | |
$speed = "unknown"; | |
$xmitbytes = "unknown"; | |
} | |
if (preg_match('/^.*total size is (\d+)\s+speedup.*$/', $sizeline, $regs)) { | |
$sizebytes = $regs[1]; | |
if ($sizebytes > 1024*1024*1024) { | |
$size = sprintf("%2.2lfGB", ($sizebytes/1024/1024/1024)); | |
} else if ($sizebytes > 1024*1024) { | |
$size = sprintf("%2.2lfMB", ($sizebytes/1024/1024)); | |
} else if ($sizebytes > 1024) { | |
$size = sprintf("%2.2lfKB", ($sizebytes/1024)); | |
} else { | |
$size = $sizebytes; | |
} | |
} else { | |
$size = "unknown"; | |
} | |
if ($ret != 0) { | |
// if the rsync failed, just dump all of the output | |
status_line("[$id] $hostname:$remotepath failed:"); | |
echo "---------[ Error Output ]--------------------------------------\n"; | |
echo implode("\n", $output) . "\n\n"; | |
} else { | |
// touch all of the paths down the line so we can see | |
// when something was last backed up in ls output | |
$e = 0; | |
$rpath = $config['backup-dir'] . "/$hostname/$localpath/"; | |
do { | |
touch($rpath); | |
} while (($rpath = dirname($rpath)) != $config['backup-dir']); | |
status_line("[$id] $hostname:$remotepath: $size, xfer: $xmitbytes, $speed, $runtime"); | |
// run any post backup scripts on the remote host | |
if (is_array($host['post-script'])) { | |
status_line("[$id] Executing post-scripts on $hostname"); | |
foreach ($host['post-script'] as $scriptname) { | |
$command = $config['protocols'][$config['hosts'][$hostname]['protocol']]['path'] . " " . $hostname . " '$scriptname'"; | |
$output = array(); | |
if ($debug) { | |
echo $command . "\n"; | |
} else { | |
exec($command, $output, $ret); | |
} | |
if ($ret != 0) { | |
echo "error: post-script failed execution\n"; | |
echo "---------[ Error Output ]--------------------------------------\n"; | |
echo implode("\n", $output) . "\n\n"; | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
// remove zfs snapshots older than the specified limits | |
function zfs_cleanup() { | |
global $config; | |
$bnpath = basename(BACKUP_DIR); | |
exec('/usr/sbin/zfs list | grep ' . $bnpath . '@ | awk \'{ print $1 }\'', $snaplist, $ret); | |
if (! is_array($snaplist)) { | |
echo "warning: no snapshots exist\n\n"; | |
return(0); | |
} | |
foreach ($snaplist as $snap) { | |
if (!preg_match('/^.*@(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})$/', $snap, $regs)) { | |
echo "warning: Ignoring unknown snapshot format on $snap\n"; | |
} | |
$snaptime = mktime($regs[4], $regs[5], $regs[6], $regs[2], $regs[3], $regs[1]); | |
$snapage = time() - $snaptime; | |
$snapday = date("D", $snaptime); | |
if ($snapday == $config['weekly-backup-day']) { | |
if ($snapage >= $config['keep-weekly'] * 86400) { | |
status_line("$snap: removing - older than " . $config['keep-weekly'] . ' days'); | |
`/usr/sbin/zfs destroy $snap`; | |
} else { | |
status_line("$snap: keeping - newer than " . $config['keep-weekly'] . ' days'); | |
} | |
} else { | |
if ($snapage >= $config['keep-daily'] * 86400) { | |
status_line("$snap: removing - older than " . $config['keep-daily'] . ' days'); | |
`/usr/sbin/zfs destroy $snap`; | |
} else { | |
status_line("$snap: keeping - newer than " . $config['keep-daily'] . ' days'); | |
} | |
} | |
} | |
} | |
// create a zfs snapshot with a timestamp | |
function zfs_snapshot() { | |
global $config, $debug; | |
$snapname = BACKUP_ZFS_NAME . '@' . date("Ymd-His"); | |
$output = array(); | |
$command = '/usr/sbin/zfs snapshot ' . $snapname . " 2>&1"; | |
if ($debug) { | |
echo $command . "\n"; | |
} else { | |
exec($command, $output, $ret); | |
} | |
if ($ret != 0) { | |
echo "error: couldn't snapshot $snapname:\n"; | |
echo implode("\n", $output) . "\n\n"; | |
} | |
status_line("Created snapshot: $snapname"); | |
} | |
// display the zfs filesystems and snapshots | |
function zfs_info() { | |
global $config; | |
$output = array(); | |
exec('/usr/sbin/zfs list -r ' . BACKUP_ZFS_NAME, $output, $ret); | |
echo implode("\n", $output); | |
echo "\n"; | |
} | |
// parse command line options | |
function parse_options() { | |
global $config, $debug, $opt_thread, $opt_host, $opt_path; | |
$longopts = array( | |
'thread:', | |
'host:', | |
'path:', | |
'debug', | |
'help', | |
'version', | |
); | |
if (version_compare(phpversion(), "5.3.0") >= 0) { | |
$optlist = getopt("dvt:h:p:", $longopts); | |
} else { | |
$optlist = getopt("dvt:h:p:"); | |
} | |
foreach ($optlist as $optind => $optarg) { | |
switch ($optind) { | |
case 'debug': | |
case 'd': | |
$config['debug'] = TRUE; | |
$debug = TRUE; | |
break; | |
case 'thread': | |
case 't': | |
$opt_thread = $optarg; | |
break; | |
case 'host': | |
case 'h': | |
$opt_host = $optarg; | |
break; | |
case 'version': | |
case 'v': | |
case 'help': | |
echo "ypass-backup v" . VERSION . " Copyright (C) 2009 Eric Kilfoil <[email protected]>\n" . | |
"\n" . | |
"This program comes with ABSOLUTELY NO WARRANTY; This is free software, and you\n" . | |
"are welcome to redistribute it under certain conditions.\n" . | |
"For more information, visit http://www.ypass.net/solaris/zfsbackup/license.html\n" . | |
"\n" . | |
"Valid options:\n" . | |
"-t, --thread Specify a thread id to back up\n" . | |
"-h, --host Specify a hostname from the config file\n" . | |
"-p, --path Specify a pathname from the config file. If you\n" . | |
" specify a pathname, you must specify a host as well.\n" . | |
"-d, --debug Enable debugging\n"; | |
exit(0); | |
case 'path': | |
case 'p': | |
$opt_path = $optarg; | |
break; | |
default: | |
echo "error: unknown option '$optind'\n"; | |
exit(1); | |
break; | |
} | |
} | |
if (strlen($opt_host) && strlen($opt_thread)) { | |
echo "error: you cannot specify a host and a thread at the same time\n"; | |
exit(1); | |
} | |
if (strlen($opt_path) && ! strlen($opt_host)) { | |
echo "error: you cannot specify a path without selecting a host\n"; | |
exit(1); | |
} | |
} | |
// main code execution start | |
parse_options(); | |
echo "---------[ Configuration File Processing ]---------------------------\n"; | |
display_status("Reading configuration file"); | |
read_config(CONFIG_FILE); | |
display_result(TRUE); | |
display_status("Validating configuration"); | |
check_config(); | |
display_result(TRUE); | |
echo "\n---------[ Filesystem Cleanup ]------------------------------------\n"; | |
zfs_cleanup(); | |
echo "\n---------[ Initializing Threads ]------------------------------------\n"; | |
display_status("Checking number of threads"); | |
if ($opt_thread) { | |
$thread_id_list = array($opt_thread => 0); | |
} | |
if ($opt_host) { | |
$thread_id_list = array($config['hosts'][$opt_host]['thread-id'] => 0); | |
} | |
display_result(TRUE, count($thread_id_list)); | |
display_status("Checking for pcntl extension"); | |
if (! extension_loaded('pcntl')) { | |
if (! dl('pcntl.so')) { | |
display_result(FALSE); | |
echo "error: could not load pcntl.so"; | |
exit(1); | |
} | |
display_result(TRUE, 'loaded'); | |
} else{ | |
display_result(TRUE, 'done'); | |
} | |
echo "\n---------[ Starting Backups ]------------------------------------\n"; | |
// fork a child for every thread-id and start run running | |
foreach ($thread_id_list as $id => $pid) { | |
$pid = pcntl_fork(); | |
if ($pid == -1 ) { | |
echo "error: could not fork -- aborting\n"; | |
exit(1); | |
} else if ($pid) { | |
// parent (master) process | |
$thread_id_list[$id] = $pid; | |
// sleep for a second or the children clutter up the output | |
continue; | |
} else { | |
// child | |
display_status("Starting thread-id $id"); | |
display_result(TRUE, "pid " . posix_getpid()); | |
run_thread($id); | |
exit(0); | |
} | |
} | |
// master process comes here after creating children. children never get | |
// to this code. | |
// wait on each child and print a status line when each child finishes | |
for ($ctr = count($thread_id_list); $ctr > 0; $ctr--) { | |
$pid = pcntl_waitpid(0, $status); | |
$id = array_search($pid, $thread_id_list); | |
status_line(" Thread [$id] completed - " . ($ctr-1) . " still running"); | |
} | |
echo "\n---------[ Creating Snapshot ]------------------------------------\n"; | |
// only create a snapshot if we did a full backup | |
if (! $opt_host && ! $opt_path && ! $opt_thread) { | |
zfs_snapshot(); | |
} else { | |
status_line("not creating snapshot for partial backup"); | |
} | |
echo "\n---------[ Filesystem Info ]------------------------------------\n"; | |
zfs_info(); | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment