Commit ceaf53d6 authored by Adrian Rossouw's avatar Adrian Rossouw

Major breakthroughs on the new backend code..

parent c47f9f96
<?php
// $Id$
/**
* @file
* Mysql provisioning module.
*
* The goal of this module is to create mysql databases and user accounts, for sites that are about to be created.
* It uses the provision API to tie into the right places in the site creation work flow.
*/
function db_drush_init() {
include_once('mysql/mysql_service.inc');
// this is where we generate the db service object.
}
function db_drush_exit() {
// determine how to close it too.
provision_service('db')->close();
}
function db_drush_help($section) {
switch ($section) {
case 'error:PROVISION_CREATE_DB_FAILED' :
return dt('Unable to create new databases.');
case 'error:PROVISION_DROP_DB_FAILED' :
return dt('Unable to drop database.');
}
}
class provisionService_db extends provisionService {
function __construct($creds) {
$this->creds = $creds;
}
/**
* Find a viable database name, based on available information.
*
* This function exists solely to work past mysql's database name restrictions.
* As mysql also does not have the ability to rename databases, it is completely
* possible that sites will be running with derivative names on the same server,
* until the upgrade / restore process is completed.
*
* TODO: abstract this properly.
*/
function suggest_db_name($url) {
if ($sid = drush_get_option('site_id')) {
$suggest_base = drush_get_option('aegir_db_prefix', 'site_') . $sid;
}
elseif ($name = drush_get_option('db_name')) {
// consider the verified database name if no site id was provided
//
// we strip out eventual _N suffixes before finding a new db name
// this is necessary because we may already have gone through this
// process (in a migration) and had a _N suffix added
$suggest_base = preg_replace('/_\d+$/', '', $name);
}
else {
// This is a last option, and not ideal: base the db name on the
// site name
//
// Provision only users will trigger this mostly.
$suggest_base = substr(str_replace(array(".", "-"), '' , ereg_replace("^www\.", "", $url)), 0, 14);
}
$suggest[] = $suggest_base;
for ($i = 0; $i < 100; $i++) {
$suggest[] = $suggest_base .'_'. $i;
}
foreach ($suggest as $option) {
if (!$this->database_exists($option)) {
return $option;
}
}
drush_set_error('PROVISION_CREATE_DB_FAILED', dt("Could not find a free database names after 100 attempts"));
return false;
}
/**
* Generate a new mysql database and user account for the specified credentials
*/
function create_site_database($url) {
$creds = $this->create_site_credentials($url);
extract($creds);
if (!$this->can_create_database()) {
drush_set_error('PROVISION_CREATE_DB_FAILED');
drush_log("Database could not be created.", 'error');
return FALSE;
}
drush_log(dt("Granting privileges to %user@%client on %database", array('%user' => $db_user, '%client' => $db_grant_host, '%database' => $db_name)));
if (!$this->grant($db_name, $db_user, $db_passwd, $db_grant_host)) {
drush_log("Could not GRANT user access.", 'warning');
}
$status = $this->database_exists($db_name);
if ($status) {
drush_log(dt('Created @name database', array("@name" => $db_name)), 'success');
}
else {
drush_set_error('PROVISION_CREATE_DB_FAILED', dt("Could not create @name database", array("@name" => $db_name)));
}
return $status;
}
/**
* Remove the database and user account for the supplied credentials
*/
function destroy_site_database($url) {
$creds = $this->fetch_site_credentials();
extract($creds);
if ( $this->database_exists($db_name) ) {
drush_log(dt("Dropping database @dbname", array('@dbname' => $db_name)));
if (!$this->drop_database($db_name)) {
drush_log(dt("Failed to drop database @dbname", array('@dbname' => $db_name)), 'warning');
}
}
if ( $this->database_exists($db_name) ) {
drush_set_error('PROVISION_DROP_DB_FAILED');
return FALSE;
}
drush_log(dt("Revoking privileges of %user@%client from %database", array('%user' => $db_user, '%client' => $db_grant_host, '%database' => $db_name)));
if (!$this->revoke($db_name, $db_user, $db_grant_host)) {
drush_log(dt("Failed to revoke user privileges"), 'warning');
}
}
function import_site_database($url) {
$dump_file = drush_get_option('sites_path') .'/'. $url .'/database.sql';
$creds = $this->fetch_site_credentials();
$exists = provision_path("exists", $dump_file, TRUE,
dt('Found database dump at @path.'),
dt('No database dump was found at @path.'),
'PROVISION_DB_DUMP_NOT_FOUND');
if ($exists) {
$readable = provision_path("readable", $dump_file, TRUE, dt('Database dump at @path is readable'),
dt('The database dump at @path could not be read.'),
'PROVISION_DB_DUMP_NOT_READABLE');
if ($readable) {
$this->import_dump($dump_file, $creds);
}
}
}
// todo - SORT THIS SHIT OUT!
function dump_site_database($url) {
return $this->dump_database($url);
}
function generate_site_credentials($url) {
$creds = array();
// replace with service type
$db_type = drush_get_option('db_type');
// As of Drupal 7 there is no more mysqli type
if (drush_drupal_major_version() >= 7) {
$db_type = ($db_type == 'mysqli') ? 'mysql' : $db_type;
}
//TODO - this should not be here at all
$creds['db_type'] = drush_set_option('db_type', $db_type, 'site');
$creds['db_host'] = drush_set_option('db_host', drush_get_option('db_host'), 'site');
$creds['db_passwd'] = drush_set_option('db_passwd', provision_password(), 'site');
$creds['db_name'] = drush_set_option('db_name', $this->suggest_db_name($url), 'site');
$creds['db_user'] = drush_set_option('db_user', $db_name, 'site');
$creds['db_grant_host'] = $this->grant_host();
return $creds;
}
function fetch_site_credentials($url) {
$creds = array();
$keys = array('db_type', 'db_user', 'db_name', 'db_host', 'db_passwd');
foreach ($keys as $key) {
$creds[$key] = drush_get_option($key, '', 'site');
}
$creds['db_grant_host'] = $this->grant_host();
return $creds;
}
function database_exists($name) {
return FALSE;
}
function drop_database($name) {
return FALSE;
}
function create_database($name) {
return FALSE;
}
function can_create_database() {
return FALSE;
}
function grant($name, $username, $password, $host = '') {
return FALSE;
}
function revoke($name, $username, $host = '') {
return FALSE;
}
function import_dump($dump_file, $creds) {
return FALSE;
}
function generate_dump($url) {
return FALSE;
}
function grant_host($db_host, $web_ip, $web_host) {
return 'localhost';
}
}
/**
* Indicates the place holders that should be replaced in _db_query_callback().
*/
define('PROVISION_QUERY_REGEXP', '/(%d|%s|%%|%f|%b)/');
// simple wrapper class for PDO based db services
class provisionService_db_pdo extends provisionService_db {
public $conn;
protected $creds;
private $dsn;
function __construct($creds) {
parent::__construct($creds);
$this->dsn = sprintf("%s:dbname=%s;host=%s", $this->db_type, $this->creds['name'], $this->creds['host']);
}
function connect() {
try {
$this->conn = new PDO($constr, $creds['user'], $creds['pass']);
}
catch (PDOException $e) {
return drush_set_error('PROVISION_DB_CONNECT_FAIL', $e->getMessage());
}
}
function close() {
$this->conn = null;
}
function query() {
$args = func_get_args();
array_shift($args);
if (isset($args[0]) and is_array($args[0])) { // 'All arguments in one array' syntax
$args = $args[0];
}
$this->query_callback($args, TRUE);
$query = preg_replace_callback(PROVISION_QUERY_REGEXP, array($this, 'query_callback'), $query);
try {
$result = $this->conn->query($query);
}
catch (PDOException e) {
drush_log($e->getMessage(), 'warning');
return FALSE;
}
return $result;
}
private function query_callback($match, $init = FALSE) {
static $args = NULL;
if ($init) {
$args = $match;
return;
}
switch ($match[1]) {
case '%d': // We must use type casting to int to convert FALSE/NULL/(TRUE?)
return (int) array_shift($args); // We don't need db_escape_string as numbers are db-safe
case '%s':
return $this->conn->quote(array_shift($args));
case '%%':
return '%';
case '%f':
return (float) array_shift($args);
case '%b': // binary data
return $this->conn->quote(array_shift($args));
}
}
}
<?php
function drush_db_provision_deploy_validate() {
provision_service('db')->connect();
}
function drush_db_provision_deploy($url) {
provision_service('db')->create_site_database($url);
provision_service('db')->import_site_database($url);
}
function drush_db_provision_deploy_rollback($url = NULL) {
provision_service('db')->destroy_site_database($url);
}
// Rollback doesn't apply here yet. Unless we trigger a deploy of the first dump
// made. Which could go on infinitely if something is really long.
function drush_db_post_provision_deploy($url) {
provision_path('unlink', drush_get_option('sites_path') .'/'. $url .'/database.sql', TRUE,
dt("Removed dump file @path after restoring from it"),
dt("Could not remove dump file @path"), 'DRUSH_PERM_ERROR');
}
<?php
function drush_db_provision_install_validate($url) {
provision_service('db')->connect();
}
function drush_db_pre_provision_install($url) {
provision_service('db')->create_site_database($url);
}
function drush_db_pre_provision_install_rollback($url) {
if (!_provision_drupal_site_installed($url)) {
provision_service('db')->destroy_site_database(
drush_get_option('db_name'), drush_get_option('db_user'),
drush_get_option('db_passwd')
);
}
}
<?php
// $Id$
function drush_db_provision_migrate_validate() {
provision_service('db')->connect();
}
// Deletes the old database credentials
function drush_db_post_provision_migrate($url) {
provision_service('db')->destroy_site_database(
drush_get_option('db_name'),
drush_get_option('db_user'),
drush_get_option('db_passwd')
);
}
<?php
// $Id$
// extends the pdo implementation
class provisionService_db_mysql extends provisionService_db_pdo {
function database_exists($name) {
$result = $this->query("SHOW DATABASES LIKE '%s'", $name);
return $result->fetchColumn(0);
}
function drop_database($name) {
return $this->query("DROP DATABASE `%s`", $name);
}
function create_database($name) {
return $this->query("CREATE DATABASE %s", $name);
}
function can_create_database() {
$test = drush_get_option('aegir_db_prefix', 'site_') .'test';
$this->create_database($test);
if ($this->database_exists($test)) {
if (!$this->drop_database($test)) {
drush_log(dt("Failed to drop database @dbname", array('@dbname' => $test)), 'warning');
}
return TRUE;
}
return FALSE;
}
function grant($name, $username, $password, $host = '') {
$host = ($host) ? $host : '%';
return $this->query("GRANT ALL PRIVILEGES ON `%s`.* TO `%s`@`%s` IDENTIFIED BY '%s'", $name, $username, $host, $password);
}
function revoke($name, $username, $host = '') {
$host = ($host) ? $host : '%';
$success = provision_db_query("REVOKE ALL PRIVILEGES ON `%s`.* FROM `%s`@`%s`", $name, $username, $host);
// check if there are any privileges left for the user
$grants = provision_db_query("SHOW GRANTS FOR `%s`@`%s`", $username, $host);
$grant_found = FALSE;
while ($grant = provision_db_fetch_array($grants)) {
// those are empty grants: just the user line
if (!preg_match("/^GRANT USAGE ON /", array_pop($grant))) {
// real grant, we shouldn't remove the user
$grant_found = TRUE;
break;
}
}
if (!$grant_found) {
$success = provision_db_query("DROP USER `%s`@`%s`", $username, $host) && $success;
}
return $success;
}
function import_dump($dump_file, $creds) {
extract($creds);
$cmd = sprintf("mysql --defaults-file=/dev/fd/3 %s", escapeshellcmd($db_name));
drush_log(sprintf("Importing database using command: %s", $cmd));
# pipe handling code, this is inspired by drush_provision_mysql_pre_provision_backup()
# we go through all this trouble to hide the password from the commandline, it's the most secure way (apart from writing a temporary file, which would create conflicts in parallel runs)
$mycnf = sprintf('[client]
host=%s
user=%s
password=%s
', $db_host, $db_user, $db_passwd);
$descriptorspec = array(
0 => array("file", $dump_file, "r"),
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
2 => array("pipe", "w"), // stderr is a file to write to
3 => array("pipe", "r"), // fd3 is our special file descriptor where we pass credentials
);
$process = proc_open($cmd, $descriptorspec, $pipes);
$output = "";
if (is_resource($process)) {
fwrite($pipes[3], $mycnf);
fclose($pipes[3]);
$output = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
// "It is important that you close any pipes before calling
// proc_close in order to avoid a deadlock"
fclose($pipes[1]);
fclose($pipes[2]);
$return_value = proc_close($process);
} else {
// XXX: failed to execute? unsure when this happens
$return_value = -1;
}
if ($return_value != 0) {
drush_set_error('PROVISION_DB_IMPORT_FAILED', dt("Database import failed: %output", array('%output' => $output)));
}
}
/**
* Properly guess the host part of the MySQL username based on a given
* database server host , web server IP and web server host.
*
* If the database and web server are the same machine, return a localhost
* grant, which is a special and very common case which does not involve any
* dns lookups and is thus faster and more secure.
*
* If the web server and database server are different machines, we prefer to
* use the IP address, which is faster and more secure because fewer lookups
* are done during connections.
*
*/
function grant_host($db_host, $web_ip, $web_host) {
$result = provision_db_result(provision_db_query("select current_user()"));
preg_match('/^.*@(.*)$/', $result, $matches);
return $matches[1];
}
//todo - SORT THIS SHIT OUT!
function generate_dump($url) {
# set the umask to 077 so that the dump itself is generated so it's non-readable by the webserver
umask(0077);
drush_log("Generating mysql dump for $url.", 'backup');
# mixed copy-paste of drush_shell_exec and provision_shell_exec
$cmd = sprintf("mysqldump --defaults-file=/dev/fd/3 -rsites/%s/database.sql %s", escapeshellcmd($url), escapeshellcmd(drush_get_option('db_name')));
drush_log($cmd);
if (drush_get_context('DRUSH_VERBOSE') || drush_get_context('DRUSH_SIMULATE')) {
drush_print('Executing: ' . $cmd, $indent);
}
if (drush_get_context('DRUSH_SIMULATE')) {
return true;
}
# pipe handling code
# we go through all this trouble to hide the password from the commandline, it's the most secure way (apart from writing a temporary file, which would create conflicts in parallel runs)
$mycnf = sprintf('[client]
host=%s
user=%s
password=%s
', drush_get_option('db_host'), urldecode(drush_get_option('db_user')), urldecode(drush_get_option('db_passwd')));
$descriptorspec = array(
// 0 => array("pipe", "r"), // this would be stdin, but we don't need to input into mysqldump
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
2 => array("pipe", "w"), // stderr is a file to write to
3 => array("pipe", "r"), // fd3 is our special file descriptor where we pass credentials
);
$process = proc_open($cmd, $descriptorspec, $pipes);
$output = array();
if (is_resource($process)) {
fwrite($pipes[3], $mycnf);
fclose($pipes[3]);
$output = array_filter(array_merge(explode("\n", stream_get_contents($pipes[1])), explode("\n", stream_get_contents($pipes[2]))));
// "It is important that you close any pipes before calling
// proc_close in order to avoid a deadlock"
fclose($pipes[1]);
fclose($pipes[2]);
$return_value = proc_close($process);
} else {
// XXX: failed to execute? unsure when this happens
$return_value = -1;
}
# resuming drush_exec copy/paste
$indent = 0;
_drush_shell_exec_output_set($output);
if (drush_get_context('DRUSH_VERBOSE')) {
foreach ($output as $line) {
drush_print($line, $indent + 2);
}
}
$result = ($return_value == 0);
if (!$result && !drush_get_option('force', false)) {
drush_set_error('PROVISION_BACKUP_FAILED', dt("Could not generate database backup from mysqldump"));
}
# reset the umask to normal permissions
umask(0022);
}
}
<?php
function drush_provision_mysql_provision_deploy_validate() {
provision_db_connect();
}
function drush_provision_mysql_provision_deploy($url) {
$db_type = drush_get_option('db_type');
// As of Drupal 7 there is no more mysqli type
if (drush_drupal_major_version() >= 7) {
$db_type = ($db_type == 'mysqli') ? 'mysql' : $db_type;
}
$db_type = drush_set_option('db_type', $db_type, 'site');
$db_host = drush_set_option('db_host', drush_get_option('db_host'), 'site');
$db_passwd = drush_set_option('db_passwd', provision_password(), 'site');
$db_name = drush_set_option('db_name', _provision_mysql_suggest_db_name($url), 'site');
$db_user = drush_set_option('db_user', $db_name, 'site');
_provision_mysql_new_site_db($db_name, $db_user, $db_passwd);
_provision_mysql_import_dump(
drush_get_option('sites_path') .'/'. $url .'/database.sql',
$db_name, $db_user, $db_passwd, $db_host );
}
function drush_provision_mysql_provision_deploy_rollback($url = NULL) {
_provision_mysql_destroy_site_db(drush_get_option('db_name'), drush_get_option('db_user'), drush_get_option('db_passwd'));
}
// Rollback doesn't apply here yet. Unless we trigger a deploy of the first dump
// made. Which could go on infinitely if something is really long.
function drush_provision_mysql_post_provision_deploy($url = NULL) {
provision_path('unlink', drush_get_option('sites_path') .'/'. $url .'/database.sql', TRUE,
dt("Removed dump file @path after restoring from it"),
dt("Could not remove dump file @path"), 'DRUSH_PERM_ERROR');
}
<?php
function drush_provision_mysql_provision_install_validate() {
provision_db_connect();
}
function drush_provision_mysql_pre_provision_install($url = NULL) {
$db_type = drush_get_option('db_type');
// As of Drupal 7 there is no more mysqli type
if (drush_drupal_major_version() >= 7) {
$db_type = ($db_type == 'mysqli') ? 'mysql' : $db_type;
}
$db_type = drush_set_option('db_type', $db_type, 'site');
$db_host = drush_set_option('db_host', drush_get_option('db_host'), 'site');
$db_passwd = drush_set_option('db_passwd', provision_password(), 'site');
$db_name = drush_set_option('db_name', _provision_mysql_suggest_db_name($url), 'site');
$db_user = drush_set_option('db_user', $db_name, 'site');
_provision_mysql_new_site_db($db_name, $db_user, $db_passwd);
}
function drush_provision_mysql_pre_provision_install_rollback($url = NULL) {
if (!_provision_drupal_site_installed($url)) {
_provision_mysql_destroy_site_db(drush_get_option('db_name'), drush_get_option('db_user'), drush_get_option('db_passwd'));
}
}
<?php
// $Id$
function drush_provision_mysql_provision_migrate_validate() {
provision_db_connect();
}
// Deletes the old database credentials
function drush_provision_mysql_post_provision_migrate($url) {
return _provision_mysql_destroy_site_db(drush_get_option('db_name'), drush_get_option('db_user'), drush_get_option('db_passwd'));
}
<?php
// $Id$
/**
* @file mysql db api extension
*
* A collection of helper functions used by the main provision hooks to accomplish their tasks.
*/
function _provision_db_connection($conn = NULL) {
static $connection = NULL;
if (!is_null($conn)) {
$connection = $conn;
}
return $connection;
}
function provision_db_connect() {
if (_provision_db_connection()) {
return TRUE;
}
$connection = @mysql_connect(drush_get_option('master_db_host'), drush_get_option('master_db_user'), drush_get_option('master_db_passwd'));
if (!$connection) {
drush_set_error('PROVISION_MASTER_DB_FAILED', dt('Could not connect to the master database server (%err).', array('%err' => mysql_error())), 'error');
}
else {
_provision_db_connection($connection);
}
return TRUE;
}
function provision_db_query($query) {
$args = func_get_args();
array_shift($args);
if (isset($args[0]) and is_array($args[0])) { // 'All arguments in one array' syntax
$args = $args[0];
}
_provision_db_query_callback($args, TRUE);
$query = preg_replace_callback(PROVISION_QUERY_REGEXP, '_provision_db_query_callback', $query);
return _provision_db_query($query);
}
/**
* Helper function for db_query().
*/
function _provision_db_query($query, $debug = 0) {
$result = mysql_query($query, _provision_db_connection());
if (!mysql_errno(_provision_db_connection())) {
return $result;
}