#!/usr/bin/php-cli -d log_errors=Off -d display_errors=On
<?php
error_reporting(E_ALL);
/*
cpbackup-mango.php - run a cPanel scheduled full backup and then rsync it wherever the user wants.
Usage: cpbackup-mango.php [--debug] [user1 [user2 [user3 [user4]]]]
If usernames are not specified, the script will loop through directories in $home.
The user must have a file called cpbackup-scheduled.conf in their home directory. Here is an example of this file:
ssh_user=john_smith
ssh_host=example.com
ssh_directory=full_backups
max_backups=4
email=john_smith@example.com
Note that you do not specify a password in your config file. Instead, set up an RSA key for authentication.
The latest version of this script may be found at http://www.toao.net/
*/
// $admin_email - set this to your email address so that you may receive reports of errors.
$admin_email = 'user@example.com';
// $from_email - set this to whatever emails to users and admins will appear from.
$from_email = '"Mango\'s Scheduled Full Backup" ';
// $debug - use the --debug switch to print general information to the console. Note that if this is on, admins will NOT be emailed in the event of an error.
$debug = (in_array('--debug', $argv)) ? 1 : 0;
// $home - set this to the path to where the home directories are stored. This typically doesn't need to be changed.
$home = '/home';
// $backup_location - the location where the backup files will temporarily be stored. This folder should be owned by root with permissions 0700.
// NOTE: Anything named *.tar.gz in this folder will be deleted!
$backup_location = '/home/cpbackups-scheduled';
//////////////////////////
// And now for the script!
// Be sure $backup_location exists and has permissions 0700.
if (!is_dir($backup_location)) mkdir($backup_location, 0700, true); else chmod($backup_location, 0700);
// Delete any old backups left behind by accident.
better_exec("rm -f $backup_location/*.tar.gz");
// What users are we doing?
$users = array();
if (isset($argv[1])) {
// Users may have been specified on the command line.
for ($i = 1; $i < count($argv); $i++) if (is_dir("$home/{$argv[$i]}") or substr($argv[$i], 0, 1) != "-") $users[] = $argv[$i];
}
if (count($users) == 0) {
// Users were not specified on the command line - look for them in $home.
if (!$handle = opendir($home)) error("Unable to open $home. Backups will NOT continue.");
while (false !== ($user = readdir($handle))) if ($user != '..' and $user != '.' and is_dir("$home/$user")) $users[] = $user;
closedir($handle);
}
// Loop through the users in $home and find out which ones have scheduled full backup enabled.
foreach ($users as $user) {
if (!is_file("$home/$user/cpbackup-scheduled.conf")) {
debug("Checking $user ...scheduled full backup not enabled.");
continue;
}
// This user has enabled a scheduled full backup. How have they configured it?
debug("Scheduled full backup enabled for user $user!");
$config = array();
$raw_config = preg_split("/\r\n|\r|\n/", file_get_contents("$home/$user/cpbackup-scheduled.conf"));
foreach ($raw_config as $option) if (strpos($option, '=') !== false) {
list($key, $value) = explode('=', $option, 2);
$config[$key] = $value;
if (preg_match("/[^\w@\-~.]/", $value)) {
error_user("Illegal character found in cpbackup-scheduled.conf file. The only characters allowed are: letters, numbers, _ (underscore), @ (at sign), - (dash), ~ (tilde), and . (period).");
continue 2;
}
}
// Make sure required configuration exists.
if ((!isset($config['ssh_user']) or strlen($config['ssh_user']) == 0) or (!isset($config['ssh_host']) or strlen($config['ssh_host']) == 0)) {
error_user("ssh_user and/or ssh_host is not defined in cpbackup-scheduled.conf file. Backup for account $user will not be run this time.");
continue;
}
if (!isset($config['ssh_directory'])) $config['ssh_directory'] = ''; else rtrim($config['ssh_directory'], '/');
debug("Config:\n" . print_r($config,1));
// Run the full backup.
list($output, $error, $return) = better_exec("/scripts/pkgacct $user $backup_location backup");
if ($error or $return != 0) {
// Error running the backup.
warn("cPanel reported a problem creating the full backup for $user. The script will try to continue, but the backup may or may not have completed correctly.\nOutput: $output\nError: $error\nReturn value: $return");
}
// Rename the file.
preg_match("/(?<=pkgacctfile is: ).*/", $output, $matches);
if (!isset($matches[0]) or strpos($matches[0], '.tar.gz') === false) {
warn("Created a backup for $user, but unable to find out the name of the backup file. This backup failed, but we will try to continue any remaining ones, if they exist.\n\n$output");
continue;
}
$backup_filename = "$user-backup-" . date('YmMd-H.i.s') . ".tar.gz";
$backup_path_file = "$backup_location/$backup_filename";
rename($matches[0], $backup_path_file);
debug("Backup file was: {$matches[0]} and has been renamed to $backup_path_file.");
// Make the directory. If it already exists, mkdir will quit silently because of the -p switch.
if ($config['ssh_directory']) better_exec("ssh -i $home/$user/.ssh/id_rsa -o 'BatchMode yes' {$config['ssh_user']}@{$config['ssh_host']} mkdir -p {$config['ssh_directory']}", 1);
// rsync the file to $config['ssh_host'].
list($output, $error, $return) = better_exec("rsync -e \"ssh -i $home/$user/.ssh/id_rsa\" $backup_path_file {$config['ssh_user']}@{$config['ssh_host']}:{$config['ssh_directory']}");
if($error or $return != 0) {
error_user("The backup file for $user could not be sent to the remote server. The error will be shown below.\n\n$error");
continue;
}
// Delete the file.
if (!@unlink($backup_path_file)) warn("Unable to delete $backup_path_file.");
// Delete old backups, if the user would like that. First, get a listing of files in the directory we will be working with.
if (isset($config['max_backups']) and $config['max_backups'] > 0) {
list($files, $error, $return) = better_exec("ssh -i $home/$user/.ssh/id_rsa -o 'BatchMode yes' {$config['ssh_user']}@{$config['ssh_host']} ls -1t {$config['ssh_directory']}/*-backup-*.tar.gz");
if($error or $return != 0) {
error_user("Unable to get directory listing for {$config['ssh_directory']}/*-backup-*.tar.gz. Cannot delete $user's old backups.");
continue;
}
$files = preg_split("/\r\n|\r|\n/", trim($files));
if (count($files) > $config['max_backups']) {
debug("Listing of files: " . print_r($files, true));
// Delete oldest files if there are more than max_backups files.
$files_to_delete = '';
for ($i = count($files)-1; $i > $config['max_backups']-1; $i=$i-1) if (preg_match("/\w/", $files[$i])) $files_to_delete .= "'{$files[$i]}' ";
if ($files_to_delete) {
debug("Files to delete: $files_to_delete");
better_exec("ssh -i $home/$user/.ssh/id_rsa -o 'BatchMode yes' {$config['ssh_user']}@{$config['ssh_host']} \"rm $files_to_delete\"", 1);
}
} else {
debug ('No files to delete');
}
}
}
function error_user($message) {
debug("USER ERROR: $message");
// Log the error to a file.
$fp = fopen("{$GLOBALS['home']}/{$GLOBALS['user']}/cpbackup-scheduled-error.log", 'a');
fwrite($fp, date('[Y-M-d H:i:s]') . ' ' . trim($message) . "\n");
fclose($fp);
// Be sure the error log is owned by the user, and the user has permissions to open it.
chown("{$GLOBALS['home']}/{$GLOBALS['user']}/cpbackup-scheduled-error.log", $GLOBALS['user']);
chmod("{$GLOBALS['home']}/{$GLOBALS['user']}/cpbackup-scheduled-error.log", 0700);
// Email the error to the user, if they have requested it.
if (isset($GLOBALS['config']['email'])) {
mail($GLOBALS['config']['email'], "Scheduled Full Backup: Error","The following error was encountered while trying to perform the backup:\n\n$message", "From: {$GLOBALS['from_email']}");
}
}
function warn($message) {
error($message, 'WARNING');
}
function error($message, $level='ERROR') {
if ($GLOBALS['debug']) {
// Do not email if debug is turned on; output to screen.
debug("$level: $message");
if ($level=='ERROR') exit; else return;
}
mail($GLOBALS['admin_email'], "Scheduled Full Backup: $level","The following problem was encountered while trying to perform the backup:\n\n$message", "From: {$GLOBALS['from_email']}");
if ($level=='ERROR') exit;
}
function debug($message) {
if ($GLOBALS['debug']) echo "DEBUG: " . trim($message) . "\n";
}
function better_exec($command) {
// Execute $command and return the output, error text, and return value.
debug($command);
$process = proc_open($command, array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => array("pipe", "w")), $pipes);
if (!is_resource($process)) error("Unable to run command:\n$command");
fclose($pipes[0]);
$output = stream_get_contents($pipes[1]);
$error = trim(stream_get_contents($pipes[2]));
fclose($pipes[1]); fclose($pipes[2]);
$return = proc_close($process);
return array($output, $error, $return);
}
?>