#!/usr/bin/php -f = 5.0.0 avec support du CLI // et de MySQL (NON compatible avec PHP 4.x) avec un accès au serveur MySQL // - Serveur MySQL toutes versions // ## LIMITATIONS ## // // - Si rsyncd est utilisé pour la synchronisation, le script n'est utilisable // que sur la machine initiant la connexion. // ## INSTALLATION, CONFIGURATION ET UTILISATION ## // // - Placez le script quelque part sur l'expéditeur ou le destinataire (de // préférence celui qui est le plus "proche" du serveur MySQL - c'est à dire // l'expéditeur dans 99% des cas). // // - Vérifiez que la première ligne du script (#!/usr/bin/php -f par défaut) // mène bien à votre éxécutable PHP. // // - Paramétrez les droits d'éxécution du script de telle sorte que // l'utilisateur éxécutant rsync sur la machine aie le droit d'éxécuter le // script. // // - Renseignez les champs de configuration plus bas dans le script. Il est // recommandé de créer un utilisateur MySQL dédié au script ; dans tous les // cas, l'utilisateur MySQL spécifié doit avoir les droits globaux suivants : // // SELECT, RELOAD, SHOW DATABASES, LOCK TABLES // // Utiliser l'utilisateur root ici est une douce folie. // // 1. Cas où le script est éxécuté sur la machine initiant la connexion // (OU en local) // // Il s'agit du cas où le script est éxécuté sur la même machine que celle où // la synchronisation est lancée. Cela peut être l'expéditeur ou le // destinataire. // // Dans ce cas, vous devez invoquer le script à la place de rsync. Les arguments // seront transmis tel quels à rsync, mais le premier argument doit être " -- ". // Exemple : // // $ ./mysql-rsync.php -- --archive /srv/mysql www.dest.com:/srv/backups // // Dans ce cas, les tables sont d'abord verrouillées, flushées, puis rsync est // démarré avec les paramètres fournis. Les flux STDOUT et STDERR sont // retransmis de manière transparente. // // 2. Cas où le script est éxécuté sur la machine recevant la connexion // // Il s'agit du cas où le script est éxécuté sur la même machine que celle où // la demande de synchronisation est reçue. Cela peut être l'expéditeur ou le // destinataire. // // NOTE : comme précisé dans la partie Limitations, ce cas n'est pas utilisable // avec rsyncd. // // Dans ce cas, vous devez invoquer le script en lieu et place de rsync sur le // serveur distant grâce au paramètre --rsync-path, en précisant le chemin du // script sur le serveur distant suivi de " -- ". Exemple : // // $ rsync --archive --rsync-path="/home/backup/mysql-rsync.php -- " \ // www.dest.com:/srv/backups /srv/mysql // // Dans ce cas, le script éxécuté sur la machine distante via le shell distant // verrouillera et flushera les tables avant d'éxécuter rsync qui commencera // le transfert. // ## FONCTIONNEMENT DU SCRIPT ## // // Le script suit quatre étapes : // // 1. Il récupère la liste de toutes les tables de toutes les bases de données // du serveur MySQL (sauf la base information_schema pour des raisons // évidentes). // // 2. Il effectue un LOCK TABLES READ sur toutes les tables d'un seul coup, // suivi d'un FLUSH TABLES. // // 3. Il éxécute rsync en tant que processus enfant du script, en lui passant // les arguments du script. Pendant toute l'éxécution de rsync, le script // fait la passerelle entre les flux std* du script et de rsync. // // 4. Une fois l'éxécution de rsync terminée, le script fait un UNLOCK TABLES // et se termine. (en pratique, le UNLOCK TABLES est surtout là pour faire // propre, puisque les tables sont de toute façon déverrouillées lorsque le // script se termine) // ## PERFORMANCES ## // // Etant donné que le script doit transférer les données de STDIN et de STDOUT // depuis/vers rsync, il constitue un point de passage supplémentaire lors du // transit des données. D'où des risques de déperdition de performances. // // Sur une machine équivalent à un Athlon 1700 Mhz, le script utilise environ 2% // du CPU lors d'un transfert à environ 400 ko/s. // // Si cela vous pose un problème, veillez à éxécuter le script sur la machine // disposant du maximum de puissance de calcul. // ----------------------------------------------------------------------------- // ## CONFIGURATION ## define('MYSQL_HOSTNAME', 'localhost'); // MySQL : adresse du serveur define('MYSQL_USERNAME', 'root'); // MySQL : nom d'utilisateur define('MYSQL_PASSWORD', ''); // MySQL : mot de passe define('RSYNC', '/usr/bin/rsync'); // Chemin d'accès à l'éxécutable rsync define('RSYNC_BUFFER', 32768); // Taille du buffer de communication // avec rsync // Augmenter pour consommer moins de CPU // mais plus de RAM // Diminuer pour consommer moins de RAM // mais plus de CPU // (Note : il s'agit de la consommation // du script PHP) // Dans le doute, laisser la valeur par // défaut. define('TESTMODE', FALSE); // Mode de test, éxécute tout sauf rsync // Montre la ligne de commande rsync // NE RIEN EDITER APRES CETTE LIGNE // -------------------------------- // ################################ // -------------------------------- // notice = Satan error_reporting(E_ALL & ~E_NOTICE); // Fonction d'affichage d'erreur vers stderr function echo_e($string) { fwrite(STDERR, $string); } // Le script utilise deux connexions simultanées à la base MySQL pour maximiser les performances : // - Une qui sert à obtenir la liste de bases de données ($mysql_db), réutilisée ensuite pour le LOCK et le FLUSH TABLES // - Une autre pour obtenir la liste des tables pour chaque base de données ($mysql_tables) // L'utilisation de mysql_unbuffered_query() permet de faire tourner les deux requêtes en simultané. $mysql_db = @mysql_connect(MYSQL_HOSTNAME, MYSQL_USERNAME, MYSQL_PASSWORD, TRUE); if (!is_resource($mysql_db)) { echo_e('Cannot connect to MySQL server'."\n"); exit(1); } $dbquery = @mysql_unbuffered_query('SHOW DATABASES', $mysql_db); if ((!is_resource($dbquery)) or mysql_errno($mysql_db)) { echo_e('Cannot fetch databases'."\n"); exit(1); } $mysql_tables = mysql_connect(MYSQL_HOSTNAME, MYSQL_USERNAME, MYSQL_PASSWORD, TRUE); if (!is_resource($mysql_tables)) { echo_e('Cannot connect to MySQL server'."\n"); exit(1); } // Pour chaque base de données while ($database = mysql_fetch_row($dbquery)) { $database = $database[0]; // Ignorer la base information_schema if ($database == 'information_schema') continue; // Selectionner la base sur la connexion $mysql_tables if (!@mysql_select_db($database, $mysql_tables)) { echo_e('Cannot select database '.$database."\n"); exit(1); } $tblquery = @mysql_unbuffered_query('SHOW TABLES', $mysql_tables); if ((!is_resource($tblquery)) or mysql_errno($mysql_tables)) { echo_e('Cannot fetch tables in database '.$database."\n"); exit(1); } // Pour chaque table while ($table = @mysql_fetch_row($tblquery)) { $table = $table[0]; // Ajouter dans la liste des tables à verrouiller $tables[] = $database.'.'.$table.' READ'; } } // Plus besoin de $mysql_tables @mysql_close($mysql_tables); // On locke $lock = mysql_query('LOCK TABLES '.implode(', ', $tables), $mysql_db); if ((!($lock === TRUE)) or mysql_errno($mysql_db)) { echo_e('Cannot lock tables'."\n"); exit(1); } // On flushe $flush = @mysql_query('FLUSH TABLES', $mysql_db); if ((!($flush === TRUE)) or mysql_errno($mysql_db)) { echo_e('Cannot flush tables'."\n"); exit(1); } // On construit la ligne de commande d'éxécution de rsync $rsync_args = $argv; // Arguments = arguments du script... array_shift($rsync_args); // ...moins le premier (nom du fichier) $cmd = RSYNC.' '.implode(' ', $rsync_args); if (TESTMODE) echo_e($cmd."\n"); // Si en mode de test, afficher simplement la ligne de commande else { // Ici, il faut éxécuter rsync mais surtout mettre en place une passerelle pour les flux std*. // Impossible d'utiliser passthru() car cette fonction bufferize systématiquement les lignes de la sortie, quelle que soit la config // On utilise donc la puissante fonction proc_open() et on fout les mains dans le cambouis (c'est à dires les pipes) $desc = array( 0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w') ); $proc = @proc_open($cmd, $desc, $pipes); if (!is_resource($proc)) { echo_e('Cannot open rsync process'."\n"); exit(1); } // Des noms un peu plus jolis pour les pipes $procIn =& $pipes[0]; $procOut =& $pipes[1]; $procErr =& $pipes[2]; // Boucle globale de communication while (TRUE) { // Note : // - $toProcBuffer = buffer contenant les données en attente de transmission vers rsync // - $toStdOutBuffer = buffer contenant les données en attente de transmission vers STDOUT // - $toStdErrBuffer = buffer contenant les données en attente de transmission vers STDERR // On remplit le tableau des flux à surveiller en entrant sur le principe suivant : // Si on a déjà reçu des données sur un flux qu'on a pas encore retransmises, on arrête d'écouter pour éviter que les buffers ne deviennent obèses $toRead = array(); if (!strlen($toProcBuffer)) $toRead[] = STDIN; if (!(is_null($procOut) or strlen($toStdOutBuffer))) $toRead[] = $procOut; if (!(is_null($procErr) or strlen($toStdErrBuffer))) $toRead[] = $procErr; // On surveille les flux en sortant uniquement quand on a des données à envoyer $toWrite = array(); if (strlen($toProcBuffer)) $toWrite[] = $procIn; if (strlen($toStdOutBuffer)) $toWrite[] = STDOUT; if (strlen($toStdErrBuffer)) $toWrite[] = STDERR; // On surveille $select = @stream_select($toRead, $toWrite, $null = array(), NULL, NULL); if ($select === FALSE) break; // Lorsqu'une écriture est réalisée, le buffer correspondant est amputé des données envoyées // (rappel : fwrite() renvoie le nombre d'octets écrits, et une écriture n'écrit pas forcément tout) if (in_array(STDOUT, $toWrite)) // STDOUT est disponible en écriture, profitons-en $toStdOutBuffer = substr($toStdOutBuffer, @fwrite(STDOUT, $toStdOutBuffer)); if (in_array(STDERR, $toWrite)) // STDERR est disponible en écriture, profitons-en $toStdErrBuffer = substr($toStdErrBuffer, @fwrite(STDERR, $toStdErrBuffer)); if (in_array($procIn, $toWrite)) // Le STDIN de rsync est disponible en écriture, profitons-en $toProcBuffer = substr($toProcBuffer, @fwrite($procIn, $toProcBuffer)); if (in_array(STDIN, $toRead)) { // On a du nouveau sur STDIN, allons voir $data = @fread(STDIN, RSYNC_BUFFER); if (!strlen($data)) break; // Le shell a rompu le pipe STDIN avec le script, on a plus rien à faire là $toProcBuffer .= $data; // Retransmettons les données vers rsync } if (in_array($procOut, $toRead)) { // On a du nouveau sur le STDOUT de rsync, allons voir $data = @fread($procOut, RSYNC_BUFFER); if (!strlen($data)) $procOut = NULL; // rsync a rompu le pipe STDOUT avec le script, on le note $toStdOutBuffer .= $data; // Retransmettons les données vers STDOUT } if (in_array($procErr, $toRead)) { // On a du nouveau sur le STDERR de rsync, allons voir $data = @fread($procErr, RSYNC_BUFFER); if (!strlen($data)) $procErr = NULL; // rsync a rompu le pipe STDERR avec le script, on le note $toStdErrBuffer .= $data; // Retransmettons les données vers STDERR } // Le fait qu'un des deux pipes de rsync soit rompu n'implique pas une sortie de la boucle, car lorsque rsync se termine il peut y avoir des données en attente sur un flux alors que l'autre est déjà rompu // Du coup, on sort de la boucle uniquement si les deux flux (STDOUT et STDERR) sont rompus, car on n'a plus rien à faire là, mais on s'assure également qu'il n'y a aucune donnée encore en attente d'envoi vers l'extérieur if (is_null($procOut) and is_null($procErr) and !strlen($toStdOutBuffer) and !strlen($toStdErrBuffer)) break; } // rsync a terminé } // On déverrouille @mysql_query('UNLOCK TABLES', $mysql_db); mysql_close($mysql_db); // Au plaisir de vous revoir exit(0); ?>