Skip to content
provision.module 26 KiB
Newer Older
/**
* @file
*   Provisioning Framework
*
* This module provides a framework for a Drupal site to manage and install new Drupal sites, using the command line
* Drush utility. 
*
* It allows for pluggable 'provisioning modules' that can extend and modify the tasks that are taken during installation.
*
* Each site has the following commands that can be run on it.
*
* Implemented : 
*   install - Install a new Drupal site. The install command uses 3 separate hooks to do it's job, 
*              namely hook_provision_pre_install(), hook_provision_install() and hook_provision_post_install()
*   synch   - Recreate all configuration files, to be in synch with changes in the front end.
*   stats   - Return an associated array of site statistics. (implemented in provision_stats module, is thus optional)
*   import  - Import the details of an already existing site into the provisioning framework.
*             This command inspects the settings.php and generates the site.php file that the framework uses for configuration.
*   backup  - Generates a tarball containing the sites directory, the site data configuration and the database dump.
*             This allows the tarball to act as a 'site package', which can be redeployed on other installations,
*             or used for an upgrade.
*   disable - Disable an installed Drupal site. Changes the virtual host config file so that it redirects to provision_disabled_site_redirect_url
*   enable  - Re-enable a site that has already been disabled. Recreates the virtual host file.
*   delete -  Generates a back up of the site, and then removes all references to it.
*   restore - Revert to a previous backup of the site.
*
* Not implemented yet :
*   upgrade - Accepts a site package (backup) as argument, and redeploys it, running the upgrade processes on it.
*             Uses hook_provision_pre_upgrade(), hook_provision_upgrade() and hook_provision_post_upgrade() hooks,
*             and allows clean roll back if any errors occur. Will include stringent checking of module versions,
*             and allow unit tests to be run.
*   rename  - Change the url of a site. This requires moving of files, and numerous other issues.
*/

  // Include the provisioning API. 
  include_once('provision.inc');
  include_once('provision.path.inc');
 *
 * 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() {
  // 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), '/'));

Adrian Rossouw's avatar
Adrian Rossouw committed
  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_TYPE', $db['scheme']);
  define('PROVISION_DB_USER', $db['user']);
  define('PROVISION_DB_PASSWD', $db['pass']);
  define('PROVISION_DB_HOST', $db['host']);
  # 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;
    }
  }
 * 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"));
  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, "<?php\n". provision_render_config($text, $data));
  fclose($fp);

  # Change the permissions of the file
  provision_path("chmod", "sites/default/provision.settings.php", 0400,
    t('Changed permissions of provision.settings.php to @confirm'),
    t('Could not change permissions of provision.settings.php to @confirm'));
  return TRUE;
}

/**
 * @defgroup provisionui Configure provisioning framework.
 * @{
 */

/**
 * Implementation of hook_menu().
 */
function provision_menu() {
  $items['admin/help/provision/requirements'] = array(
    'title' => 'Provisioning requirements',
    'description' => "Information of how to configure the provisioning system.",
    'page callback' => 'provision_help_requirements',
  $items['provision'] = array(
    'title' => 'Configure your platform',
    'description' => 'Configure your platform.',
    'page callback' => 'provision_front',
    'type' => MENU_CALLBACK,
    'access arguments' => TRUE,
  $items['provision/disabled'] = array(
    'title' => 'Site disabled',
    'description' => 'Page showed when a site is disabled.',
    'page callback' => 'provision_disabled_site',
    'access arguments' => array('access content'),
  $items['provision/maintenance'] = array(
    'title' => 'Site is undergoing maintenance.',
    'description' => 'Page shown when a site is being restored or moved etc.',
    'page callback' => 'provision_site_maintenance',
    'access arguments' => array('access content'),
  $items['provision/notfound'] = array(
    'title' => 'Site not found.',
    'description' => 'The site you have requested is not available.',
    'page callback' => 'provision_site_notfound',
    'type' => MENU_CALLBACK,
    'access arguments' => array('access content'),
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 "<p>This site was disabled by the site administrators.</p>";
}
function provision_site_maintenance() {
  drupal_set_breadcrumb(array());
  return "<p>This site is being worked on presently. Check back later.</p>";
}

function provision_site_notfound() {
  drupal_set_breadcrumb(array());
  return "<p>The site you have requested does not exist.</p>";
}

/**
 * @} 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).')),    
  $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.'
  );

  if (!function_exists('hosting_setup')) {
    $items['provision setup'] = array(
      'callback' => '_provision_setup_cmd',
      'description' => 'Initialize this platform to be able to create hosted sites.',
    );
  }

  $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);
  $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);

    _provision_drupal_switch_active_site($url); # Change headers and db info, also backs up
    _provision_drupal_force_load_modules($url);
    _provision_drupal_install_schema($data['profile'], $data['language'], $data['client_email']);
    _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) {
  }
  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']);
    $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);
  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')) {
    $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);


/**
 * 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);
    }
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);

  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);

  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.";
  }
}