Extending Aegir

Tagged:

Aegir is designed to be easily extendable by developers. As it is made with Drupal and Drush, it is made of the hooks and command you know and love. If you are a user or admin looking to deploy contrib modules, you should look into the contrib modules list and user documentation instead.

What can I extend exactly?

These extensions may come in the form of

This area is be devoted to teaching you how to extend and develop for Aegir to encourage contributions to the Aegir project or to help you modify Aegir to suit your unique use case.

Aegir API documentation

The inline documentation is a good start to understand the various hooks and internals that allow you to extend and customize Aegir to your liking. The documentation is rendered on api.aegirproject.org daily.

See also the developer cheat sheet.

Please submit any suggestions or bug reports to the Aegir Project issue queue of your choice, under the "documentation" component.

Example modules

The contrib modules page documents all known Aegir extensions in existence. There are also specific developer documentation pages below.

Contrib developer documentation and roadmaps

Contrib developers are free to use this space to document the internals of their modules or their roadmaps. There is already this documentation:

Aegir developer cheat sheet

1. Locations and paths

  • Site URI: d()->uri; // ex: $base_url = 'http://' . d()->uri;
  • Drupal root: d()->site // returns: /var/aegir/platform/drupal-6.20/
  • Site path: d()->site_path // returns: /var/aegir/platform/drupal-6.20/sites/example.org/

2. File permissions and ownership

 $ctools_path = d()->site_path . '/files/ctools';

 provision_file()->chmod($ctools_path, 0770, TRUE)
   ->succeed('Changed permissions of @path to @perm')
   ->fail('Could not change permissions @path to @perm')
   ->status();

 provision_file()->chgrp($ctools_path, d('@server_master')->web_group, TRUE)
   ->succeed('Change group ownership of settings.php to @path')
   ->fail('Could not change group ownership of settings.php to @path')
   ->status();

3. Enabling/disabling drush/provision extensions from hostmaster

Every drush extension can implement a hook_drush_load(), and check if a module is enabled in the hostmaster site. See provision.api.php for an example.

4. References

http://api.aegirproject.org/

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

E-Commerce Integration Roadmap

Related links:

  • For installation and configuration instructions, see the Administrator Manual.
  • There was some valuable discussion on use cases on groups.drupal.org.

Don't put feature requests in here. Submit them to the uc_hosting or hostmaster issue queues instead.

1. Drupal 6 / uc_hosting 1.0

Aiming for compatibility with:

  • Hostmaster 1.0
  • Ubercart 2.4

Structure:

  • uc_hosting: This implements shared functions and classes, including the function to create a client based on an ubercart order and product.
  • uc_hosting_products: This is where we put the ubercart function calls, attribute generation, etc... This module is intended to be used alone for the "my ubercart is on my aegir site" scenario.

1.1. Goals

1.1.1. Single site products

In.

1.1.2. Multi site packages

In.

1.1.3. Recurring payments

We need to test the module using uc_recurring.

1.2. Nice-to-haves

Given where we are at now, this stuff will most likely not make it in until an eventual 1.1 release. The exception being Hadsie's work on "try before you buy".

1.2.1. Purchasing wizard

Stills needs to be developed. The create my site now option is a start and is already in.

1.2.2. Make it unnecessary to log single-site purchasers into the aegir site

Needs some testing but should be doable. Maybe just needs a little documentation. I believe Hadsie's work is related to this.

1.2.3. Deferred payments

Ergonlogic is working on this.

2. Drupal 7 / uc_hosting 2.0

We are still unsure about whether to support commerce, ubercart, or both with this module. We will most likely wait until development on the D7 port of aegir starts before beginning this work so we will see then what the landscape looks like.

Aiming for compatibility with:

  • Hostmaster 2.x
  • Ubercart 3.x, or Commerce 1.x (maybe both?)

2.1. Goals

2.1.1. Remote storefronts

Hopefully, this will be supported by plans for new xmlrpc methods in the hostmaster signup module.

2.2. Nice-to-haves

2.2.1. Desjardins recurring payments

This is a seperate module being developed by Koumbit.

2.2.2. Importable demo products

This would be really great.

2.2.3. Simple procedure to deny user 1 to certain clients

Ergonlogic has published a module that should make this possible: http://drupal.org/project/hosting_profile_roles.

3. Wishlist:

4. Unanswered Questions

  1. Will the Drupal 7 version use Ubercart or Commerce? Both? Neither?
  2. How and when will people be billed? And what degree of control will uc_hosting offer over that?
  3. Aegir has clients & UC has clients. How are they related/distinct? How do we keep this clear to users/admins?

Creating product features using uc_hosting 1.x

Tagged:

This document assumes you want to implement a product feature using uc_hosting's database tables and exiting infrastructure. For a more complete example of an Ubercart 2.x product feature implementation, check out uc_file in the Ubercart core.

The Basics

Any module implementing a uc_hosting feature should include both uc_hosting and uc_hosting_products as dependencies. Include these lines in your .info file:

dependencies[] = uc_hosting
dependencies[] = uc_hosting_products

Declaring product features to Ubercart 2.x

The first step is to implement Ubercart's hook_product_feature. Here is an example implementation for the platform access feature in uc_hosting_products:

/**
 * Implementation of hook_product_feature().
 */
function uc_hosting_products_product_feature () {
  $features = array();

  // Set the feature for the platforms
  $features[] = array(
    'id' => 'hosting_platform',
    'title' => t('Access to a platform'),
    'callback' => 'uc_hosting_products_platform_form',
    'delete' => 'uc_hosting_products_feature_delete',
    'settings' => 'uc_hosting_products_platform_settings',
  );

  return $features;
}

An implementation of hook_product_feature returns a numerical array of associative arrays. Feature arrays must include five keys:

  • id will be used to identify your feature at the database level, in the uc_product_features table, and also in urls. Think of it as the type of your feature.
  • title is the name of your feature as it will be displayed to site admins.
  • callback will be used to create and modify instances of your feature attached to specific products. This callback should return a drupal form passed through the uc_product_feature_form function.
  • delete is a simple function called on the removal of a feature instance from a product.
  • settings is a callback that provides additional settings for the feature, and is not used presently in any uc_hosting product feature.

Of note is that, unlike Drupal's core hook_menu and hook_theme, hook_product_feature does not include a 'file' key. uc_hosting works around this by using include_once at the beginning of the hook_product_feature implementation.

If you wish to use uc_hosting's shared feature functions, make sure to include the file "products/inc/uc_hosting_products.feature_shared.inc" (path relative to the uc_hosting root) at the beginning of your implementation of hook_product_feature.

The callback form

This form is called everytime someone adds a product feature to a product. It allows you to set any options for your feature that should be delivered on a per-product basis. Lets take a look at this function for the platform access feature:

/**
 * Callback to add the platform form on the product feature page
 */
function uc_hosting_products_platform_form ($form_state, $node, $feature) {
  $form = array();

  // Get default values and shared form elements for all uc_hosting features
  $hosting_product = _uc_hosting_products_feature_fetch_product($feature);
  if (empty($feature)) {
    $feature = array('id' => 'hosting_platform');
  }

_uc_hosting_products_feature_fetch_product attempts to retrieve existing values for this instance of a product feature from the database.

  _uc_hosting_products_feature_shared_elements(&$form, $node, $feature, $hosting_product);

_uc_hosting_products_feature_shared_elements declares some form elements that either uc_hosting or Ubercart itself depend on to provide the product feature functionality.

  // @see _hosting_get_platforms()
  if (user_access('view locked platforms')) {
    $platforms = _hosting_get_platforms();
  }
  else if (user_access('view platform')) {
    $platforms = _hosting_get_enabled_platforms();
  }
  else {
    $platforms = array();
    drupal_set_message('You must have permission to access platforms to enable this feature.', 'warning');
  }

  $form['platform'] = array(
    '#type' => 'select',
    '#title' => t('Platform'),
    '#description' => t('Select the platform to associate with this product.'),
    '#default_value' => $hosting_product->value,
    '#options' => $platforms,
    '#required' => TRUE,
  );

This code block generates a list of platforms to allow the admin to select which platform to bind to his product. This code is specific to the platform access implementation - this is where you should include your own form elements relevant to your feature.

  $form['#validate'][] = 'uc_hosting_products_single_feature_validate';

For many implementations, uc_hosting_products_single_feature_validate ensures that a given uc_hosting product feature can only be enabled once on any given product. All of the exising uc_hosting_products features make use of this function.

  return uc_product_feature_form ($form);
}

Finally, uc_product_feature_form adds some required form elements from Ubercart.

You also need to make sure to code a submit function for your callback, of the form callback_submit:

/**
 * Save the platform feature settings.
 */
function uc_hosting_products_platform_form_submit($form, &$form_state) {
  $hosting_product = array(
    'pfid' => $form_state['values']['pfid'],
    'model' => $form_state['values']['model'],
    'type' => 'platform',
    'value' => $form_state['values']['platform'],
  );

  $platform_node = node_load($hosting_product['value']);
  $description = t('<strong>SKU:</strong> !sku<br />', array('!sku' => empty($hosting_product['model']) ? 'Any' : $hosting_product['model']));
  $description .= t('<strong>Platform:</strong> !platform', array('!platform' => $platform_node->title));

  $data = array(
    'pfid' => $form_state['values']['pfid'],
    'nid' => $form_state['values']['nid'],
    'fid' => 'hosting_platform',
    'description' => $description,
  );

  $form_state['redirect'] = uc_product_feature_save($data);

We manually save the product_feature to ubercart's database tables here. This is so that we can then retrieve the index of the entry in the uc_product_features table for later use in our own data.

  // Insert or update uc_hosting_products table
  if (empty($hosting_product['pfid'])) {
    $hosting_product['pfid'] = db_last_insert_id('uc_product_features', 'pfid');
  }

  $existing_prod = db_fetch_object(db_query("SELECT hpid, data FROM {uc_hosting_products} WHERE pfid = %d LIMIT 1", $hosting_product['pfid']));

  $key = NULL;
  if ($existing_prod->hpid) {
    $key = 'hpid';
    $hosting_product['hpid'] = $existing_prod->hpid;
    $hosting_product['data'] = unserialize($existing_prod->data);
    $hosting_product['data']['platform'] = $hosting_product['value'];
  }
  // If necessary build a data array from scratch
  else {
    $hosting_product['data'] = array(
      'platform' => $hosting_product['value'],
    );
  }

  $hosting_product['data'] = serialize($hosting_product['data']);

  drupal_write_record('uc_hosting_products', $hosting_product, $key);
}

Note that the uc_hosting_products table has both an integer column value, and a longtext column data for storing serialized information about the feature.

Taking action in Aegir based on product features

uc_hosting assumes that most purchases made via Ubercart are intended to be expressed as changes to an Aegir client node. So it provides some code to help you create new clients or modify exiting ones when an order containing your feature is made. Any other actions needed to make your feature work you will need to implement yourself. This will involve implementing the Ubercart hook, hook_order and your own callback function.

Lets start by taking a look at uc_hosting_products own implementation of hook_order:

/**
 * Implementation of hook_order
 *
 * Actions t$form_state['values']['create_later']o take on order changes involving an aegir product
 *
 * @param $op string
 *   Provided by ubercart on invocation
 * @param &$arg1
 *   Different data depending on the op
 * @param $arg2
 *   Different data depending on the op
 */
function uc_hosting_products_order ($op, &$arg1, $arg2) {
  switch ($op) {
    case 'update':
      if ($arg2 == 'completed') {
        foreach ($arg1->products as $product) {
          if (_uc_hosting_products_has_feature ($product)) {
            // Make the changes necessary to the hosting client
            $client = uc_hosting_update_client($arg1, $product, 'uc_hosting_products_client_update');
          }
        }
      }
      break;
    default:
      break;
  }
}

_uc_hosting_products_has_feature is defined in uc_hosting_products.module, and provides some simple logic to detect uc_hosting product features. Rather than use this function, you will most likely want to define your own conditions.

uc_hosting_update_client does the work of matching up an incoming ubercart order to an existing Aegir client, if possible. If not possible, it creates a new client node. It then calls the function provided as a third argument, in this case uc_hosting_products_client_update, and passes to that function the affected client node, as well as the product and order objects from Ubercart.

This callback function is the place to take actions in Aegir or pass commands. Take a look at uc_hosting_products for a fully fleshed out example.

Class auto-loading

Tagged:

Provision has been refactored in 6.x-2.x to use class auto-loading. This simplifies use of classes, as it obviates the need to explicitly include the files in which the class and it's parent classes are defined. In addition, it standardizes the placement of class definitions within a hierarchical file-system structure and will make name-spacing simpler if/when we begin to adopt Symfony components.

While not strictly required in contributed modules, it is highly encouraged. If you maintain a contributed Provision extension, some re-factoring will be required to support class auto-loading.

First off, you'll need to add a registration function that will enable Provision to autoload your extensions classes. Here's an example from provision_civicrm:

/**
* Register our directory as a place to find Provision classes.
*
* This allows Provision to autoload our classes, so that we don't need to
* specifically include the files before we use the class.
*/
function civicrm_provision_register_autoload() {
  static $loaded = FALSE;
  if (!$loaded) {
    $loaded = TRUE;
    $list = drush_commandfile_list();
    $provision_dir = dirname($list['provision']);
    include_once($provision_dir . '/provision.inc');
    include_once($provision_dir . '/provision.service.inc');
    provision_autoload_register_prefix('Provision_', dirname(__FILE__));
  }
}

Next, your extension will need to call your new function in a hook_drush_init(), so that it is run as early as possible during a Drush bootstrap.

/**
* Implements hook_drush_init().
*/
function provision_civicrm_drush_init() {
  // Register our service classes for autoloading.
  civicrm_provision_register_autoload();
  ...

Finally, you can move your class definitions into the proper file-system structure. In provision_civicrm, we defined a new (stub) service to allow saving data from the front-end into a site context (entity/alias). So, we created a file at Provision/Service/civicrm.php that included the class definition:

/
* The civicrm service base class.
*/
class Provision_Service_civicrm extends Provision_Service {
  public $service = 'civicrm';

  /

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

Customizing Platform Templates

Non-standard Aegir installs may require adjustments to the default configurations for a platform that are defined by Aegir during the creation of a platform. These templates can be found in /home/aegir/.drush/provision.

Editing the actual templates prevents Aegir from overwriting your customizations when the verify task is run on an live platform.

Below are examples of customizations made to non-standard Aegir installs

Running an Aegir platform on a unix socket for php-fpm on Nginx

First let's find out where provision has the default config templates for our platfom.

[root@host]cd /home/aegir/.drush; grep -nir "fastcgi_pass" --exclude=*.svn* *
provision/http/nginx/nginx_advanced_include.conf:227:        fastcgi_pass   127.0.0.1:9000; ### php-fpm listening on port 9000
provision/http/nginx/nginx_advanced_include.conf:360:          fastcgi_pass   127.0.0.1:9000; ### php-fpm listening on port 9000
provision/http/nginx/nginx_simple_include.conf:213:        fastcgi_pass   127.0.0.1:9000; ### php-fpm listening on port 9000
provision/http/nginx/nginx_simple_include.conf:346:          fastcgi_pass   127.0.0.1:9000; ### php-fpm listening on port 9000
We can see that the config templates for Nginx direct all php requests to port 9000 on the localhost I.P. Let's change that to a unix socket (which avoids the slight delay associated with tcp connections). After commenting out those lines and adding our custom lines which will use a unix socket instead of a tcp port, we get..
provision/http/nginx/nginx_advanced_include.conf:227:    #    fastcgi_pass   127.0.0.1:9000; ### php-fpm listening on port 9000
provision/http/nginx/nginx_advanced_include.conf:228:        fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
provision/http/nginx/nginx_advanced_include.conf:361:          #fastcgi_pass   127.0.0.1:9000; ### php-fpm listening on port 9000
provision/http/nginx/nginx_advanced_include.conf:362:          fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
provision/http/nginx/nginx_simple_include.conf:213:        #fastcgi_pass   127.0.0.1:9000; ### php-fpm listening on port 9000
provision/http/nginx/nginx_simple_include.conf:214: fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
provision/http/nginx/nginx_simple_include.conf:347:#          fastcgi_pass   127.0.0.1:9000; ### php-fpm lis#tening on port 9000
provision/http/nginx/nginx_simple_include.conf:348:     fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;

Remember to do the same to your platform config files in /home/aegir/config/includes. Next time you verify your platform your customizations will remain after verification.

Passing values into the site drushrc.php file during migrations

It's possible rely to on manipulating the drush context for certain tasks and using hook_post_command to make use of that data. In this case, I wanted to save values to the site's drushrc.php file.

Add the following code to hook_post_provision_verify:

<?php
if (d()->type == 'site') {
 
$val = drush_get_option('my_custom_option');
 
drush_set_option('my_custom_option', $val, 'site');
}
?>

This makes sure that my_custom_option perpetuates through these potentially destructive operations. Since it is provision-verify that writes the drushrc.php file for the site, drush options set in the site context during hook_post_provision_verify will be saved.

The final step is to make sure the verify hook is run with the appropriate options at the end of my migrate task, so add this to hook_post_provision_migrate.

<?php
if (d()->type == 'site') {
 
$options = array('my_custom_option' => drush_get_option('my_custom_option'));
 
$target = drush_get_option('target_name');
 
provision_backend_invoke($target, 'provision-verify', array(), $options);
}
?>

I got this idea from looking at the drush_provision_drupal_provision_migrate function in platform/migrate.provision.inc.

I used this approach to create a patch for provision_civicrm

This was originally documented in the provision issue queue.