summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--provision.inc417
-rw-r--r--provision.info4
-rw-r--r--provision.module364
-rw-r--r--provision_apache.info4
-rw-r--r--provision_apache.module114
-rw-r--r--provision_drupal.info4
-rw-r--r--provision_drupal.module288
-rw-r--r--provision_mysql.info4
-rw-r--r--provision_mysql.module101
-rw-r--r--provision_stats.info4
-rw-r--r--provision_stats.module40
11 files changed, 1344 insertions, 0 deletions
diff --git a/provision.inc b/provision.inc
new file mode 100644
index 0000000..4af71e7
--- /dev/null
+++ b/provision.inc
@@ -0,0 +1,417 @@
+<?php
+/**
+ * @file
+ * The provisioning framework API.
+ *
+ * API functions that are used by the provisioning framework to provide structure to the provisioning modules.
+ *
+ * @see errorhandling
+ * @see logging
+ * @see sitedata
+ * @see provisiontokens
+ */
+
+/**
+ * Invoke provision api calls.
+ *
+ * Call the correct hook for all the modules that implement it. We can not use Drupal's default module_invoke, because we
+ * can not pass references through it.
+ * Additionally, the ability to rollback when an error has been encountered is also provided.
+ * If at any point during execution, the provision_get_error() function returns anything but 0, provision_invoke will
+ * trigger $hook_rollback for each of the hooks that implement it, in reverse order from how they were executed.
+ *
+ * @param hook
+ * The hook name to be executed for all the modules.
+ * @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.
+ * @param rollback
+ * A boolean specifying whether or not the entire action needs to be rolled back.
+ * This is used specifically in commands which implement multiple hooks, such as 'install',
+ * which implements 'pre_install', 'install' and 'post_install' hooks.
+ * @return
+ * A boolean specifying whether or not any rollback has been performed.
+ *
+ */
+function provision_invoke($hook, $url, &$data, $rollback = false) {
+
+ if (!$rollback) {
+ foreach (module_implements("provision_$hook") as $name) {
+ $completed[] = $name;
+ $func = $name . "_provision_" . $hook;
+ $func($url, $data);
+ if (provision_get_error()) {
+ # As soon as an error occurs, roll back
+ $rollback = TRUE;
+ break;
+ }
+ }
+ }
+ else {
+ $completed = module_implements("provision_$hook");
+ }
+
+ if ($rollback) {
+ foreach (array_reverse($completed) as $name) {
+ $func = $name . "_" . $hook . '_rollback';
+ if (function_exists($func)) {
+ $func($url, $data);
+ provision_set_log("Rollback", "Changes for $name module have been rolled back.");
+ }
+ }
+ return FALSE;
+ }
+ else {
+ return TRUE;
+ }
+}
+
+/**
+ * Return output to the command line.
+ *
+ * Provides support for the -b/--backend flag to drush, which returns a serialized data structure.
+ * This feature is used for communication with the front end.
+ *
+ * @param url
+ * The url of the site being invoked.
+ * @param data
+ * The complete associative array containing all the aggregated site settings.
+ * @param extra
+ * An associative array containing additional data to be returned from the command. @see provision_stats_stats()
+ */
+function provision_output($url, $data, $extra = null) {
+ $return = $extra;
+ $return['site'] = $data;
+ $return['error_status'] = provision_get_error(); // error code being returned
+ $return['log'] = provision_get_log(); // Append logging information
+ $return['messages'] = drupal_get_messages();
+ if (drush_get_option(array('b', 'backend'), FALSE)) {
+ print serialize($return);
+ }
+ else {
+ foreach (provision_get_log() as $entry) {
+ printf("%10s|%10s|%10%s", $entry['timestamp'], $entry['type'], $entry['severity'], $entry['message']);
+ }
+ if ($data) {
+ /** TODO : return a cleanly formatted display of all the necessary information */
+ print_r($return);
+ }
+
+ }
+ exit(provision_get_error());
+}
+
+
+/**
+ * @defgroup errorhandling Managing errors that occur in the provisioning framework.
+ * @{
+ * Functions that manage the current error status of the provisioning framework.
+ *
+ * These functions operate by maintaining a static variable that is a bitmask of all the errors that have occurred.
+ * This bitmask value is returned at the end of program execution, and provide the hosting front end more information
+ * on how to diagnose any problems that may have occurred.
+ */
+
+/**
+* @name Error status definitions
+* @{
+* Bitmask values used to generate the error code to return.
+* @see provision_set_error(), provision_get_error(), provision_cmp_error()
+*/
+
+/** Database could not be accessed, or configured */
+define('PROVISION_DB_ERROR', 1);
+/** Drupal was unable to complete it's installation */
+define('PROVISION_INSTALL_ERROR', 2);
+/** Could not create files due to permission error - potentially less severe */
+define('PROVISION_PERM_ERROR', 4);
+/** Web server could not be restarted, or other server related issues - less severe */
+define('PROVISION_WEB_ERROR', 8);
+/** To be used while testing if the provision framework is actually working. */
+define('PROVISION_FRAMEWORK_ERROR', 16);
+/** When the site is not available on this platform, or has not been installed. */
+define('PROVISION_SITE_NOT_FOUND', 32);
+/** When the site is already installed, ie: there is a conflict. */
+define('PROVISION_SITE_INSTALLED', 64);
+
+/**
+ * @} End of "name Error status defintions".
+ */
+
+/**
+ * Set an error code for the error handling system.
+ *
+ * @param error_code
+ * Any of the defined error status definitions. A numerical bitmask value.
+ * @return
+ * The current aggregate error status
+ */
+function provision_set_error($error_code = 0) {
+ static $error = 0;
+
+ if ($error_code) {
+ $error = $error | (int) $error_code;
+ }
+
+ return $error;
+}
+
+/**
+ * Return the current error handling status
+ *
+ * @return
+ * The current aggregate error status
+ */
+function provision_get_error() {
+ return provision_set_error();
+}
+
+/**
+ * Check if a specific error status has been set.
+ *
+ * @param error
+ * Any of the defined error status definitions. A numerical bitmask value.
+ * @return
+ * TRUE if the specified error has been set, FALSE if not
+ */
+function provision_cmp_error($error) {
+ return provision_get_error() ^ $error;
+}
+
+/**
+ * @} End of "defgroup errorhandling".
+ */
+
+/**
+ * @defgroup logging Logging information to be provided as output.
+ * @{
+ * Functions that allow the provisioning framework to log messages to be provided to the front end.
+ *
+ * These functions are primarily for diagnostic purposes, but also provide an overview of actions that were taken
+ * by the framework during creation of a site.
+ */
+
+/**
+ * Maintain a static array containing all the log messages
+ *
+ * @param entry
+ * Associative array containing the log message.
+ * @return
+ * Entire log history, only if $entry is null
+ */
+function _provision_set_log($entry = null) {
+ static $log = array();
+ if ($entry == null) {
+ return $log;
+ }
+ else {
+ $log[] = $entry;
+ }
+}
+
+/**
+ * Add a log message to the log history.
+ *
+ * @param type
+ * The type of message to be logged. Common types are 'warning', 'error' and 'notice'.
+ * @param message
+ * String containing the message to be logged.
+ */
+function provision_log($type, $message) {
+ _provision_set_log(array(
+ 'type' => $type,
+ 'message' => $message,
+ 'timestamp' => time()
+ ));
+}
+
+/**
+ * Retrieve the log messages from the log history
+ *
+ * @return
+ * Entire log history
+ */
+function provision_get_log() {
+ return _provision_set_log();
+}
+
+/**
+ * @} End of "defgroup errorhandling".
+ */
+
+/**
+ * @defgroup sitedata Site data management utility functions.
+ * @{
+ * The provision framework maintains a site.php file in the sites directory, to maintain additional
+ * information from the front end, as well as providing a change history of setting changes.
+ *
+ * These functions load, save and manage changes made to the site data. This data has diagnostic and infrastructure
+ * values, that allow sites to be more easily moved between different provisioned platforms.
+ */
+
+/**
+ * Returns the aggregated site data from both the pre-existing site.php file, and the options passed to Drush
+ *
+ * This function merges the data from the command line parser, and the information already saved by previous invokations
+ * of the api. This provides a single view of all data relating to the site.
+ * This function also provides sensible defaults for some of the settings.
+ *
+ * @param url
+ * The url of the site being invoked.
+ * @return
+ * An associated array containing the relevant settings for the site.
+ */
+function provision_get_site_data($url) {
+ global $args;
+ #TODO: Accept serialized string via unix pipe.
+ foreach ($args['options'] as $key => $value) {
+ if (preg_match("/^site-/", $key)) {
+ $site_data[$key] = $value;
+ }
+ }
+ $site_data['site-url'] = $url;
+ $site_data['site-action-type'] = $args['commands'][1];
+ $docroot = variable_get('provision_root', '');
+ $site_data['site-document-root'] = ($docroot) ? $docroot . '/webroot' : $_SERVER['DOCUMENT_ROOT'];
+ $site_data['site-temporary-url'] = str_replace('.', '-', $url) . '.' . variable_get('provision_tempurl_base', $_SERVER['HTTP_HOST']);
+ $site_data['site-profile'] = ($site_data['site-profile']) ? $site_data['site-profile'] : variable_get('provision_default_profile', 'default');
+ $site_data['site-ip'] = variable_get('provision_apache_server_ip', '127.0.0.1');
+ $site_data['site-port'] = variable_get('provision_apache_server_ip', 80);
+ if ($old_data = provision_load_site_data($url)) {
+ # Merge previously saved data with the new data. This way, old parameters overwrite new ones.
+ $site_data = array_merge(provision_load_site_data($url), $site_data);
+ }
+ return $site_data;
+}
+
+/**
+ * Load site data stored in the site.php file for the specified site.
+ *
+ * @param url
+ * The url of the site being invoked
+ * @return
+ * If the file was found, an associative array of the data that was loaded. Otherwise returns FALSE.
+ */
+function provision_load_site_data($url) {
+ # Load the configuration data.
+ $conf_file = "sites/$url/site.php";
+ if (file_exists($conf_file) && is_readable($conf_file)) {
+ require_once($conf_file);
+ return (array) $data;
+ }
+ return false;
+}
+
+/**
+ * Save modified options to the site.php file
+ *
+ * @param url
+ * The url of the site being invoked
+ * @param data
+ * The complete data structure that has been created. Only settings that have been changed will be recorded.
+ */
+function provision_save_site_data($url, $data) {
+ global $args;
+
+ $old_data = provision_load_site_data($url);
+
+ $conf_file = "sites/$url/site.php";
+ $fp = fopen($conf_file, "w+"); # Append to the end of the config file.
+ if (!$fp) {
+ provision_log("error", "Site config file could not be written");
+ provision_set_error(PROVISION_PERM_ERROR);
+ }
+ else {
+ fwrite($fp, "<?php\n");
+ $action = array('id' => $data['action-id'],
+ 'timestamp' => mktime(),
+ 'action' => $data['site-action-type'],
+ 'success' => $data['success']);
+ $line = "\n\n\$actions[] = " . var_export($action, TRUE) . ';';
+ fwrite($fp, $line);
+
+ foreach ($data as $key => $value) {
+ if ($data[$key] != $old_data[$key]) {
+ $line = "\n\$data['$key'] = " . var_export($value, true) . ';';
+ fwrite($fp, $line);
+ }
+ }
+ fwrite($fp, "?>");
+ fclose($fp);
+ }
+}
+
+/**
+ * @} End of "defgroup sitedata".
+ */
+
+
+/**
+ * @defgroup provisiontokens Token replacement support for the provisioning framework
+ * @{
+ * The provisioning framwork makes use of the token module for value replacement in configuration file templates.
+ */
+
+
+/**
+ * Implementation of hook_token_values().
+ *
+ * @param type
+ * A flag indicating the class of substitution tokens to return
+ * information on.
+ * @return
+ * A keyed array listing the substitution tokens. Elements should be
+ * in the form of: $list[$type][$token] = $description
+ */
+function provision_token_values($type, $object = null) {
+ global $args;
+ switch ($type) {
+ case 'site':
+ $values = array_merge(provision_get_site_data($args['commands'][2]), $object) ;
+ break;
+ }
+ return (array) $values;
+
+}
+/**
+ * Implementation of hook_token_list().
+ *
+ * @param type
+ * A flag indicating the class of substitution tokens to return
+ * information on.
+ * @return
+ * A keyed array listing the substitution tokens. Elements should be
+ * in the form of: $list[$type][$token] = $description
+ */
+function provision_token_list($type = 'all') {
+ if ($type == 'site') {
+ /** TODO: Complete the token list to allow the front end to more easily edit the settings. */
+ $tokens['site']['site-url'] = t("The domain name used to access the site.");
+ $tokens['site']['site-db-type'] = t("");
+ $tokens['site']['site-db-username'] = t("");
+ $tokens['site']['site-db-password'] = t("");
+ $tokens['site']['site-db-name'] = t("");
+ $tokens['site']['site-profile'] = t("");
+ $tokens['site']['site-action-type'] = t("");
+ }
+ return $tokens;
+}
+
+/**
+ * @} End of "defgroup provisiontokens".
+ */
+
+/**
+ * Confirm that provision is running through Drush.
+ *
+ * This module requires a 'bootstrap' and 'configtest' feature, which is to be used during installation, to make sure that
+ * all the provisioning modules have the correct permissions / access to be able to do their jobs. This bootstrap process should
+ * be used when deploying new platforms (ie: new releases).
+ */
+
+function _provision_confirm_drush() {
+ #confirm that code is running through the command line.
+}
+
diff --git a/provision.info b/provision.info
new file mode 100644
index 0000000..11f0617
--- /dev/null
+++ b/provision.info
@@ -0,0 +1,4 @@
+name = Provision
+description = Allows for the automated provisioning of hosted Drupal sites, via the command line
+package = Provision
+dependencies = drush provision_drupal provision_mysql provision_apache token \ No newline at end of file
diff --git a/provision.module b/provision.module
new file mode 100644
index 0000000..24776fa
--- /dev/null
+++ b/provision.module
@@ -0,0 +1,364 @@
+<?php
+/**
+* @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 actions 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.
+*
+* Not implemented yet :
+* disable - Disable an installed Drupal site. This is done by removing the virtual host file, so that Apache no longer serves it.
+* enable - Re-enable a site that has already been disabled. Recreates the virtual host file.
+* 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.
+* 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.
+* rollback - Revert to a previous backup of the site.
+* rename - Change the url of a site. This requires moving of files, and numerous other issues.
+* delete - Generates a back up of the site, and then removes all references to it.
+*/
+
+
+/** Include the provisioning API. */
+include_once('provision.inc');
+
+/**
+ * Implementation of hook_perm().
+ */
+function hook_perm() {
+ return array("administer provisioning");
+}
+
+
+/**
+ * @defgroup provisionui Configure provisioning framework.
+ * @{
+ */
+
+/**
+ * Implementation of hook_menu().
+ */
+function provision_menu($items, $may_cache = true) {
+ if ($may_cache) {
+ $items[] = array(
+ 'path' => 'admin/settings/provision',
+ 'title' => t('Provisioning'),
+ 'description' => t("Configure how new Drupal sites will be provisioned."),
+ 'callback' => 'drupal_get_form',
+ 'callback arguments' => array('provision_configure'),
+ 'access' => user_access('administer provisioning'),
+ );
+ }
+ return $items;
+}
+
+/**
+ * Menu callback.
+ *
+ * Configuration screen for the provisioning framework.
+ */
+function provision_configure() {
+
+ $form['provision_root'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Provision root'),
+ '#description' => t('The path where the provision platform is based.'),
+ '#default_value' => variable_get('provision_root', ereg_replace("/webroot$", "", $_SERVER['DOCUMENT_ROOT'])),
+ '#size' => 40,
+ '#maxlength' => 255,
+ );
+
+ $form['provision_tempurl_base'] = array(
+ '#type' => 'textfield',
+ '#required' => TRUE,
+ '#title' => t('Temporary URL base'),
+ '#description' => t('Each new domain that gets created gets created with a temporary url base that allows it to be immediately accessible.'),
+ '#default_value' => variable_get('provision_tempurl_base', $_SERVER['HTTP_HOST']),
+ '#size' => 40,
+ '#maxlength' => 255,
+ );
+
+ $form['provision_user'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Provision user'),
+ '#description' => t('The owner of the files. must not be the web server user.'),
+ '#default_value' => variable_get('provision_user', 'hosting'),
+ '#size' => 40,
+ '#maxlength' => 255,
+ );
+
+ $form['provision_group'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Provision group'),
+ '#description' => t('The group owner of the files. should be the group the web server is running as.'),
+ '#default_value' => variable_get('provision_group', 'apache'),
+ '#size' => 40,
+ '#maxlength' => 255,
+ );
+
+ foreach (module_implements('provision_configure') as $module) {
+ $form[$module] = array(
+ '#type' => 'fieldset',
+ '#title' => module_invoke($module, 'provision_service'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ '#access' => user_access('administer provisioning'),
+ );
+ $form[$module] = array_merge($form[$module], module_invoke($module, "provision_configure"));
+ }
+ return system_settings_form($form);
+}
+
+/**
+ * @} 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',
+ 'description' => 'Provision a new site using the provided data.'
+ );
+
+ $items['provision synch'] = array(
+ 'callback' => '_provision_synch',
+ 'description' => 'Regenerate all the config files for a site.'
+ );
+ $items['provision import'] = array(
+ 'callback' => '_provision_import',
+ 'description' => 'Turn an already running site into a provisioned site.'
+ );
+ /*
+ $items['provision enable'] = array(
+ 'callback' => '_provision_enable',
+ 'description' => 'Enable a disabled site.'
+ );
+ $items['provision disable'] = array(
+ 'callback' => '_provision_disable',
+ 'description' => 'Disable a site.'
+ );
+ // Not implemented yet.
+ $items['provision delete'] = array(
+ 'callback' => '_provision_delete',
+ 'description' => 'Delete a site.'
+ );
+
+ $items['provision backup'] = array(
+ 'callback' => '_provision_backup',
+ 'description' => 'Generate a back up for the site.'
+ );
+ $items['provision rollback'] = array(
+ 'callback' => '_provision_rollback',
+ 'description' => 'Roll back the site to a previous backup.'
+ );
+ $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_set_log("error", "Incorrect usage of the provisioning framework");
+ drupal_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 ($data['site-installed'] == TRUE) {
+ print "The site $url is already installed.\nIf you want to re-install it, you will need to delete it first.";
+ drupal_set_error(PROVISION_SITE_INSTALLED);
+ 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 action.
+ *
+ * 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($data['site-profile']);
+ _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['site-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 drupal_output() function.
+ * Will exit with a PROVISION_SITE_NOT_FOUND error if the site does not exist.
+ */
+function _provision_synch($url) {
+ if (!_provision_drupal_site_exists($url)) {
+ provision_log("Error", "Site has not been installed yet.");
+ drupal_set_error(PROVISION_SITE_NOT_FOUND);
+ drupal_output($url, $data);
+ }
+ $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);
+ provision_save_site_data($url, $data);
+ drupal_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 drupal_output() function.
+ * Will exit with a PROVISION_SITE_NOT_FOUND error if the site does not exist.
+ */
+function _provision_import($url) {
+ if (!_provision_drupal_site_exists($url)) {
+ provision_log("Error", "Site directory is not present, and can not be imported.");
+ drupal_set_error(PROVISION_SITE_NOT_FOUND);
+ drupal_output($url, $data);
+ }
+ $data = provision_get_site_data($url);
+
+ include_once("sites/$url/settings.php");
+
+ $parts = parse_url($db_url);
+ $data['site-db-type'] = $parts['scheme'];
+ $data['site-db-user'] = $parts['user'];
+ $data['site-db-host'] = $parts['host'];
+ $data['site-db-passwd'] = $parts['pass'];
+ $data['site-db-name'] = substr($parts['path'], 1);
+ $data['site-profile'] = $installed_profile;
+
+ provision_save_site_data($url, $data);
+ provision_output($url, $data);
+}
diff --git a/provision_apache.info b/provision_apache.info
new file mode 100644
index 0000000..9d2b19e
--- /dev/null
+++ b/provision_apache.info
@@ -0,0 +1,4 @@
+name = Provision: Apache
+description = Provides provisioning requirements for the Apache web server
+package = Provision
+dependencies = provision
diff --git a/provision_apache.module b/provision_apache.module
new file mode 100644
index 0000000..f82ccac
--- /dev/null
+++ b/provision_apache.module
@@ -0,0 +1,114 @@
+<?php
+/**
+ * @file
+ * Apache provisioning module
+ * This module simply serves to generate the virtual host entry, and make sure apache gets reloaded properly.
+ * Because Drupal is running via the command line for the entirety of this process, it is only necessary to make
+ * it available online once everything has been completed.
+ *
+ * This module still requires configuration and sanity checks. Need to figure out a way to inspect the apache configuration,
+ * to ensure that the sites are getting loaded up correctly.
+ */
+
+function provision_apache_provision_service() {
+ return t("Apache webserver");
+}
+
+/**
+ * Hook into central configuration form for provisioning framework.
+ */
+function provision_apache_provision_configure() {
+
+ $form['provision_apache_vhost_template'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Virtual Host configuration template'),
+ '#description' => t('The text to use when generating a virtual host configuration file for apache'),
+ '#default_value' => variable_get('provision_apache_vhost_template', _provision_apache_default_template()),
+ '#cols' => 60,
+ '#rows' => 5,
+ );
+
+ $default_path = variable_get('provision_root', ereg_replace("/webroot$", "", $_SERVER['DOCUMENT_ROOT'])) . '/vhost.d';
+ $form['provision_apache_vhost_path'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Path to the directory to store apache configuration files for hosted sites'),
+ '#size' => 40,
+ '#value' => variable_get('provision_apache_vhost_path', $default_path),
+ '#maxlength' => 255,
+ );
+ $form['provision_apache_restart_cmd'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Apache restart command'),
+ '#description' => t('The command to run to restart apache for new changes to take effect. This is required for the new site to become live'),
+ '#size' => 40,
+ '#maxlength' => 255,
+ '#default_value' => variable_get('provision_apache_restart_cmd', 'sudo apachectl graceful')
+ );
+
+ return $form;
+}
+function _provision_apache_default_template() {
+ return <<<EOF
+ <VirtualHost *:80>
+ ServerAdmin [site-email]
+ DocumentRoot [site-document-root]
+ ServerName [site-url]
+ ServerAlias [site-temporary-url]
+ ServerAlias www.[site-url]
+
+ # Error handler for Drupal > 4.6.7
+ <Directory "[site-document-root]/sites/[site-url]/files">
+ SetHandler This_is_a_Drupal_security_line_do_not_remove
+ </Directory>
+
+ </VirtualHost>
+EOF;
+}
+function provision_apache_provision_pre_install($url, &$data) {
+ return _provision_apache_create_vhost_config($url, $data);
+}
+function provision_apache_provision_post_install($url, &$data) {
+ return _provision_apache_restart_apache();
+}
+
+function provision_apache_provision_enable($url, &$data) {
+ _provision_apache_create_vhost_config($url, $data);
+ _provision_apache_restart_apache();
+}
+
+function provision_apache_provision_regenerate($url, &$data) {
+ _provision_apache_create_vhost_config($url, $data);
+ _provision_apache_restart_apache();
+}
+
+function _provision_apache_delete_vhost_config($url, $data) {
+ $vhost_path = variable_get('provision_apache_vhost_path', 'vhost.d');
+ if (file_exists()) {
+ unlink($vhost_path . '/' . $url);
+ }
+}
+
+function _provision_apache_create_vhost_config($url, $data) {
+ $vhost_path = variable_get('provision_apache_vhost_path', 'vhost.d');
+ $file = fopen($vhost_path . '/' . $url, "w");
+ if (!$file) {
+ provision_log("error", "Could not create apache configuration file.");
+ provision_set_error(PROVISION_WEB_ERROR | PROVISION_PERM_ERROR);
+ return false;
+ }
+ $text = token_replace(variable_get('provision_apache_vhost_template', _provision_apache_default_template()) , 'site', $data);
+
+ fwrite($file, $text);
+ fclose($file);
+}
+
+function _provision_apache_restart_apache() {
+ # This is required to be configurable, due to the fact that different hosts might need to do this differently.
+ # TODO : add configuration / test for this
+ $apache_restart_cmd = escapeshellcmd(variable_get('provision_apache_restart_cmd', 'sudo apachectl graceful'));
+ $code = drush_shell_exec($apache_restart_cmd);
+ if ($code) {
+ provision_set_error(PROVISION_WEB_ERROR);
+ provision_log("error", "Web server could not be restarted. Changes might not be available until this has been done.");
+ }
+} \ No newline at end of file
diff --git a/provision_drupal.info b/provision_drupal.info
new file mode 100644
index 0000000..8ff52e9
--- /dev/null
+++ b/provision_drupal.info
@@ -0,0 +1,4 @@
+name = Provision: Drupal
+description = Allows for Drupal sites to be provisioned using the provisioning framework.
+package = Provision
+dependencies = drush provision_drupal provision_mysql provision_apache \ No newline at end of file
diff --git a/provision_drupal.module b/provision_drupal.module
new file mode 100644
index 0000000..1526ba6
--- /dev/null
+++ b/provision_drupal.module
@@ -0,0 +1,288 @@
+<?php
+/**
+ * @file
+ * Drupal specific functions for the provisioning framework.
+ *
+ * This module is responsible for the creation and maintenance of the drupal settings.php file, the sites directory structure
+ * and all the install api code.
+ */
+
+/**
+ * @ingroup provisionui
+ * @{
+ */
+
+/**
+ * Implementation of hook_provision_service()
+ */
+function provision_drupal_provision_service() {
+ return t("Drupal sites");
+}
+
+/**
+ * Implentation of hook_provision_configure()
+ */
+function provision_drupal_provision_configure() {
+ $template = _provision_default_template();
+ $profiles = file_scan_directory('./profiles', '\.profile$', array('.', '..', 'CVS'), 0, TRUE, 'name', 0);
+ // Don't need to choose profile if only one available.
+ if (sizeof($profiles) == 1) {
+ $profile = array_pop($profiles);
+ $form['provision_default_profile'] = array('#type' => 'value', '#value' => $profile->name);
+ }
+ elseif (sizeof($profiles) > 1) {
+ foreach ($profiles as $profile) {
+ if ($_POST['profile'] == $profile->name) {
+ $options[$profile->name] = $profile->name;
+ }
+ }
+ $form['provision_default_profile'] = array(
+ '#type' => 'radios',
+ '#title' => t('Default install profile'),
+ '#description' => t('New sites will be created with the following install profile'),
+ '#options' => $options,
+ '#default_value' => variable_get('provision_default_profile', 'default'),
+ );
+
+ }
+
+ $form['provision_settings_template'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Drupal settings template'),
+ '#description' => t('The template for the generated settings.php file.'),
+ '#default_value' => variable_get('provision_settings_template', $template),
+ '#cols' => 60,
+ '#rows' => 5,
+ );
+
+ return $form;
+}
+
+/**
+ * @} End "ingroup provisionui"
+*/
+
+/**
+ * Test to see if the site settings.php exists
+ *
+ * @param url
+ * The url of the site to check
+ * @return
+ * If the file exists, return TRUE, else return FALSE.
+ */
+function _provision_drupal_site_exists($url) {
+ return file_exists("sites/$url/settings.php");
+}
+
+/**
+ * The default template to use while generating config files.
+ *
+ * @return
+ * The default template for the config file
+ */
+function _provision_drupal_default_template() {
+ return <<<END
+ <?php
+ \$db_url = "[site-db-type]://[site-db-username]:[site-db-passwd]@[site-db-host]/[site-db-name]";
+ \$profile = "[site-profile]";
+
+ # Additional host wide configuration settings. Useful for safely specifying configuration settings.
+ if (file_exists('includes/global.inc')) {
+ include_once('includes/global.inc');
+ }
+ ?>
+END;
+}
+
+/**
+ * Generate a settings file for the site.
+ *
+ * @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.
+ */
+function _provision_drupal_create_settings_file($url, &$data) {
+ $fp = fopen("sites/$url/settings.php", "w");
+ $text = variable_get('provision_settings_template', _provision_default_template());
+ fwrite($fp, token_replace($text, 'site', $data));
+ fclose($fp);
+
+ # Change the ownership of the file
+ #chown($path, variable_get('provision_user', 'hosting'), variable_get('provision_group', 'apache'));
+
+ # Change the permissions of the file
+# system("chmod 0550 sites/$url/settings.php");
+ # TODO: add md5 of the file created to $data
+}
+
+/**
+ * Create the directories needed to host a drupal site
+ *
+ * Also maintains permissions on existing directories.
+ */
+function _provision_drupal_create_directories($url, $profile = null) {
+ $paths = array(
+ "sites/$url" => '0750',
+ "sites/$url/files" => '2750',
+ "sites/$url/files/tmp" => '2770',
+ "sites/$url/files/images" => '2770',
+ "sites/$url/files/pictures" => '2770',
+ "sites/$url/themes" => '2750',
+ "sites/$url/modules" => '2750',
+ );
+
+ foreach ($paths as $path => $perm) {
+ if (!is_dir($path)) {
+ mkdir($path);
+ }
+ # Change the ownership of the files so that they are owned by the user the script is running as, and the
+ # web server has access to them.
+ # chown($path, variable_get('provision_user', 'hosting'), variable_get('provision_group', 'apache'));
+
+ # Change the permissions to the optimal settings that were specified.
+# system("chmod $perm $path");
+
+ }
+
+}
+
+/**
+ * Switch the active database to the newly created database
+ *
+ * This function tricks Drupal into thinking that it's running on an uninstalled site,
+ * so that it can cleanly install the database schema. It also handles switching back to the
+ * main provisioning site.
+ */
+function _provision_drupal_switch_active_site($url = null) {
+ static $backups;
+ if ($url) {
+ /* Pretend to be the site being installed */
+
+ // Fake the necessary HTTP headers that Drupal needs:
+ $drupal_base_url = parse_url($url);
+ $_SERVER['HTTP_HOST'] = $drupal_base_url['host'];
+ $_SERVER['PHP_SELF'] = $drupal_base_url['path'].'/index.php';
+ $_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'] = $_SERVER['PHP_SELF'];
+ $_SERVER['REMOTE_ADDR'] = NULL;
+ $_SERVER['REQUEST_METHOD'] = NULL;
+
+
+ /**
+ * This code is sourced from bootstrap.inc. I am trying to avoid patching core, but it might
+ * be smarter to make a small patch to allow core to re-initialize itself more easily
+ */
+
+ // Export the following settings.php variables to the global namespace
+ global $base_url, $base_path, $base_root;
+ global $db_url, $db_prefix, $cookie_domain, $conf, $installed_profile, $active_db, $profile;
+
+ # This is just for backup, to be able to restore to the old DRUSH system.
+ $backups = compact("active_db", "base_url", "base_path", "db_url", "db_prefix", "cookie_domain", "conf", "installed_profile", "profile");
+
+ $conf = array();
+ include_once $_SERVER['DOCUMENT_ROOT'] .'sites/' . $url . '/settings.php';
+
+ // Create base URL
+ $base_root = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http';
+ $base_url = $base_root .= '://'. preg_replace('/[^a-z0-9-:._]/i', '', $_SERVER['HTTP_HOST']);
+ if ($dir = trim(dirname($_SERVER['SCRIPT_NAME']), '\,/')) {
+ $base_path = "/$dir";
+ $base_url .= $base_path;
+ $base_path .= '/';
+ }
+ else {
+ $base_path = '/';
+ }
+
+ unset($active_db);
+ $db_url['new'] = $db_url;
+ db_set_active('new');
+
+ }
+ else {
+ /**
+ * Restore everything to the way it was before we switched sites.
+ */
+ // Fake the necessary HTTP headers that Drupal needs:
+ $drupal_base_url = parse_url(DRUSH_URI);
+ $_SERVER['HTTP_HOST'] = $drupal_base_url['host'];
+ $_SERVER['PHP_SELF'] = $drupal_base_url['path'].'/index.php';
+ $_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'] = $_SERVER['PHP_SELF'];
+ $_SERVER['REMOTE_ADDR'] = NULL;
+ $_SERVER['REQUEST_METHOD'] = NULL;
+
+ global $base_url, $base_path, $base_root;
+ // Export the following settings.php variables to the global namespace
+ global $db_url, $db_prefix, $cookie_domain, $conf, $installed_profile, $profile;
+
+ # This is just for backup, to be able to restore to the old DRUSH system.
+ extract($backups, EXTR_OVERWRITE);
+
+ }
+}
+
+/**
+ * Force drupal to load the modules it expects to find on an uninstalled site
+ */
+function _provision_drupal_force_load_modules($url = null) {
+ static $backup_list;
+ if ($url) {
+ $backup_list = module_list();
+ require_once './modules/system/system.install';
+ require_once './includes/file.inc';
+ require_once './includes/install.inc';
+ // Load module basics (needed for hook invokes).
+ include_once './includes/module.inc';
+ $module_list['system']['filename'] = 'modules/system/system.module';
+ $module_list['filter']['filename'] = 'modules/filter/filter.module';
+ module_list(TRUE, FALSE, FALSE, $module_list);
+ drupal_load('module', 'system');
+ drupal_load('module', 'filter');
+ #should i load all the other modules? i don't know .=\
+ }
+ else {
+ module_list(TRUE, FALSE, FALSE, $backup_list);
+ }
+}
+
+/**
+ * Install the drupal schema and install profile
+ */
+function _provision_install_schema($profile) {
+ // Load the profile.
+ require_once "./profiles/$profile/$profile.profile";
+
+ $requirements = drupal_check_profile($profile);
+ $severity = drupal_requirements_severity($requirements);
+
+ // If there are issues, report them.
+ if ($severity == REQUIREMENT_ERROR) {
+ foreach ($requirements as $requirement) {
+ if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) {
+ drupal_set_message($requirement['description'] .' ('. st('Currently using !item !version', array('!item' => $requirement['title'], '!version' => $requirement['value'])) .')', 'error');
+ }
+ }
+
+ return false;
+ }
+
+ // Verify existence of all required modules.
+ $modules = drupal_verify_profile($profile, null);
+
+ if (!$modules) {
+ return false;
+ }
+
+ // Perform actual installation defined in the profile.
+ drupal_install_profile($profile, $modules);
+
+ // Show profile finalization info.
+ $function = $profile .'_profile_final';
+ if (function_exists($function)) {
+ // More steps required
+ $profile_message = $function();
+ }
+}
+
diff --git a/provision_mysql.info b/provision_mysql.info
new file mode 100644
index 0000000..80d92c8
--- /dev/null
+++ b/provision_mysql.info
@@ -0,0 +1,4 @@
+name = Provision: Mysql
+description = Provides provisioning requirements for the Mysql database
+package = Provision
+dependencies = provision
diff --git a/provision_mysql.module b/provision_mysql.module
new file mode 100644
index 0000000..538050c
--- /dev/null
+++ b/provision_mysql.module
@@ -0,0 +1,101 @@
+<?php
+/**
+ * @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.
+ */
+
+/**
+ * @ingroup provisionui
+ * @{
+ */
+
+/**
+ * Implementation of provision_service()
+ */
+function provision_mysql_provision_service() {
+ return t("Mysql database server");
+}
+
+/**
+ * Implementation of provision_configure
+ */
+function provision_mysql_provision_configure() {
+ $form['provision_mysql_user'] = array(
+ '#type' => 'textfield',
+ '#required' => TRUE,
+ '#title' => t('Mysql user account'),
+ '#description' => t('The user that will be used to create users and databases for new sites.'),
+ '#size' => 40,
+ '#default_value' => variable_get('provision_mysql_user', 'root'),
+ '#maxlength' => 255,
+ );
+ $form['provision_mysql_password'] = array(
+ '#type' => 'password',
+ '#required' => TRUE,
+ '#title' => t('Mysql user password'),
+ '#description' => t('The user account that will be used to create new mysql users and databases for new sites'),
+ '#size' => 30,
+ '#maxlength' => 64,
+ );
+
+ $form['provision_mysql_host'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Mysql server hostname'),
+ '#description' => t('The mysql server to connect to.'),
+ '#size' => 30,
+ '#default_value' => variable_get('provision_mysql_host', 'localhost'),
+ '#maxlength' => 64,
+ );
+ return $form;
+}
+/**
+ * @} end "ingroup provisionui"
+ */
+
+
+function provision_mysql_provision_pre_install($url, &$data) {
+ $data['site-db-type'] = 'mysql'; # only support innodb. for now.
+ $data['site-db-host'] = ($data['site-db-host']) ? $data['site-db-host'] : variable_get('provision_mysql_host', 'localhost');
+ $data['site-db-passwd'] = user_password(); # generate a random password for use
+ if ($data['site_id']) {
+ $data['site-db-name'] = 'site_' . $data['site_id'];
+ $data['site-db-username'] = $data['site-db-name']; // mysql has some really really stupid rules about who db / usernames, so site id is the safest.
+ }
+ else {
+ $data['site-db-name'] = substr(ereg_replace("^www\.", "", str_replace(".", "", $url)), 0, 10);
+ $data['site-db-username'] = $data['site-db-name'];
+ // TODO : A reasonable fallback if the site id isn't available. This is going to make it a bit harder to test at first, but that's ok.
+ }
+
+ # For this to work, the user account the provisioning site has been set up with, requires CREATE database permissions.
+ # TODO : Add additional configuration for a database account to use for these , but this is the quickest way to get the code up and running.
+ $db_url = sprintf("mysqli://%s:%s@%s/mysql", variable_get('provision_mysql_user', 'root'), variable_get('provision_mysql_password', 'root'), $data['site-db-host'] );
+
+ if ( db_result(db_query("SHOW DATABASES LIKE '%s'", $data['site-db-name'])) ) {
+ db_query("DROP DATABASE %s", $data['site-db-name']);
+ }
+
+ db_query("CREATE DATABASE %s", $data['site-db-name']);
+
+ if ( !db_result(db_query("SHOW DATABASES LIKE '%s'", $data['site-db-name'])) ) {
+ provision_set_error(PROVISION_DB_ERROR);
+ provision_log("error", "Database could not be created.");
+
+ return FALSE;
+ }
+
+ db_query("GRANT ALL PRIVILEGES ON %s.* TO %s@`%%` IDENTIFIED BY '%s'", $data['site-db-name'], $data['site-db-username'], $data['mysql_passwd']);
+ db_query("GRANT ALL PRIVILEGES ON %s.* TO %s@%s IDENTIFIED BY '%s'",$data['site-db-name'], $data['site-db-username'], $data['site-db-host'], $data['mysql_passwd']);
+
+
+ if ($data['site-mysql-old-passwords']) {
+ db_query("SET PASSWORD FOR '%s'@'%%' = OLD_PASSWORD('%s')", $data['site-db-username'], $data['site-db-passwd']);
+ db_query("SET PASSWORD FOR %s@%s = OLD_PASSWORD('%s')", $data['site-db-username'], $data['site-db-host'], $data['site-db-passwd']);
+ }
+ db_query("FLUSH PRIVILEGES");
+ #TODO : Test to confirm that the database is actually writeable. Taking this on faith for now.
+}
+
diff --git a/provision_stats.info b/provision_stats.info
new file mode 100644
index 0000000..f7f15d7
--- /dev/null
+++ b/provision_stats.info
@@ -0,0 +1,4 @@
+name = Provision: Statistics
+description = Generate statistics from a running Drupal site.
+package = Provision
+dependencies = drush \ No newline at end of file
diff --git a/provision_stats.module b/provision_stats.module
new file mode 100644
index 0000000..8ccbe84
--- /dev/null
+++ b/provision_stats.module
@@ -0,0 +1,40 @@
+<?php
+
+function provision_stats_drush_command() {
+ $items['provision stats'] = array(
+ 'callback' => '_provision_stats',
+ 'description' => 'Return statistics from a running site.'
+ );
+ return $items;
+}
+
+function _provision_stats($url) {
+ $data = provision_get_site_data($url);
+ if (!$data['site-installed']) {
+ print t('The site %site has not been installed yet.', array('%site' => $url));
+ exit(PROVISION_FRAMEWORK_ERROR); #exit with error, so front end can catch it
+ }
+ #needs to be done on active database
+ $modules = module_implements('provision_stats');
+ drupal_get_messages(); # clear the messages being saved so far.
+
+ # Change headers and db info, also backs up to restore later
+ _provision_drupal_switch_active_site($url);
+
+ #TODO: add some required modules here, possibly update_status if not enabled.
+ #Load the modules from the hosted site
+ module_list(TRUE);
+
+ $stats['node_count'] = db_result(db_query("select max(nid) from {node}"));
+ $stats['user_count'] = db_result(db_query("select max(uid) from {users}"));
+
+ foreach ($modules as $name) {
+ $func = $name . "_provision_stats";
+ $stats = array_merge($stats, $func($url, $data));
+ }
+
+ _provision_switch_active_site();
+ module_list(TRUE);
+
+ return provision_output($url, $data, array('stats' => $stats));
+} \ No newline at end of file