Commit 91b37703 authored by Steven Jones's avatar Steven Jones

Move provisionService_db to an autoload.

parent c50da8f9
......@@ -27,7 +27,7 @@ class Provision_Context_server extends Provision_Context {
'--master_url' => 'server: Hostmaster URL',
);
foreach (drush_command_invoke_all('provision_services') as $service => $default) {
$reflect = new reflectionClass('provisionService_' . $service);
$reflect = new reflectionClass('Provision_Service_' . $service);
$base_dir = dirname($reflect->getFilename());
$types = array();
$options['--' . $service . '_service_type'] = 'placeholder';
......@@ -81,7 +81,7 @@ class Provision_Context_server extends Provision_Context {
* Spawn an instance for a specific service type and associate it to the owner.
*/
function spawn_service($service, $default = null) {
$reflect = new reflectionClass('provisionService_' . $service);
$reflect = new reflectionClass('Provision_Service_' . $service);
$base_dir = dirname($reflect->getFilename());
$type_option = "{$service}_service_type";
......@@ -96,14 +96,17 @@ class Provision_Context_server extends Provision_Context {
}
if ($type) {
$file = sprintf("%s/%s/%s_service.inc", $base_dir, $type, $type);
$className = sprintf("provisionService_%s_%s", $service, $type);
if (file_exists($file)) {
$className = sprintf("Provision_Service_%s_%s", $service, $type);
if (class_exists($className)) {
drush_log("Loading $type driver for the $service service");
include_once($file);
//include_once($file);
$object = new $className($this->name);
$this->services[$service] = $object;
$this->setProperty($type_option, $type);
}
else {
drush_log("Unable to load $type driver for the $service service", 'error');
}
}
else {
$this->services[$service] = new Provision_Service_null($this->name);
......
<?php
class Provision_Service_db extends Provision_Service {
protected $service = 'db';
/**
* Register the db handler for sites, based on the db_server option.
*/
static function subscribe_site($context) {
$context->setProperty('db_server', '@server_master');
$context->is_oid('db_server');
$context->service_subscribe('db', $context->db_server->name);
}
static function option_documentation() {
return array(
'--master_db' => 'server with db: Master database connection info, {type}://{user}:{password}@{host}',
);
}
function init_server() {
parent::init_server();
$this->server->setProperty('master_db');
$this->creds = array_map('urldecode', parse_url($this->server->master_db));
return TRUE;
}
/**
* Verifies database connection and commands
*/
function verify_server_cmd() {
if ($this->connect()) {
if ($this->can_create_database()) {
drush_log(dt('Provision can create new databases.'), 'message');
}
else {
drush_set_error('PROVISION_CREATE_DB_FAILED');
}
if ($this->can_grant_privileges()) {
drush_log(dt('Provision can grant privileges on database users.'), 'message');;
}
else {
drush_set_error('PROVISION_GRANT_DB_USER_FAILED');
}
} else {
drush_set_error('PROVISION_CONNECT_DB_FAILED');
}
}
/**
* Find a viable database name, based on the site's uri.
*/
function suggest_db_name() {
$uri = $this->context->uri;
$suggest_base = substr(str_replace(array('.', '-'), '' , preg_replace('/^www\./', '', $uri)), 0, 16);
if (!$this->database_exists($suggest_base)) {
return $suggest_base;
}
for ($i = 0; $i < 100; $i++) {
$option = sprintf("%s_%d", substr($suggest_base, 0, 15 - strlen( (string) $i) ), $i);
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($creds = array()) {
if (!sizeof($creds)) {
$creds = $this->generate_site_credentials();
}
extract($creds);
if (!$this->can_create_database()) {
drush_set_error('PROVISION_CREATE_DB_FAILED');
drush_log("Database could not be created.", 'error');
return FALSE;
}
foreach ($this->grant_host_list() as $db_grant_host) {
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_set_error('PROVISION_CREATE_DB_FAILED', dt("Could not create database user @user", array('@user' => $db_user)));
}
}
$this->create_database($db_name);
$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($creds = array()) {
if (!sizeof($creds)) {
$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;
}
foreach ($this->grant_host_list() as $db_grant_host) {
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($dump_file = null, $creds = array()) {
if (is_null($dump_file)) {
$dump_file = d()->site_path . '/database.sql';
}
if (!sizeof($creds)) {
$creds = $this->fetch_site_credentials();
}
$exists = provision_file()->exists($dump_file)
->succeed('Found database dump at @path.')
->fail('No database dump was found at @path.', 'PROVISION_DB_DUMP_NOT_FOUND')
->status();
if ($exists) {
$readable = provision_file()->readable($dump_file)
->succeed('Database dump at @path is readable')
->fail('The database dump at @path could not be read.', 'PROVISION_DB_DUMP_NOT_READABLE')
->status();
if ($readable) {
$this->import_dump($dump_file, $creds);
}
}
}
function generate_site_credentials() {
$creds = array();
// replace with service type
$db_type = drush_get_option('db_type', function_exists('mysqli_connect') ? 'mysqli' : 'mysql');
// 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', $this->server->remote_host, 'site');
$creds['db_port'] = drush_set_option('db_port', $this->server->db_port, 'site');
$creds['db_passwd'] = drush_set_option('db_passwd', provision_password(), 'site');
$creds['db_name'] = drush_set_option('db_name', $this->suggest_db_name(), 'site');
$creds['db_user'] = drush_set_option('db_user', $creds['db_name'], 'site');
return $creds;
}
function fetch_site_credentials() {
$creds = array();
$keys = array('db_type', 'db_port', 'db_user', 'db_name', 'db_host', 'db_passwd');
foreach ($keys as $key) {
$creds[$key] = drush_get_option($key, '', 'site');
}
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 can_grant_privileges() {
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() {
return FALSE;
}
/**
* Return a list of hosts, as seen by the db server, which should be granted
* access to the site database.
*/
function grant_host_list() {
return array_unique(array_map(array($this, 'grant_host'), $this->context->service('http')->grant_server_list()));
}
/**
* Return a hostname suitable for database grants from a server object.
*/
function grant_host(Provision_Context_Server $server) {
return $server->remote_host;
}
}
<?php
// $Id$
// extends the pdo implementation
class Provision_Service_db_mysql extends Provision_Service_db_pdo {
public $PDO_type = 'mysql';
protected $has_port = TRUE;
function default_port() {
return 3306;
}
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;
}
/**
* Verifies that provision can grant privileges to a user on a database.
*
* @return
* TRUE if the check was successful.
*/
function can_grant_privileges() {
$dbname = drush_get_option('aegir_db_prefix', 'site_');
$user = $dbname . '_user';
$password = $dbname . '_password';
$host = $dbname . '_host';
if ($status = $this->grant($dbname, $user, $password, $host)) {
$this->revoke($dbname, $user, $host);
}
return $status;
}
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 = $this->query("REVOKE ALL PRIVILEGES ON `%s`.* FROM `%s`@`%s`", $name, $username, $host);
// check if there are any privileges left for the user
$grants = $this->query("SHOW GRANTS FOR `%s`@`%s`", $username, $host);
$grant_found = FALSE;
if ($grants) {
while ($grant = $grants->fetch()) {
// 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 = $this->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));
$success = $this->safe_shell_exec($cmd, $db_host, $db_user, $db_passwd, $dump_file);
drush_log(sprintf("Importing database using command: %s", $cmd));
if (!$success) {
drush_set_error('PROVISION_DB_IMPORT_FAILED', dt("Database import failed: %output", array('%output' => $this->safe_shell_exec_output)));
}
}
function grant_host(Provision_Context_Server $server) {
$command = sprintf('mysql -u intntnllyInvalid -h %s -P %s -e ""',
escapeshellarg($this->server->remote_host),
escapeshellarg($this->server->db_port));
$server->shell_exec($command);
if (preg_match("/Access denied for user 'intntnllyInvalid'@'([^']*)'/", implode('', drush_shell_exec_output()), $match)) {
return $match[1];
}
elseif (preg_match("/Host '([^']*)' is not allowed to connect to/", implode('', drush_shell_exec_output()), $match)) {
return $match[1];
}
else {
return drush_set_error('PROVISION_DB_CONNECT_FAIL', dt('Dummy connection failed to fail: %msg', array('%msg' => join("\n", drush_shell_exec_output()))));
}
}
function generate_dump() {
// Aet the umask to 077 so that the dump itself is generated so it's
// non-readable by the webserver.
umask(0077);
// Mixed copy-paste of drush_shell_exec and provision_shell_exec.
$cmd = sprintf("mysqldump --defaults-file=/dev/fd/3 %s | sed 's|/\\*!50001 CREATE ALGORITHM=UNDEFINED \\*/|/\\*!50001 CREATE \\*/|g; s|/\\*!50017 DEFINER=`[^`]*`@`[^`]*`\s*\\*/||g' | sed '/\\*!50013 DEFINER=.*/ d' > %s/database.sql", escapeshellcmd(drush_get_option('db_name')), escapeshellcmd(d()->site_path));
$success = $this->safe_shell_exec($cmd, drush_get_option('db_host'), urldecode(drush_get_option('db_user')), urldecode(drush_get_option('db_passwd')));
if (!$success && !drush_get_option('force', false)) {
drush_set_error('PROVISION_BACKUP_FAILED', dt('Could not generate database backup from mysqldump. (error: %msg)', array('%msg' => $this->safe_shell_exec_output)));
}
// Reset the umask to normal permissions.
umask(0022);
}
/**
* 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)
*
* XXX: this needs to be refactored so it:
* - works even if /dev/fd/3 doesn't exit
* - has a meaningful name (we're talking about reading and writing
* dumps here, really, or at least call mysql and mysqldump, not
* just any command)
* - can be pushed upstream to drush (http://drupal.org/node/671906)
*/
function safe_shell_exec($cmd, $db_host, $db_user, $db_passwd, $dump_file = null) {
$mycnf = sprintf('[client]
host=%s
user=%s
password=%s
port=%s
', $db_host, $db_user, $db_passwd, $this->server->db_port);
$stdin_spec = (!is_null($dump_file)) ? array("file", $dump_file, "r") : array("pipe", "r");
$descriptorspec = array(
0 => $stdin_spec,
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
);
$pipes = array();
$process = proc_open($cmd, $descriptorspec, $pipes);
$this->safe_shell_exec_output = '';
if (is_resource($process)) {
fwrite($pipes[3], $mycnf);
fclose($pipes[3]);
$this->safe_shell_exec_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;
}
return ($return_value == 0);
}
}
<?php
// simple wrapper class for PDO based db services
class provisionService_db_pdo extends provisionService_db {
public $conn;
protected $creds;
private $dsn;
function init_server() {
parent::init_server();
$this->dsn = sprintf("%s:host=%s", $this->PDO_type, $this->creds['host']);
if ($this->has_port) {
$this->dsn = "{$this->dsn};port={$this->server->db_port}";
}
}
function connect() {
$user = isset($this->creds['user']) ? $this->creds['user'] : '';
$pass = isset($this->creds['pass']) ? $this->creds['pass'] : '';
try {
$this->conn = new PDO($this->dsn, $user, $pass);
return $this->conn;
}
catch (PDOException $e) {
return drush_set_error('PROVISION_DB_CONNECT_FAIL', $e->getMessage());
}
}
function close() {
$this->conn = null;
}
function 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];
}
$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;
}
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 substr($this->conn->quote(array_shift($args)), 1, -1);
case '%%':
return '%';
case '%f':
return (float) array_shift($args);
case '%b': // binary data
return $this->conn->quote(array_shift($args));
}
}
function database_exists($name) {
$dsn = $this->dsn . ';dbname=' . $name;
try {
// Try to connect to the DB to test if it exists.
$conn = new PDO($dsn, $this->creds['user'], $this->creds['pass']);
// Free the $conn memory.
$conn = NULL;
return TRUE;
}
catch (PDOException $e) {
return FALSE;
}
}
}
This diff is collapsed.
<?php
// $Id$
// extends the pdo implementation
class provisionService_db_mysql extends provisionService_db_pdo {
public $PDO_type = 'mysql';
protected $has_port = TRUE;
function default_port() {
return 3306;
}
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;
}
/**
* Verifies that provision can grant privileges to a user on a database.
*
* @return
* TRUE if the check was successful.
*/
function can_grant_privileges() {
$dbname = drush_get_option('aegir_db_prefix', 'site_');
$user = $dbname . '_user';
$password = $dbname . '_password';
$host = $dbname . '_host';
if ($status = $this->grant($dbname, $user, $password, $host)) {
$this->revoke($dbname, $user, $host);
}
return $status;
}
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 = $this->query("REVOKE ALL PRIVILEGES ON `%s`.* FROM `%s`@`%s`", $name, $username, $host);
// check if there are any privileges left for the user
$grants = $this->query("SHOW GRANTS FOR `%s`@`%s`", $username, $host);
$grant_found = FALSE;
if ($grants) {
while ($grant = $grants->fetch()) {
// 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 = $this->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));
$success = $this->safe_shell_exec($cmd, $db_host, $db_user, $db_passwd, $dump_file);
drush_log(sprintf("Importing database using command: %s", $cmd));
if (!$success) {
drush_set_error('PROVISION_DB_IMPORT_FAILED', dt("Database import failed: %output", array('%output' => $this->safe_shell_exec_output)));
}
}
function grant_host(Provision_Context_Server $server) {
$command = sprintf('mysql -u intntnllyInvalid -h %s -P %s -e ""',
escapeshellarg($this->server->remote_host),
escapeshellarg($this->server->db_port));
$server->shell_exec($command);
if (preg_match("/Access denied for user 'intntnllyInvalid'@'([^']*)'/", implode('', drush_shell_exec_output()), $match)) {
return $match[1];
}
elseif (preg_match("/Host '([^']*)' is not allowed to connect to/", implode('', drush_shell_exec_output()), $match)) {
return $match[1];
}
else {
return drush_set_error('PROVISION_DB_CONNECT_FAIL', dt('Dummy connection failed to fail: %msg', array('%msg' => join("\n", drush_shell_exec_output()))));
}
}
function generate_dump() {
// Aet the umask to 077 so that the dump itself is generated so it's
// non-readable by the webserver.
umask(0077);
// Mixed copy-paste of drush_shell_exec and provision_shell_exec.
$cmd = sprintf("mysqldump --defaults-file=/dev/fd/3 %s | sed 's|/\\*!50001 CREATE ALGORITHM=UNDEFINED \\*/|/\\*!50001 CREATE \\*/|g; s|/\\*!50017 DEFINER=`[^`]*`@`[^`]*`\s*\\*/||g' | sed '/\\*!50013 DEFINER=.*/ d' > %s/database.sql", escapeshellcmd(drush_get_option('db_name')), escapeshellcmd(d()->site_path));
$success = $this->safe_shell_exec($cmd, drush_get_option('db_host'), urldecode(drush_get_option('db_user')), urldecode(drush_get_option('db_passwd')));
if (!$success && !drush_get_option('force', false)) {
drush_set_error('PROVISION_BACKUP_FAILED', dt('Could not generate database backup from mysqldump. (error: %msg)', array('%msg' => $this->safe_shell_exec_output)));
}
// Reset the umask to normal permissions.
umask(0022);
}
/**
* 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)
*
* XXX: this needs to be refactored so it:
* - works even if /dev/fd/3 doesn't exit
* - has a meaningful name (we're talking about reading and writing
* dumps here, really, or at least call mysql and mysqldump, not
* just any command)
* - can be pushed upstream to drush (http://drupal.org/node/671906)
*/
function safe_shell_exec($cmd, $db_host, $db_user, $db_passwd, $dump_file = null) {
$mycnf = sprintf('[client]
host=%s
user=%s
password=%s
port=%s
', $db_host, $db_user, $db_passwd, $this->server->db_port);
$stdin_spec = (!is_null($dump_file)) ? array("file", $dump_file, "r") : array("pipe", "r");
$descriptorspec = array(
0 => $stdin_spec,
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
);
$pipes = array();
$process = proc_open($cmd, $descriptorspec, $pipes);
$this->safe_shell_exec_output = '';
if (is_resource($process)) {
fwrite($pipes[3], $mycnf);
fclose($pipes[3]);
$this->safe_shell_exec_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]);