Hosting API

The frontend extension contract. Where the Provision API covers the backend Drush layer, this leaf covers the Hostmaster half — the D7 hosting_* module suite that models servers/platforms/sites as nodes, queues work as task nodes, and exposes the drush hosting-* command surface. All of it is Drupal 7 running under the webserver user, and — since these functions execute inside the Drush 8 fork during dispatch — every line must stay parse- and runtime-safe on PHP 5.6.

Authoritative source is the *.api.php docblock set: hosting.api.php (root) plus five per-module files — task/hosting_task.api.php, alias/hosting_alias.api.php, quota/hosting_quota.api.php, server/hosting_server.api.php, site/hosting_site.api.php. Trust these plus the live *.module/*.drush.inc implementations, not older prose: several hook names in the legacy docs are garbled (see Hook-name corrections at the end).

Where a task actually runs

One hosting_task node → one backend provision-* invocation. The chain, verified at source:

1. Frontend saves a hosting_task node (status queued).
2. cron → drush @hostmaster hosting-dispatch          (dispatch.hosting.inc:16)
     forks each due queue: drush @hostmaster hosting-<queue>
3. hosting-tasks worker (hosting_tasks_queue) forks per queued task node:
     drush_invoke_process('@self','hosting-task',<nid>) (task/hosting_task.module:785)
4. drush_hosting_task():                               (task.hosting.inc:189)
     if task_info['provision_save']:  provision-save re-exports the node as a
        backend alias, after hosting_<type>_context_options() mutates it
                                                       (task.hosting.inc:197-208)
     task_command = command | 'provision-' . task_type (task.hosting.inc:222-223)
     provision_backend_invoke($alias, task_command, …) (task.hosting.inc:226)
5. Backend result rides the Drush backend channel back; frontend fires
     post_hosting_<type>_task() / hosting_<type>_task_rollback()
                                                       (task.hosting.inc:259-264)

Every hook below plugs into one box of that chain.

Declaring a task type — hook_hosting_tasks()

This is the hook to add an Aegir UI task. It is not parameterised by object: the single hook is hook_hosting_tasks() (task/hosting_task.api.php:42), aggregated by hosting_available_tasks() through module_invoke_all('hosting_tasks') + drupal_alter('hosting_tasks', …) (task/hosting_task.module:885-902). The object token is an array key, not part of the function name:

/**
 * Implements hook_hosting_tasks().
 */
function mymodule_hosting_tasks() {
  $tasks = array();
  $tasks['site']['mytask'] = array(
    'title'          => t('My custom task'),
    'description'    => t('Run the custom thing on a site.'),
    'weight'         => 5,
    // Omit 'command' to default to provision-mytask; set it to override.
    'command'        => 'provision-mytask',
    'dialog'         => TRUE,          // Show a confirm form before running.
    // 'hidden'      => TRUE,          // Runnable but not shown in the UI.
    // 'access callback' => 'user_access',  // Default: hosting_task_menu_access.
    // 'provision_save' => TRUE,       // provision-save before running (see below).
  );
  return $tasks;
}

The outer key (site) is the entity type the task operates on — site, platform or server. The inner key (mytask) is the task type. Defined keys (task/hosting_task.api.php:21-37):

Key Meaning
title Required. Human label in task lists.
command Drush command to run. Default: provision-<tasktype> (task.hosting.inc:222).
description Long description on the confirm form.
weight Ordering in the task dropdown.
dialog TRUE → show a confirmation dialog before executing.
hidden TRUE → runnable by the frontend but not shown as a UI button.
access callback Access gate; defaults to hosting_task_menu_access.
provision_save TRUE → run provision-save (re-export the node as a backend context) before the task. Used by install/verify/import. Pair with hook_hosting_TASK_OBJECT_context_options().

Adding the task confirm-form fields is a separate hook — hosting_task_<TASKTYPE>_form($node) (and its _form_validate), built as hosting_task_<tasktype>_form at task/hosting_task.module:619. It is keyed by task type, not object (task/hosting_task.api.php:82,94).

Passing state to the backend — provision_save + context options

A task with provision_save => TRUE runs provision-save first, which writes the node's context_options into the backend alias file that the following provision-* command reads. To inject options, implement hook_hosting_TASK_OBJECT_context_options(&$task) (hosting.api.php:218) — here the object token is part of the name, but only because the dispatcher resolves it by entity type at runtime:

$hook = 'hosting_' . $task->ref->type . '_context_options';   // task.hosting.inc:202

so for a site task you implement mymodule_hosting_site_context_options(), for a platform task …_platform_context_options(), etc. It is passed by reference; only mutate $task->context_options:

/**
 * Implements hook_hosting_site_context_options().
 */
function mymodule_hosting_site_context_options(&$task) {
  // $task->ref is the site node. Value survives as a backend context property.
  $task->context_options['mymodule_enabled'] = (int) $task->ref->mymodule_enabled;
}

That value becomes a property the backend reads via d()->mymodule_enabled (see Provision API). This is the only data channel frontend → backend: nothing else in the Hostmaster database is visible to the Drush backend. If you push context options on save, also implement hook_drush_context_import() (hosting.api.php:71) so a re-imported context recreates the node property.

Reacting to task results

Two frontend hooks fire when the backend returns, both resolved by task type at runtime (str_replace('-', '_', $task->task_type), task.hosting.inc:249,262):

Hook Fires Signature
hook_post_hosting_TASK_TYPE_task($task, $data) Task succeeded (hosting.api.php:325) e.g. mymodule_post_hosting_backup_task()
hook_hosting_TASK_TYPE_task_rollback($task, $data) Task failed + rolled back (hosting.api.php:246) e.g. mymodule_hosting_install_task_rollback()

$data is the decoded drush_backend_output() array: output, error_status, log, error_log, context. Use context to read backend-set values — the in-tree backup handler pulls $data['context']['backup_file'] back into the frontend (hosting.api.php:327-332).

A broader, status-agnostic hook is hook_hosting_task_update_status($task, $status) (hosting.api.php:392), fired on every status transition — $task->task_status holds the old value, $status the new one. The status constants are (task/hosting_task.module:1535-1557):

Constant Value
HOSTING_TASK_PROCESSING -1
HOSTING_TASK_QUEUED 0
HOSTING_TASK_SUCCESS 1
HOSTING_TASK_ERROR 2
HOSTING_TASK_WARNING 3

There is no HOSTING_TASK_SUCCESSFUL; compare against HOSTING_TASK_SUCCESS.

Guarding destructive tasks

BOA blocks dangerous tasks against protected nodes (the Hostmaster site and platform). Two frontend hooks let an extension extend both lists:

  • hook_hosting_task_guarded_nodes() / …_alter(&$nids) (hosting.api.php:420,437) — return/mutate the list of protected node IDs.
  • hook_hosting_task_dangerous_tasks() / …_alter(&$tasks) (hosting.api.php:446,464) — the task-type names blocked on guarded nodes (default set: backup-delete, backup, delete, disable, login-reset, restore).

If your task type mutates or destroys data, add it in …_dangerous_tasks_alter() so it inherits the guard.

The queue system

BOA drives the queue from cron via hosting-dispatch, not the hosting_queued daemon. hosting-dispatch (dispatch.hosting.inc:16) reads the registered queues from hosting_get_queues() (hosting.queues.inc:56) and, for each enabled and due queue, forks the matching drush hosting-<queue> worker under its own lock.

Registering a queue — hook_hosting_queues()

hook_hosting_queues() (hosting.api.php:138) returns a queue spec keyed by queue name. This fork ships exactly three implementers:

Queue key Module Type Worker function
tasks hosting_task (task/hosting_task.module:475) serial hosting_tasks_queue()
cron hosting_cron (cron/hosting_cron.module:22) batch hosting_cron_queue()
task_gc hosting_task_gc (task_gc/hosting_task_gc.module:13) batch hosting_task_gc_queue()

The dynamic hosting-<queue> Drush command is generated per registered queue (next section). Defaults merged into every spec by hosting_get_queues() (hosting.queues.inc:61-72): type serial, frequency 300s, items 5, max_threads 6, min_threads 1, threshold 100, enabled TRUE. Per-queue overrides live in variable_get('hosting_queue_<key>_frequency|items|enabled'), so operators retune without touching code.

Queue type (hosting.module:22,41,61) controls the thread/frequency maths in hosting_get_queues() (hosting.queues.inc:91-124):

  • HOSTING_QUEUE_TYPE_SERIAL ('serial') — run once per frequency, fixed items; use for anything that must not run concurrently (the tasks queue).
  • HOSTING_QUEUE_TYPE_BATCH ('batch') — divide total_items across min_threadsmax_threads by threshold; use for parallelisable per-site work (cron, task_gc).
  • HOSTING_QUEUE_TYPE_SPREAD ('spread') — amortise total_items across the frequency window, at most once a minute.

To add a queue:

/**
 * Implements hook_hosting_queues().
 */
function mymodule_hosting_queues() {
  $queues['mything'] = array(
    'name'        => t('My thing'),
    'description' => t('Process my-thing items.'),
    'type'        => HOSTING_QUEUE_TYPE_BATCH,
    'frequency'   => strtotime('1 minute', 0),
    'total_items' => mymodule_pending_count(),   // Needed for batch/spread maths.
    'items'       => 5,
    'singular'    => t('item'),
    'plural'      => t('items'),
  );
  return $queues;
}

The worker is named hosting_<queue>_queue($count) — not a Drupal hook but a direct callback resolved by name (documented as hosting_QUEUE_TYPE_queue(), hosting.api.php:355), dispatched by hosting_run_queue() which sets $func = "hosting_{$queue}_queue" (hosting.queues.inc:165):

/**
 * Queue worker for the 'mything' queue — hosting_run_queue() calls this.
 */
function hosting_mything_queue($count = 5) {
  foreach (mymodule_get_pending($count) as $item) {
    // Fork heavy work, or process inline. Return when the batch is done.
    drush_invoke_process('@self', 'mymodule-process', array($item->id), array(), array('fork' => TRUE));
  }
}

Two alter hooks reshape queues before/after the item maths: hook_hosting_queues_alter(&$queues) (hosting.api.php:156, pre-calc — e.g. zero out total_items) and hook_hosting_processed_queues_alter(&$queues) (hosting.api.php:179, post-calc — e.g. force enabled => FALSE).

Locking: both dispatch and worker hold a lock_acquire() semaphore with HOSTING_QUEUE_LOCK_TIMEOUT = 900s (hosting.drush.inc:14) as the crash recovery bound; the hold itself is sub-minute (fork-and-return). --lock-wait tunes the acquire wait (HOSTING_QUEUE_DEFAULT_LOCK_WAIT = 30s); --force bypasses a stuck lock.

hosting_queued's daemon (hosting-queued / hosting-release-lock) is not used by BOA — the cron-driven hosting-dispatch above is the queue engine. Do not build against the daemon.

Declaring hosting-* Drush commands

Same hook_drush_command() pattern as Provision, in <module>.drush.inc, with two differences: bootstrap is DRUSH_BOOTSTRAP_DRUPAL_FULL (you need the Hostmaster Drupal context to read task nodes), and the alias target is @hostmaster (Master) or @hm (a per-Octopus Hostmaster, whose alias is drush_aliases => 'hm'). The static hosting-* roster (hosting.drush.inc): hosting-dispatch (:20), hosting-setup (:29), hosting-task (:56), hosting-import (:75), hosting-pause (:83), hosting-resume (:87).

function mymodule_drush_command() {
  $items['hosting-mything'] = array(
    'description' => dt('Process the my-thing queue.'),
    'bootstrap'   => DRUSH_BOOTSTRAP_DRUPAL_FULL,
  );
  return $items;
}

function drush_mymodule_hosting_mything() {
  $nids = db_query("SELECT nid FROM {node} WHERE type = 'mything' AND status = 1")
    ->fetchCol();
  foreach (node_load_multiple($nids) as $node) {
    // Process.
  }
}

Dynamically registered per-queue commands

hosting_drush_command() also registers one command per queue at runtime (hosting.drush.inc:40-54): for every entry from hosting_get_queues() it emits $items['hosting-' . $queue] with callback => 'hosting_run_queue' and a queue => <key> marker. In this fork that yields hosting-tasks, hosting-cron, hosting-task_gc. These are what hosting-dispatch forks; you rarely call them by hand. Because the registration is conditional on hosting_get_queues() being available, the block guards with function_exists() and bootstraps to max on drush help (hosting.drush.inc:33-40) — so drush @hostmaster help | grep '^ hosting-' lists the true set on any given box.

Feature toggles

Hostmaster's Features admin page (?q=admin/hosting/features) enables and disables optional bundles. A feature is declared by hook_hosting_feature() (hosting.api.php:110) and — critically — the frontend enables/disables the declared module when the toggle flips, and can run per-feature callbacks and grant role permissions (hosting.features.inc:401-472). The declaring hook is discovered even for disabled modules: it can live in a standalone file named hosting.feature.<FEATURE_KEY>.inc, picked up by hosting_get_features() via drupal_system_listing('/hosting\.feature\..*\.inc$/') and a scan for *_hosting_feature functions (hosting.features.inc:275-292).

/**
 * Implements hook_hosting_feature().
 * Lives in hosting.feature.mything.inc (loaded even when the module is off).
 */
function mymodule_hosting_feature() {
  $features['mything'] = array(
    'title'       => t('My thing'),
    'description' => t('Adds the my-thing capability.'),
    'status'      => HOSTING_FEATURE_DISABLED,   // Default off.
    'module'      => 'mymodule',                 // Enabled/disabled with the toggle.
    'group'       => 'experimental',             // required | optional | advanced | experimental.
    // 'node'    => 'mything',                    // Node type provided by the feature.
    // 'enable'  => 'mymodule_feature_enable',    // Callback on enable.
    // 'disable' => 'mymodule_feature_disable',   // Callback on disable.
    // 'role_permissions' => array('aegir client' => array('access mything')),
  );
  return $features;
}

Status constants (hosting.features.inc:10-20): HOSTING_FEATURE_DISABLED (0), HOSTING_FEATURE_ENABLED (1), HOSTING_FEATURE_REQUIRED (2). A required feature cannot be switched off in the UI. Query a feature's live state with hosting_feature('mything') (hosting.features.inc:32) — which resolves through module_exists() for module-backed features. On the backend side the mirror is provision_hosting_feature_enabled() (see Provision API), reading the hosting_features option the frontend passes down.

group places the checkbox in a fieldset; the four real groups the form renders are required, optional, advanced, experimental (hosting.features.inc:83-106). Enabling a feature queues an automatic Verify of the Hostmaster site unless --no-verify is set (hosting_feature_rebuild_caches(), hosting.features.inc:534-541).

Permissions

Per-module + per-task. Declare permissions with hook_permission(); feature enable can auto-grant them via the feature's role_permissions map, and the aegir administrator role automatically inherits every permission a hosting module declares (hosting_add_permissions(), hosting.features.inc:565-571):

function mymodule_permission() {
  return array(
    'access mything ui' => array('title' => t('Access my-thing UI')),
    'administer mything' => array('title' => t('Administer my-thing')),
  );
}

Wire it to a task via the access callback / access arguments keys in the task definition (default callback hosting_task_menu_access).

Adding a hosting_* feature module

The end-to-end unit is a paired module: a hosting_* frontend module plus a backend *.drush.inc commandfile (Provision API → Extension skeleton). The in-tree reference pair lives in one http_basic_auth/ directory in the hosting_tasks_extra satellite repo — the hosting_http_basic_auth frontend module plus its paired drush/http_basic_auth.drush.inc backend commandfile. Layout on disk:

http_basic_auth/
  hosting_http_basic_auth.info          # D7 module, dependencies[] = hosting
  hosting_http_basic_auth.module        # node hooks, form additions
  hosting_http_basic_auth.install       # schema
  hosting_http_basic_auth.drush.inc     # frontend _context_options hook
  hosting.feature.http_basic_auth.inc   # hook_hosting_feature() (loaded when off)
  drush/http_basic_auth.drush.inc       # backend half — the paired commandfile
  drush/Provision/Service/http/basic/auth.php   # backend service class

Its hosting.feature.http_basic_auth.inc is a minimal, faithful template:

function hosting_http_basic_auth_hosting_feature() {
  $features['http_basic_auth'] = array(
    'title'       => t('HTTP Basic Authentication'),
    'description' => t('Allows admins to specify HTTP basic authentication for sites.'),
    'status'      => HOSTING_FEATURE_DISABLED,
    'module'      => 'hosting_http_basic_auth',
    'group'       => 'advanced',
  );
  return $features;
}

BOA's hosting_tasks_extra itself registers as a feature the same way (hosting.feature.tasks_extra.inc, status => HOSTING_FEATURE_ENABLED, group => 'advanced') and is cloned from its own repo + enabled on @hostmaster by the installer — it is the extension that hides obsolete task buttons to keep the UI in lock-step with the running BOA release. Model a new feature module on this pair. Checklist:

  1. Frontend module with dependencies[] = hosting (plus any hosting_* siblings you build on).
  2. hook_hosting_feature() in hosting.feature.<key>.inc so the toggle can enable your module.
  3. hook_hosting_tasks() if you add a UI task; _context_options() if that task needs provision_save.
  4. Backend commandfile implementing provision-<verb> and any provision_* hooks — shipped into .drush/usr/<ext>/, not a tenant-writable path (see Provision API and Drush fork internals).
  5. Result hooks (post_hosting_<type>_task, …_task_rollback) to record backend outcomes in the frontend.

Rules of the road:

  • PHP 5.6 is the floor — syntax and runtime — for every line the Drush fork loads during dispatch.
  • State crosses to the backend only through context_options at provision-save time; never make the backend read the Hostmaster database.
  • Log through Drush (drush_log() / drush_set_error()) so output rides the backend channel into the task log.
  • cache-clear drush after adding or moving a commandfile.

Hook-name corrections

Older docs garbled several hosting hook names; a copy-paste of the old forms gives dead code. Corrected against source at HEAD:

Wrong (legacy docs) Correct
hook_hosting_TASK_OBJECT_tasks() (e.g. mymodule_hosting_site_tasks) hook_hosting_tasks() returning $tasks[OBJECT][TYPE] (task/hosting_task.api.php:42)
hook_hosting_TASK_OBJECT_form hosting_task_<TASKTYPE>_form($node) (task/hosting_task.api.php:82)
hook_hosting_task_TASK_FORM same — hosting_task_<TASKTYPE>_form (task/hosting_task.module:619)
HOSTING_TASK_SUCCESSFUL HOSTING_TASK_SUCCESS (task/hosting_task.module:1545)
hosting-migrate-batch Drush command no such command — migration is a Batch API UI form (migrate/hosting_migrate.batch.inc)
hook_hosting_queued_process does not exist; the daemon is unused by BOA anyway
@hm-oN / @hm-o1 alias @hm (per-Octopus) or @hostmaster (Master)

Related