Author image
Senior Developer

Storing data in Aegir

Aegir is a very clever Drupal hosting system built using Drupal and Drush. It is divided into two parts: the frontend and the backend. The frontend is essentially just a standard Drupal site that stores its data in the database and then some drush scripts that manipulate the data. The backend (provision) is just a collection of drush scripts, and it stores its data in Aegir contexts which are essentially just arrays of data stored in text files on disk. One of the most mysterious processes in Aegir is sending data from the frontend to the backend to be stored in these contexts. Recently I cracked this mystery, and I'm going to explain how.

The problem

I wanted to add a couple of simple lines to the auto-generated settings.php and virtual host files, these needed to be based on a value defined in the frontend, and stored against particular sites. I imagine this is probably quite a frequent requirement.

Mig5 has written a tutorial that does something similar to this, but the settings are lost whenever the backend does something on its own, such as migrating or cloning a site. This is because the approach that mig5 took relies on the frontend telling the backend the particular options every time a command is run on the backend, and if the backend triggers a command all by itself, the frontend isn't going to be able let it know the extra data in the options. This happens everytime a clone or migrate happens.

Contexts and services

If I can get my data into a provison context, then I'll be able to access it any time a settings.php or vhost file is being written, and I can add the extra lines I want. There is one and only one way to ensure that additional data is stored in a provison context file: services.

A service in Aegir is a couple of classes that essentially define some resource on a server, like a HTTP server, or DB server. Services are tied to servers, which means that Aegir makes it really easy to say which of your servers have particular services available, and then it makes it easy to use those services when working with sites in the backend, for example, it's quite easy when creating a new site to ask the DB service to create a new database for the site.

Back to front

When writing a service from scratch its much easier to start in the backend, then write the frontend component, though obviously some tweaking will occur in both at the same time.

My service is going to be very simple indeed, and I'm going to call it the 'subfolder' service, and it's not even going to have an implementation, but that won't stop me from using it to store the data.

I started by adding a folder: .drush/provision/subfolder to hold the code for my service. These path names are hard coded in Aegir at the moment, so it's not the easiest to extend modularly unfortunately. The real meat of the backend code is stored in .drush/provision/subfolder/subfolder.drush.inc:

/**
* @file
*   The subfolder provision service.
*/

include_once(dirname(__FILE__) . '/../provision.service.inc');

/**
* Expose the service type this extension defines to provision.
*
* @return
*   An array with the service type the key, and the default implementation the value.
*/
function subfolder_provision_services() {
  return array('subfolder' => NULL);
}

/**
* The subfolder service base class.
*/
class provisionService_subfolder extends provisionService {
  public $service = 'subfolder';

  /**
   * Add the subfolder_path property to the site context.
   */
  static function subscribe_site($context) {
    $context->setProperty('subfolder_path');
  }
}

subfolder_provision_services() just declares the service type to provision, and then there's a very simple class that defines the service type itself. The important bit here is the static method: subscribe_site(), this method is called when provision is initialising a site context and we call the setProperty method on the $context object, passing in a value of subfolder_path. This is the key line, this means that if we send the subfolder_path option from the frontend to the backend, instead of it disappearing into oblivion it will get saved into the file the context is stored in.

Now I just need to use the data in the context, to get the correct lines in the settings.php and the vhost file, so to the same file, I added the following:

/**
* Implementation of hook_provision_apache_vhost_config().
*
* Add instructions for mod_rewrite to remove the subfolder path.
* This will result in the following lines in virtual host files:
*   RewriteEngine On
*   RewriteOptions inherit
*   RewriteRule ^/SUBFOLDER(/?)$ /index.php
*   RewriteRule ^/SUBFOLDER(/.*)$ $1
*/
function subfolder_provision_apache_vhost_config($uri, $data) {
  $lines = array();
  if (d()->type == 'site') {
    if (!empty(d()->subfolder_path)) {
      $lines[] = 'RewriteEngine On';
      $lines[] = 'RewriteOptions inherit';
      $lines[] = 'RewriteRule ^/' . d()->subfolder_path . '(/?)$ /index.php';
      $lines[] = 'RewriteRule ^/' . d()->subfolder_path . '(/.*)$ $1';
    }
  }

  return implode($lines, "\n");
}

/**
* Implementation of hook_provision_drupal_config().
*
* Add the subfolder to the $base_url because Drupal won't pick it up
* automatically.
* This will result in the following code in the settings.php:
*   $base_url = 'http://site-url.domain/SUBFOLDER';
*/
function subfolder_provision_drupal_config($uri, $data) {
  $lines = array();
  if (d()->type == 'site') {
    if (!empty(d()->subfolder_path)) {
      $lines[] = '// Fix the basepath to include the subfolder structure.';
      $lines[] = '$base_url = \'http://' . $uri . '/' . d()->subfolder_path . '\';';

    }
  }
  return implode($lines, "\n");
}

You can see here that I'm able to access the property on the context by just doing d()->subfolder_path, nice!

The frontend

I now need a simple Drupal module in the frontend, my cm_subfolders.info file just contains:

name = Hosting subfolders
description = A hacky way to get sites to use subfolders.
package = Hosting
dependencies[] = hosting
core = 6.x

Because my service is so simple, I don't even need to surface it to the frontend in Aegir, so my cm_subfolders.module just contains the code to the load and store the subfolder path from the variable table. Note that you should store data in your own database tables really, and not put random stuff in the variables table, but to cut down on the amount of code, that's what I've gone for here.

/**
* Implementation of hook_form_alter().
*
* Add a simple form element to specify the subfolder.
*/
function cm_subdirectories_form_alter(&$form, $form_state, $form_id) {
  if ($form_id == 'site_node_form') {
    $node = $form['#node'];

    _hosting_site_field($form, $node, 'subfolder_path', array(
        '#type' => 'textfield',
        '#title' => t('Subfolder path'),
        '#required' => FALSE,
        '#default_value' => isset($node->subfolder_path) ? $node->subfolder_path : '',
      ));
  }
}

/**
* Implementation of hook_nodeapi().
*
* Just store and load the subfolder_path field in/from the variable table.
*/
function cm_subdirectories_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  switch ($op) {
    case 'update':
    case 'insert':
      if (isset($node->subfolder_path)) {
        variable_set('cm_subdirectories_' . $node->nid . '_path', $node->subfolder_path);
      }
      break;
    case 'load':
      $additions = array();

      $path = variable_get('cm_subdirectories_' . $node->nid . '_path', NULL);
      if (!is_null($path)) {
        $additions['subfolder_path'] = $path;
      }

      return $additions;
  }
}

Now I just need to implement passing the data from the frontend to the backend, which is done really simply in a couple of hooks, which I pop in a drush include: cm_subdirectories.drush.inc:

/**
* Implementation of hook_hosting_site_context_options().
*/
function cm_subdirectories_hosting_site_context_options(&$task) {
  if (isset($task->ref->subfolder_path)) {
    $task->context_options['subfolder_path'] = $task->ref->subfolder_path;
  }
}

/**
* Implementation of hook_drush_context_import().
*/
function cm_subdirectories_drush_context_import($context, &$node) {
  if ($context->type == 'site') {
    if (isset($context->subfolder_path)) {
      $node->subfolder_path = $context->subfolder_path;
    }
  }

}

The first hook just sends the 'subfolder_path' property of the site node to the backend in the 'subfolder_path' context option, this will then get stored by the service that I created earlier in the backend.

The second hook is there because when you are cloning a site, the Aegir backend sends the provision context to the frontend to be imported to the site node, so this hook just looks for our property and sets it on the site node.

Note that these are module hooks, but they are only ever invoked when running a drush command, so they are safe to put in a drush include.

Conclusion

That's it! The data will get stored in the frontend, in the variable table in my case, passed to the backend, and stored by the service in the provison context. Other scripts can now use the data I've stored to build things like the settings.php. Genius! This is how you must store data in the backend for your drush scripts that need it.

All the above code is available from github to save you from copy/pasting.