Extending Aegir and communicating with install profiles

Tagged:

This article has been republished from mig5's website. Original URL

Aegir is a pretty powerful tool that allows you to very quickly provision new Drupal sites out of the box and manage them throughout their lifespan through a variety of tasks.

Its ability to understand different install profiles and thus 'distributions' shoots the awesome factor through the roof once you can deploy instances of OpenAtrium, Pressflow, ManagingNews or your own custom distro in just a couple of clicks.

Inevitably, though, your requirements will grow more complex. Aegir is about automation, and install profiles that prompt users for information through a series of tasks, and fail without that information, aren't much help. You might resort to a bunch of hacks to get that data into your client's site. You might try and insert some custom stuff into the site's vhost only to whimper in dismay when Aegir does a routine Verify sanity check and obliterates your changes.

If you get to this point, the simplicity of Aegir will be a distant memory and you'll be shaking your fist at us developers for forgetting about your corner case. Curses!

Rest assured, despite the constantly shifting development in Aegir and its early alpha stages, we try to maintain an ability to hook into Aegir and extend it as easily as possible, and it will only continue to get easier, especially once our API stabilises and is published.

Today I'm going to show you:

  • how to add a custom Hosting submodule to Aegir
  • how to alter the site form to add custom fields of data
  • how to send that data to the backend
  • how to apply that data to site vhosts and inside your custom install profile and site database

There are a few files here, because we're creating a custom module and install profile, and a few other things. For the impatient, I've attached a tarball to this article called 'cheesy.tar.gz' that contains all the relevant components and a README.txt instructing where to put it all.

Those of you who follow me on Twitter know I like to perpetuate the semi-French stereotype and rant and rave about cheese. I spent some time trying to think of a non-cheesy example to use in this tutorial, before realising what a silly idea that was. You can never have enough cheese :)

Part one - Adding a cheesy module

We'll add a custom module to the Hosting frontend called 'Cheese'.

Hosting is the 'frontend' bit of the Aegir system that allows you to plug data in via a web interface and have that information, if necessary, communicated to the backend Drush/Provision system by way of 'tasks'.

Our new module is simple: we're going to invoke a hook_form_alter() to add a textfield to the site form. The module will add a table to the Aegir database that stores this value, linked to the site nid. Keeping it out of the hosting_site table means we can disable and uninstall this module later and clean up properly.

There's nothing new or unusual about a Hosting submodule than any other Drupal module. Make the directory /var/aegir/hostmaster-0.x/profiles/hostmaster/modules/hosting/cheese

Info file

Here's our hosting_cheese.info

name = Cheese
description = Cheesemongers need to define cheese in their Drupal sites.
package = Hosting
dependencies[] = hosting_site

core = 6.x

You can see we depend on hosting_site, because hosting_site is the module that provides the site form for installing sites!

Install file

Now let's add an .install file that defines our database table schema and how to install/uninstall it. Standard Drupal stuff.

<?php
/**
* Implementation of hook_schema().
*/
function hosting_cheese_schema() {
 
$schema['hosting_cheese'] = array(
   
'fields' => array(
     
'vid' => array(
       
'type' => 'int',
       
'not null' => TRUE,
       
'default' => 0,
      ),
     
'nid' => array(
       
'type' => 'int',
       
'unsigned' => TRUE,
       
'not null' => TRUE,
       
'default' => 0,
      ),
     
'cheese' => array(
       
'type' => 'varchar',
       
'length' => 128,
       
'not null' => TRUE,
      ),
    ),
   
'indexes' => array(
     
'vid' => array('vid'),
     
'cheese' => array('cheese'),
    ),
  );

  return

$schema;
}

function

hosting_cheese_install() {
 
// Create tables.
 
drupal_install_schema('hosting_cheese');
}

function

hosting_cheese_uninstall() {
 
// Remove tables.
 
drupal_uninstall_schema('hosting_cheese');
}
?>

The module

Here's our simple hosting_cheese.module file.

It's pretty straightforward: our first function is a hook_form_alter() of hosting_site_form(). We are adding a single textfield to the form called 'Cheese'. Because it's my favourite, the default value is going to be 'camembert' if it hasn't already been set on the node (if we're updating a site rather than creating a new one).

The next few functions are invocations of common database hooks such as insert, update and delete. They will insert the 'cheese' value from our form into the hosting_cheese table.

I call hook_nodeapi() to do $stuff on those actions. There's validation to check that a cheese was entered. We invoke hook_load(), which allows various modules and features to return their bits and pieces from various parts of the database as 'additions' to the core node attributes. With this we can call $node->cheese when we might need to.

<?php
/**
* Implementation of hook_form_alter()
*/
function hosting_cheese_form_alter(&$form, $form_state, $form_id) {
  if (
$form_id == 'site_node_form') {
   
$form['cheese'] = array(
     
'#type' => 'textfield',
     
'#title' => t('Cheese'),
     
'#description' => t('What sort of cheese?'),
     
'#default_value' => $form['#node']->cheese ? $form['#node']->cheese : 'camembert',
     
'#required' => TRUE,
     
'#weight' => 0,
    );
    return
$form;
  }
}

/**
* Implementation of hook_insert()
*/
function hosting_cheese_insert($node) {
  if (
$node->cheese) {
   
db_query("INSERT INTO {hosting_cheese} (vid, nid, cheese) VALUES (%d, %d, '%s')", $node->vid, $node->nid, $node->cheese);
  }
}

/**
* Implementation of hook_update()
*/

function hosting_cheese_update($node) {
 
db_query("UPDATE {hosting_cheese} SET cheese = '%s' WHERE nid = %d", $node->cheese, $node->nid);
}

/**
* Implementation of hook_delete()
*/
function hosting_cheese_delete($node) {
 
db_query("DELETE FROM {hosting_cheese} WHERE nid=%d", $node->nid);
}

/**
* Implementation of hook_delete_revision()
*/
function hosting_cheese_delete_revision($node) {
 
db_query("DELETE FROM {hosting_cheese} WHERE vid=%d", $node->vid);
}

/**
* Implementation of hook_nodeapi()
*/
function hosting_cheese_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  if (
$node->type == 'site') {
    switch (
$op) {
    case
'insert':
       
hosting_cheese_insert($node);
        break;
      case
'update':
       
hosting_cheese_update($node);
        break;
      case
'delete' :
       
hosting_cheese_delete($node);
        break;
      case
'delete revision':
       
hosting_cheese_delete_revision($node);
        break;
      case
'validate' :
        if (!
$node->cheese) {
         
form_set_error('cheese', t('You must enter a cheese!'));
        }
        break;
      case
'load':
       
$additions['cheese'] = db_result(db_query("SELECT cheese FROM {hosting_cheese} WHERE vid=%d", $node->vid));
        return
$additions;
        break;
    }
  }
}
?>

At this point, you can go to /admin/build/modules and enable the Cheese feature. The database table will get installed, and you can now go to Create Content > Site and see our cheese field is present in the site form!

Part two - sending the cheese to the backend

Yes, it's very exciting that we have cheese in Aegir now. But don't provision a site yet! The fun hasn't even started.

In the same directory as the hosting_cheese module, create a new file called hosting_cheese.drush.inc. This is a Drush file that is going to be responsible for catching when an Install or Verify task is being executed against a site, and to send the cheese value along for the ride to the backend.

<?php
/**
* Implementation of drush_hook_pre_hosting_task()
* Send the site's cheese attribute to the backend for processing.
*/
function drush_hosting_cheese_pre_hosting_task() {
 
$task =& drush_get_context('HOSTING_TASK');
  if (
$task->ref->type == 'site' && ($task->task_type == 'install' || $task->task_type == 'verify')) {
   
$task->options['cheese'] = $task->ref->cheese;
  }
}
?>

In this code, $task->ref represents the node object (the site).

Because of our additions in our implementation of hook_node_load(), the cheese value is fetched and available for us to use.

Part three - an install profile (just for fun)

I'm demonstrating all this to show you how to get data from the frontend (in the site form) to the backend. We've achieved that, but you probably want to actually *do* stuff with the data.

I'll demonstrate two things you can do with this data. They are just examples - Aegir isn't limited to this at all.

cheesy_sites.profile
First let's create a simple install profile called 'Cheesy sites'.

<?php
/**
* Return an array of the modules to be enabled when this profile is installed.
*
* @return
*  An array of modules to be enabled.
*/
function cheesy_sites_profile_modules() {
  return array(
   
/* core */ 'block', 'color', 'filter', 'help', 'menu', 'node', 'system', 'user', 'path',
   
/* other contrib */ 'install_profile_api', 'token', 'pathauto', 'views',
  );
}

/**
* Return a description of the profile for the initial installation screen.
*
* @return
*   An array with keys 'name' and 'description' describing this profile.
*/
function cheesy_sites_profile_details() {
  return array(
   
'name' => 'Cheesy site',
   
'description' => 'Select this profile to install a really cheesy site.'
 
);
}

function

cheesy_sites_profile_tasks(&$task, $url) {
 
// Install dependencies
 
install_include(cheesy_sites_profile_modules());
 
// Fetch and set the cheese attribute from Drush, sent originally
  // from the site form in the Aegir frontend.
 
variable_set('cheese' , drush_get_option('cheese', 'camembert'));
}
?>

This is not meant as an exercise in learning how to write install profiles so I'll gloss over some of the details here. Basic version: we define our modules that the profile depends on (core + some contrib), we install those modules, and finally we do a basic variable_set to store a 'cheese' variable in the site's database, fetching the value from the Drush/Provision backend (sent by the frontend per above), defaulting to 'camembert' if there was none.

Obviously in a normal install profile, this file would be bigger, as you'd be doing a bunch of other stuff such as setting up node types, setting up menus, blocks, users etc.

cheesy_sites.make
I provide a Drush makefile here to fetch the contrib for you. This is totally out of the scope of the tutorial, but it's just fun to use Drush Make and I encourage you to get used to it :)

core = 6.x
api = 2

; Contrib modules
projects[token][version] = "1.15"
projects[pathauto][version] = "1.5"
projects[views][version] = "2.11"
projects[install_profile_api][version] = "2.1"

Download Drupal into /var/aegir/drupal-6.19 and copy the cheesy_sites directory into the profiles directory of drupal-6.19, or use a Drush stub makefile to do it all.

If you downloaded core manually, you'll want to cd drupal-6.19; drush make profiles/cheesy_sites/cheesy_sites.make --no-core. The contrib will be downloaded to /var/aegir/drupal-6.19/sites/all/modules

Add the platform

Add a new Platform in Aegir with the Publish Path being that which we just created: /var/aegir/drupal-6.19. A Verify task will be spawned, and Aegir will pick up all the modules and also the 'cheesy_sites' profile, which you can check by viewing the 'Installation profiles' section of the 'Packages' menu tab on the platform node, after the Verify task has completed.

You could also add this profile and its dependencies to an existing platform and just re-Verify the platform, and the new profile will be synced up and added to this platform's package registry.

Part four - make the backend knowledgeable about cheese

Now we've set up the frontend to get the cheese, set up a drush file to send the cheese to the backend, and we have an install profile that expects to find cheese and set a variable about it in a database when installing a site.

We've left out one crucial stage, and that is to actually prepare the backend to tell it that cheese is coming!

cheese.drush.inc

Telling the backend about the incoming cheese is very similar to what we did earlier about capturing the cheese info on specific tasks and do $stuff with it.

To tell Drush and Provision (Aegir's Drush extension) about the cheese, create a file in ~aegir/.drush called cheese.drush.inc.

We'll invoke a Drush hook that sets the value for the backend on installing a site.

<?php
/**
* Implementation of hook_pre_provision_install()
*/
function drush_cheese_pre_provision_install($url = NULL) {
 
drush_set_option('cheese', drush_get_option('cheese', 'camembert'), 'site');
}
?>

This saves the value into the site context, where it's usable by the install profile. This occurs in the pre hook, prior to when Aegir starts executing the tasks outlined by the install profile.

Part five - using Aegir hooks to inject settings into the site vhost

In Part four, we'll have accomplished what is needed to set the cheese value for the backend, which in turn means you can now create a site in the Aegir frontend, choose the 'Cheesy site' profile, and expect the install profile to pick that cheese setting up and store it in the new site's database as a variable.

But! While I'm here, let me show you another Aegir hook that allows you to safely inject extra configuration sent from the frontend into the site's Apache vhost config file, persistently, without it being overwritten next time the site is Verified.

We'll invoke an Aegir hook called provision_apache_vhost_config() to insert a SetEnv into the vhost file, just for the hell of it.

Another example where you might want to do this is, say, to set a htpasswd password in the site form, and use the hook to inject mod_auth htpasswd protection for the site (perhaps a demonstration for another time!).

Add this to your cheese.drush.inc:

<?php
/*
* Implementation of hook_provision_apache_vhost_config()
*/
function cheese_provision_apache_vhost_config($uri, $data) {
  return
"  SetEnv cheese  " . drush_get_option('cheese', 'camembert');
}
?>

The return value is inserted into the vhost. You can return multiple lines by wrapping them in an array, or even more elegantly, simply return a path to a template .tpl.php file and do your customisations in the template!

Part six - check the results!

If you've done everything up to Part four and five prior to installing a site using the 'Cheesy site' install profile, do so now!

I'm going to create a site called 'cheesy.mig5-forge.net' and set my cheese type to be 'cheddar'.

Now let's check to see those settings in their end result:

The variable in the new site's database

Run this command: drush @yoursite.com vget cheese

You should see something like this:

Great! Now let's look at the Apache vhost file for this site, and we should see a 'SetEnv' parameter injected into the vhost.

Conclusion

I know these have been silly examples, but I hope you've taken some important lessons out of all this:

  • How to extend the frontend of Aegir and hook into things like the site form and site node
  • How to capture data on task 'events' like install and send it to the backend
  • How to recognise that data in the backend and use it in install profiles or invoke our hooks to inject the data into configuration files
  • How much I really like cheese

Cheesy tarball

As mentioned at the start of this article, you can download all these components I've been talking about here: cheesy.tar.gz. Remember to read the README.txt

Further reading

A great article by hadsie on g.d.o on sending data to an install profile via the Signup form covers similar information on capturing data sent to the backend in an install profile.

Steven Jones has published a HTTP mod_auth feature extension for injecting htpasswd password protection on sites over at https://github.com/computerminds/aegir_http_basic

Referenced by Last post Author
Contrib Place? Wed, 05/04/2011 - 14:41 leet
Can't edit referenced page Thu, 11/11/2010 - 23:49 Steven Jones