$value) { if (preg_match("/^load-([a-z\-]*)$/", $arg, $matches)) { $module = str_replace("-", "_", $matches[1]); provision_log("notice", t("Loaded @module on request", array("@module" => $module))); drupal_load('module', $module); $loaded[$module]['name'] = $module; $loaded[$module]['filename'] = drupal_get_filename('module', $module); $reset = TRUE; } } if ($reset) { $list = module_list(TRUE, FALSE); foreach ($list as $module) { $loaded[$module]['name'] = $module; $loaded[$module]['filename'] = drupal_get_filename('module', $module); } module_list(TRUE, FALSE, FALSE, $loaded); // Force to regenerate the stored list of hook implementations. module_implements('', FALSE, TRUE); } } /** * Implementation of hook_init * * Provide defines for all the major paths and settings. * These are settings that must not be modified during the running of the * program, but are configurable. */ function provision_init() { // Load requested additional modules provision_load_from_args(); // Set up defines for platform if (function_exists('drush_get_option')) { $docroot = drush_get_option(array("r", "root"), $_SERVER['PWD']); } else { $docroot = $_SERVER['pwd']; } define('PROVISION_DOMAIN', $_SERVER['HTTP_HOST']); $path = ($docroot) ? $docroot : $_SERVER['DOCUMENT_ROOT']; define('PROVISION_DOCROOT_PATH', rtrim($path, '/')); define('PROVISION_SITES_PATH', rtrim($path, '/') .'/sites'); define('PROVISION_DRUSH_PATH', './drush.php'); $parts = explode("/", rtrim($path, '/')); array_pop($parts); define('PROVISION_PARENT_PATH', rtrim(implode("/" , $parts), '/')); if (file_exists('sites/default/provision.settings.php') && is_readable('sites/default/provision.settings.php')) { include_once('sites/default/provision.settings.php'); } else { $defaults = _provision_config_defaults(); define('PROVISION_BACKUP_PATH', $defaults['backup_path']); define('PROVISION_CONFIG_PATH', $defaults['config_path']); define('PROVISION_WEB_GROUP', $defaults['web_group']); define('PROVISION_MASTER_DB', $defaults['master_db']); define('PROVISION_SCRIPT_USER', $defaults['script_user']); define('PROVISION_RESTART_CMD', $defaults['restart_cmd']); define('PROVISION_MASTER_URL', $defaults['master_url']); define('PROVISION_MASTER_DB', $defaults['master_db']); } # These settings are based on settings in the provision.settings.php file define('PROVISION_VHOST_PATH', PROVISION_CONFIG_PATH .'/vhost.d'); define('PROVISION_DRUSHRC_PATH', PROVISION_CONFIG_PATH .'/drushrc.d'); define('PROVISION_WEB_DISABLE_URL', PROVISION_MASTER_URL .'/provision/disabled'); define('PROVISION_WEB_MAINTENENCE_URL', PROVISION_MASTER_URL .'/provision/maintenance'); $db = parse_url(PROVISION_MASTER_DB); define('PROVISION_DB_USER', $db['user']); define('PROVISION_DB_PASSWD', $db['pass']); define('PROVISION_DB_HOST', $db['host']); // Drupal does not support multiple types of connections in the same session preg_match("$^([a-z]*)://$", $GLOBALS['db_url'], $matches); define('PROVISION_DB_TYPE', $matches[1]); } function _provision_default_restart_cmd() { # try to detect the apache restart command $command = '/usr/sbin/apachectl'; # a proper default for most of the world foreach (array('/usr/local/sbin/apachectl', # freebsd '/usr/sbin/apache2ctl', # debian + apache2 $command) as $test) { if (is_executable($test)) { $command = $test; } } return "sudo $command graceful"; } /** * Provides a set of best guess values for the provision.settings.php file */ function _provision_config_defaults() { $default = array(); $defaults['backup_path'] = defined('PROVISION_BACKUP_PATH') ? PROVISION_BACKUP_PATH : PROVISION_PARENT_PATH .'/backups'; $defaults['config_path'] = defined('PROVISION_CONFIG_PATH') ? PROVISION_CONFIG_PATH : PROVISION_PARENT_PATH .'/config'; $defaults['script_user'] = defined('PROVISION_SCRIPT_USER') ? PROVISION_SCRIPT_USER : get_current_user(); $info = posix_getgrgid(posix_getgid()); $defaults['web_group'] = defined('PROVISION_WEB_GROUP') ? PROVISION_WEB_GROUP : $info['name']; $defaults['master_db'] = defined('PROVISION_MASTER_DB') ? PROVISION_MASTER_DB : $GLOBALS['db_url']; $defaults['restart_cmd'] = defined('PROVISION_RESTART_CMD') ? PROVISION_RESTART_CMD : _provision_default_restart_cmd(); $master_url = variable_get('provision_install_url', $GLOBALS['base_url']); $defaults['master_url'] = defined('PROVISION_MASTER_URL') ? PROVISION_MASTER_URL : $master_url; return $defaults; } /** * Generate a provision.settings.php file to configure provision */ function _provision_generate_config($data = array()) { provision_log('notice', t("Generating provision.settings.php file")); provision_path("chmod", "sites/default", 0750); if (provision_path("exists", "sites/default/provision.settings.php")) { provision_path("chmod", "sites/default/provision.settings.php", 0600, t('Changed permissions of provision.settings.php to @confirm'), t('Could not change permissions of provision.settings.php to @confirm')); } $data = array_merge(_provision_config_defaults(), $data); $fp = fopen("sites/default/provision.settings.php", "w"); $text = file_get_contents(drupal_get_path('module', 'provision') .'/provision_settings.tpl.php'); fwrite($fp, " t("Basic configuration")); } /** * Implementation of hook_menu(). */ function provision_menu($may_cache) { $items[] = array( 'path' => 'admin/help/provision/requirements', 'title' => t('Provisioning requirements'), 'description' => t("Information of how to configure the provisioning system."), 'callback' => 'provision_help_requirements', 'type' => MENU_CALLBACK ); $items[] = array( 'path' => 'provision', 'title' => t('Configure your platform'), 'description' => t('Configure your platform.'), 'callback' => 'provision_front', 'type' => MENU_CALLBACK, 'access' => TRUE, ); $items[] = array( 'path' => 'provision/disabled', 'title' => t('Site disabled'), 'description' => t('Page showed when a site is disabled.'), 'callback' => 'provision_disabled_site', 'type' => MENU_CALLBACK, 'access' => user_access('access content'), ); $items[] = array( 'path' => 'provision/maintenance', 'title' => t('Site is undergoing maintenance.'), 'description' => t('Page shown when a site is being restored or moved etc.'), 'callback' => 'provision_site_maintenance', 'type' => MENU_CALLBACK, 'access' => user_access('access content'), ); $items[] = array( 'path' => 'provision/notfound', 'title' => t('Site not found.'), 'description' => t('The site you have requested is not available.'), 'callback' => 'provision_site_notfound', 'type' => MENU_CALLBACK, 'access' => user_access('access content'), ); return $items; } function provision_front() { if (variable_get('provision_setup', FALSE)) { drupal_goto('provision/notfound'); } return _provision_requirements("provision_setup"); } function provision_disabled_site() { drupal_set_breadcrumb(array()); return "

This site was disabled by the site administrators.

"; } function provision_site_maintenance() { drupal_set_breadcrumb(array()); return "

This site is being worked on presently. Check back later.

"; } function provision_site_notfound() { drupal_set_breadcrumb(array()); return "

The site you have requested does not exist.

"; } /** * @} End of "defgroup provisionui" */ /** * @defgroup provisiondrush Command line interface for Provision. * @{ */ /** * Implementation of hook_drush_command(). */ function provision_drush_command() { $items['provision install'] = array( 'callback' => 'provision_install_cmd', 'arguments' => array('domain.com' => t('The domain of the site to install.')), 'description' => t('Provision a new site using the provided data.') ); $items['provision synch'] = array( 'callback' => '_provision_synch', 'optional arguments' => array('domain.com' => t('The domain of the site to synch.')), 'description' => t('Regenerate the configuration files for a site or platform.') ); $items['provision import'] = array( 'callback' => '_provision_import', 'arguments' => array('domain.com' => t('The domain of the site to import.')), 'description' => t('Turn an already running site into a provisioned site.') ); $items['provision backup'] = array( 'callback' => '_provision_backup', 'arguments' => array('domain.com' => t('The domain of the site to back up.')), 'optional arguments' => array('backup-file' => t('The file to save the backup to. This will be a gzipped tarball.')), 'description' => t('Generate a back up for the site.') ); $items['provision enable'] = array( 'callback' => '_provision_enable', 'arguments' => array('domain.com' => t('The domain of the site to enable (only if enabled).')), 'description' => 'Enable a disabled site.' ); $items['provision disable'] = array( 'callback' => '_provision_disable', 'arguments' => array('domain.com' => t('The domain of the site to disable (only if disabled).')), 'description' => 'Disable a site.' ); $items['provision verify'] = array( 'callback' => '_provision_verify', # 'arguments' => array('domain.com' => t('The domain of the site to disable (only if disabled).')), 'description' => 'Verify that the provisioning framework is correctly installed.' ); $items['provision restore'] = array( 'callback' => '_provision_restore', 'description' => 'Restore the site to a previous backup. This will also generate a backup of the site as it was.', 'arguments' => array('domain.com' => t('The domain of the site to be restored'), 'site_backup.tar.gz' => t('The backup to restore the site to.')) ); $items['provision delete'] = array( 'callback' => '_provision_delete', 'description' => 'Delete a site.' ); $items['provision cron'] = array( 'callback' => '_provision_cron', 'description' => 'Run cron process for site.', 'arguments' => array('domain.com' => t('The domain of the site to be processed')) ); if (!function_exists('hosting_setup')) { $items['provision setup'] = array( 'callback' => '_provision_setup_cmd', 'description' => 'Initialize this platform to be able to create hosted sites.', ); } /* // Not implemented yet. $items['provision deploy'] = array( 'callback' => '_provision_deploy', 'description' => 'Deploy a backup made on another provisioning platform on this one.' ); $items['provision rename'] = array( 'callback' => '_provision_rename', 'description' => 'Change the url of an existing site.' ); */ return $items; } /** * Drush callback function * * Installs a new site at $url. * It does so by calling hook_provision_pre_install(), hook_provision_install() and hook_provision_post_install(). * * @param url * The url of the site being installed. * @return * Returns provision_output on success or error. * Will exit with a PROVISION_SITE_INSTALLED error if the site already exists. * Will exit with a PROVISION_FRAMEWORK_ERROR if the command is incorrectly used. */ function provision_install_cmd($url) { global $args; $data = provision_get_site_data($url); if (!$args['commands'][2]) { print "Usage: drush.php provision install DOMAIN [OPTIONS]\n"; print "Install a new site for the domain DOMAIN.\n"; print "Example: drush.php provision install mydomain.com --site-db-host localhost\n"; provision_log("error", "Incorrect usage of the provisioning framework"); provision_set_error(PROVISION_FRAMEWORK_ERROR); provision_output($url, $data); } $ops = array('pre_install', 'install', 'post_install'); //these are the provision hooks that will get triggered. if (_provision_drupal_site_installed($url)) { provision_set_error(PROVISION_SITE_INSTALLED); provision_log("error", t("Site has already been installed. Exiting.")); provision_output($url, $data); } foreach ($ops as $op ) { $func = "_provision_$op"; $func($url, $data); } provision_save_site_data($url, $data); provision_output($url, $data); } /** * Drush task. * * Calls hook_provision_pre_install(). * Also responsible for calling creating site directory layout, and the drupal settings file. * * @param url * The url of the site being invoked. * @param data * A reference to the associated array containing the data for the site. This needs to be a reference, * because the modules might provide additional information about the site. * @return * Boolean denoting whether the provision_invoke rolled back changes made. */ function _provision_pre_install($url, &$data) { $rolled_back = provision_invoke("pre_install", $url, $data); if (!provision_get_error()) { // This is the actual drupal provisioning requirements. _provision_drupal_create_directories($url, $data['profile']); // Requires at least the database settings to complete. _provision_drupal_create_settings_file($url, $data); } return $rolled_back; } /** * Install drupal site * * The magic here is that we need to drive the install api through this code. * At this point, we no longer have access to the central database, and we need to be able * to drive this code blind, without causing bad stuff to happen. * * Install profile gets triggered at the end of this code. * * @param url * The url of the site being invoked. * @param data * A reference to the associated array containing the data for the site. This needs to be a reference, * because the modules might provide additional information about the site. * @return * Boolean denoting whether the provision_invoke rolled back changes made. */ function _provision_install($url, &$data) { $rolled_back = provision_invoke("install", $url, $data); if (!$rolled_back) { _provision_drupal_switch_active_site($url); # Change headers and db info, also backs up _provision_drupal_force_load_modules($url); _provision_drupal_install_schema($url, $data['profile'], $data['language'], $data['client_email']); _provision_drupal_maintain_aliases($url, $data); # Create symlinks for site aliases (if any) _provision_drupal_force_load_modules(); _provision_drupal_switch_active_site(); # This *should* bring the site back to where we were before installing } return $rolled_back; } /** * Clean up after installation. * * Most notably give the web server the opportunity to recheck it's configuration files. * * @param url * The url of the site being installed. * @param data * A reference to the associated array containing the data for the site. This needs to be a reference, * because the modules might provide additional information about the site. * @return * Boolean denoting whether the provision_invoke rolled back changes made. */ function _provision_post_install($url, &$data) { $rolled_back = provision_invoke("post_install", $url, $data); if (!$rolled_back) { $data['installed'] = TRUE; } return $rolled_back; } /** * Regenerate the config files of an already running site. * * @param url * The url of the site being synched. * @return * Output of provision_output() function. * Will exit with a PROVISION_SITE_NOT_FOUND error if the site does not exist. */ function _provision_synch($url = NULL) { if (!$url) { global $args; _provision_generate_config($args['options']); provision_output(NULL, $args['options']); } else { if (!_provision_drupal_site_installed($url)) { provision_log("error", "Site has not been installed yet."); provision_set_error(PROVISION_SITE_NOT_FOUND); provision_output(); } $data = provision_get_site_data($url); // This is the actual drupal provisioning requirements. _provision_drupal_create_directories($url, $data['profile']); _provision_drupal_maintain_aliases($url, $data); $rolled_back = provision_invoke("synch", $url, $data); // Requires at least the database settings to complete. _provision_drupal_create_settings_file($url, $data); $data['installed'] = TRUE; provision_save_site_data($url, $data); provision_output($url, $data); } } /** * Generate a backup of the site using a site package. * * @param url * The url of the site being backed up. * @return * Output of provision_output() function. * Will exit with a PROVISION_SITE_NOT_FOUND error if the site does not exist. */ function _provision_backup($url) { if (!_provision_drupal_site_installed($url)) { provision_log("Error", "Site has not been installed yet."); provision_set_error(PROVISION_SITE_NOT_FOUND); provision_output($url, $data); } $data = provision_get_site_data($url); $args = func_get_args(); array_shift($args); $file = array_shift($args); _provision_backup_site($url, $data, $file); $data['installed'] = TRUE; provision_save_site_data($url, $data); provision_output($url, $data); } /** * Generate a backup tarbal for a site. */ function _provision_backup_site($url, &$data, $file = NULL) { // This is the actual drupal provisioning requirements. if (!is_dir(PROVISION_BACKUP_PATH)) { provision_log("Backup directory does not exist."); provision_set_error(PROVISION_PERM_ERROR); provision_output($url, $data); } if (is_file($file)) { provision_log("File specified already exists."); provision_set_error(PROVISION_PERM_ERROR); provision_output($url, $data); } $suggested = PROVISION_BACKUP_PATH ."/$url-". date("Y-m-d", mktime()) .".tar"; // Use format of mysite.com-2008-01-02, if already existing, add number. while (is_file($suggested .'.gz')) { $count++; $suggested = PROVISION_BACKUP_PATH ."/$url-". date("Y-m-d", mktime()) ."_$count.tar"; } $data['backup_file'] = (!empty($file)) ? ereg_replace('.gz$', '', $file) : $suggested; $rolled_back = provision_invoke("backup", $url, $data); provision_shell_exec("gzip %s", $data['backup_file']); $data['backup_file'] = $data['backup_file'] .'.gz'; } /** * Import a running Drupal site into a provisioned site. * * This is accomplished by inspecting the settings.php file and generating a site.php file. * * @param url * The url of the site being synched. * @return * Output of provision_output() function. * Will exit with a PROVISION_SITE_NOT_FOUND error if the site does not exist. */ function _provision_import($url = NULL) { $rolled_back = provision_invoke("import", $url, $data); provision_output($url, $data); } /** * Import a running Drupal site into a provisioned site. * * This is accomplished by inspecting the settings.php file and generating a site.php file. * * @param url * The url of the site being synched. * @return * Output of provision_output() function. * Will exit with a PROVISION_SITE_NOT_FOUND error if the site does not exist. */ function _provision_verify($url = '') { _provision_create_dir(PROVISION_CONFIG_PATH, t('Provision configuration'), 0700); _provision_create_dir(PROVISION_BACKUP_PATH, t('Web server configuration'), 0700); $data = array(); $rolled_back = provision_invoke("verify", $url, $data); if ($url) { provision_save_site_data($url, $data); } provision_output($url, $data); } /** * Restore command implementation * * This command when called will * 1. Make a backup of the current site, before modifications are made. * 2. Temporarily disable the site by causing apache to redirect to a help page. Restarting apache is required. * 3. Extract the backup that is being restored to to a temporary folder in the sites directory. * 4. Create a new database, belonging to the site's user, and switch it around with the current site's database. * 5. Import the old database and site.php details. * 6. Switch around the sites directory of the current site and the backup being restored. * 7. Regenerate configuration files. * 8. TODO: diagnostic to test that everything is ok? * 9. Remove the temporary redirect and restart apache so the previous site is available again. * 10. Remove the extranuous db and duplicate site directory. * * If at any time an error occurs, before step 9. It should reverse all the changes it has made, * and leave the current site directory and database in the right place, and remove all cruft that * was created by this process. */ function _provision_restore($url, $restore_file) { if (!($exists = _provision_drupal_site_installed($url))) { // this can probably be done more consistently with another // provision_path like function. provision_log("Error", "Site has not been installed yet."); provision_set_error(PROVISION_SITE_NOT_FOUND); } $exists &= provision_path("exists", $restore_file, TRUE, t("Restoring site from @path"), t("Could not find backup file @path"), PROVISION_FRAMEWORK_ERROR); if ($exists) { $data = provision_get_site_data($url); $data['restore_file'] = $restore_file; $phases = array("pre_restore", "restore", "post_restore"); $completed = array(); $rolled_back = FALSE; // initializes to FALSE. foreach ($phases as $phase) { $rolled_back = provision_invoke($phase, $url, $data, $rolled_back); if (!$rolled_back) { $completed[] = $phase; } else { break; // exit out of the loop, to allow any changes to be reversed next. } } if ($rolled_back) { // An error has occurred, and we must undo all the changes we made, in reverse // This works by triggering the _rollback functions for each of the hooks. foreach (array_reverse($completed) as $phase) { provision_invoke($phase, $url, $data, $rolled_back); } } else { provision_save_site_data($url, $data); } } provision_output($url, $data); } function provision_provision_pre_restore($url, &$data) { _provision_backup_site($url, $data); // Backup site for posterity, before rolling back. provision_path("extract", $data['restore_file'], PROVISION_SITES_PATH ."/$url.restore", t('Successfully extracted the contents of @path to @confirm'), t('Failed to extract the contents of @path to @confirm'), PROVISION_PERM_ERROR, PROVISION_FRAMEWORK_ERROR); } function provision_provision_pre_restore_rollback($url, $data) { _provision_recursive_delete(PROVISION_SITES_PATH ."/$url.restore"); } function provision_provision_post_restore($url, $data) { _provision_recursive_delete(PROVISION_SITES_PATH ."/$url.restore"); } function _provision_disable($url) { if (!_provision_drupal_site_installed($url)) { provision_log("Error", "Site has not been installed yet."); provision_set_error(PROVISION_SITE_NOT_FOUND); provision_output($url, $data); } $data = provision_get_site_data($url); _provision_backup_site($url, $data); # Backup site for posterity, before disabling $rolled_back = provision_invoke("disable", $url, $data); $data['enabled'] = FALSE; provision_save_site_data($url, $data); provision_output($url, $data); } function _provision_enable($url) { if (!_provision_drupal_site_installed($url)) { provision_log("Error", "Site has not been installed yet."); provision_set_error(PROVISION_SITE_NOT_FOUND); provision_output($url, $data); } $data = provision_get_site_data($url); $rolled_back = provision_invoke("enable", $url, $data); $data['enabled'] = TRUE; provision_save_site_data($url, $data); provision_output($url, $data); } function _provision_delete($url) { $data = provision_get_site_data($url); $args = func_get_args(); array_shift($args); $file = array_shift($args); _provision_backup_site($url, $data, $file); $rolled_back = provision_invoke("delete", $url, $data); provision_output($url, $data); } /** * Initial setup of platform * * Creates symlink to drush.php * Creates config path * Creates drushrc path * * This function is re-used by the hosting_setup command, as it is a superset of this functionality. */ function _provision_setup() { $success = TRUE; $drush_path = sprintf("%s/%s/drush.php", PROVISION_DOCROOT_PATH, drupal_get_path('module', 'drush')); $success &= provision_path('symlink', $drush_path, PROVISION_DOCROOT_PATH . '/drush.php', t('Created symlink for drush.php file'), t('Could not create symlink for drush.php'), PROVISION_FRAMEWORK_ERROR); $success &= _provision_generate_config(); return $success; } /** * Drush command wrapper for the setup of the platform */ function _provision_setup_cmd() { if (_provision_setup()) { variable_set('provision_setup', TRUE); } // @TODO use provision_output for this, but we need pretty print first. $logs = provision_get_log(); foreach ($logs as $log) { print "$log[message]\n"; } if (provision_get_error()) { print "\nThe command did not complete successfully, please fix the issues and re-run this script."; } } /** * Drush command to run cron */ function _provision_cron($url) { if (!_provision_drupal_site_installed($url)) { provision_log("Error", "Site has not been installed yet."); provision_set_error(PROVISION_SITE_NOT_FOUND); provision_output($url, $data); } $data = provision_get_site_data($url); _provision_drupal_switch_active_site($url); drupal_cron_run(); _provision_drupal_switch_active_site(); }