Provision API

The backend extension contract. Two files define it: provision.api.php — the documented hook stubs — and the Drush 8 command-hook engine that actually fires them (_drush_invoke_hooks(), drush/includes/command.inc:306 in the omega8cc Drush 8 fork). There is no api.drush.inc anywhere in the fork; if an old doc cites one, it is quoting a filename that never existed here. The authoritative hook docs are provision.api.php plus the file-level docblock of provision.drush.inc:1-39.

Everything on this page runs unprivileged (the aegir master user or an Octopus instance user) under the Drush 8 fork, and must stay parse- and runtime-safe on PHP 5.6. Sibling leaf Provision backend internals covers the framework core and the db//http//platform/ orchestrators; this leaf covers the surfaces you implement against.

Two dispatch channels

Provision extends through two distinct mechanisms — do not conflate them:

  1. Drush command hooks — procedural functions named after the running provision-* command, fired by _drush_invoke_hooks() for every loaded commandfile. This is the task lifecycle: validate/pre/main/post/rollback.
  2. drush_command_invoke_all('<hook>', …) API hooks — the named hooks documented in provision.api.php (provision_services, provision_nginx_vhost_config, provision_config_variables_alter, …), invoked at specific points inside the framework, implemented as <commandfile>_<hook>().

A third, object-oriented channel — class-method verbs on contexts and service drivers (verify_server_cmd(), init_site(), …) — is dispatched via method_invoke/type_invoke/command_invoke (Provision/Context.php:136-173) and fails safe: provision::method_invoke() silently skips methods that do not exist (provision.inc:1449-1454), mirrored by the __call() traps on both Provision_Context and Provision_Service.

Channel 1: the command lifecycle

For a command provision-install, the hook base is the command name with dashes flattened (provision_install). The engine iterates five phases (command.inc:372-376), and within each phase calls drush_<commandfile>_<phase-variation>() for every loaded commandfile:

Phase Function pattern (commandfile mymod)
pre-validate drush_mymod_provision_install_pre_validate()
validate drush_mymod_provision_install_validate()
pre drush_mymod_pre_provision_install()
main drush_mymod_provision_install()
post drush_mymod_post_provision_install()

One naming wrinkle: when the commandfile name is a prefix of the hook, the prefix collapses (command.inc:403) — commandfile provision + command provision-save yields drush_provision_save() (provision.drush.inc:371), not drush_provision_provision_save().

Failure and rollback. Any hook returning FALSE or setting an error via drush_set_error() stops the run; the engine then re-walks the completed hook functions in reverse order calling <func>_rollback where defined. So drush_mymod_pre_provision_install_rollback() undoes drush_mymod_pre_provision_install(). Run any command with --show-invoke to print every hook and rollback candidate the engine considered.

There is no monolithic callback. The shipped commands do their work entirely through this fan-out. provision-install has no drush_provision_install(); the work is split across the shipped commandfiles:

Commandfile File Hooks on provision-install
db db/install.provision.inc:3-13 drush_db_provision_install_validate, drush_db_pre_provision_install (+ _rollback) — creates DB + grants
http http/install.provision.inc:8-26 drush_http_provision_install, drush_http_post_provision_install (+ _rollback) — writes the vhost
provision_drupal platform/install.provision.inc:17,66,102,442 _validate, pre, main, post — site dir, settings.php, profile install

A current BOA-fork example of the post phase: drush_provision_drupal_post_provision_install() hands the new site's files/ + private/ over to the per-account static store via _provision_drupal_native_symlink() (platform/install.provision.inc:442,471; platform/provision_drupal.drush.inc:503).

Guards that run before your hooks. provision_drush_init() (provision.drush.inc:51-61) loads the target context into d(), then:

  • refuses to run any provision-* command as root (_provision_drush_check_user(), provision.drush.inc:67-73PROVISION_IS_ROOT);
  • aborts when system load is critical (_provision_drush_check_load(), :80-86; provision_load_critical() :489 — threshold ncpus × critical_load_multiplier (default 5) or flat 10; on BOA the CPU count comes from /data/all/cpuinfo before /proc/cpuinfo, provision_count_cpus() :445-464).

Write hook bodies assuming both: your code never sees root, and a run can be refused before it starts.

Channel 2: the provision.api.php hook surface

All of these are invoked with drush_command_invoke_all() (or the by-ref variant), so an implementation in commandfile mymod is named mymod_<hook>() — no drush_ prefix. Verified in-tree example: http_basic_auth_provision_nginx_vhost_config($uri, $data) (hosting_tasks_extra/http_basic_auth/drush/http_basic_auth.drush.inc:128). The drush_hook_ / hook_ prefixes on the stubs in provision.api.php are docblock convention only.

Service + context plumbing

Hook (stub line in provision.api.php) Fired from Contract
hook_provision_services() (:82) Provision_Context::init() (Context.php:201), load_services() (Context/server.php:77) Return array('type' => default-or-NULL); registers a service type (see drivers below)
hook_provision_context_alter(&$context) (:96) d() first-load path (provision.context.inc:74) Swap or mutate a context object right after init

Config rendering (the alter chain)

Every config file Provision writes goes through Provision_Config; three hooks interpose, in order:

Hook Fired from Contract
hook_provision_config_load_templates($config) (:274) Provision/Config.php:120 Return a template filename to use instead of the class default
hook_provision_config_load_templates_alter(&$templates, $config) (:291) Provision/Config.php:126 Trim/reorder the suggested templates
hook_provision_config_variables_alter(&$variables, $template, $config) (:311) Provision/Config.php:173 Mutate the variables injected into the template

Plus the per-server-config append hooks, whose joined return values land in $data['extra_config'] inside the rendered file:

Hook Fired from
provision_nginx_server_config($data) (:220) http/Provision/Config/Nginx/Server.php:10, …/Nginx/Ssl/Server.php:17
provision_nginx_dir_config($data) (:239) platform-level Nginx config
provision_nginx_vhost_config($uri, $data) (:260) http/Provision/Config/Nginx/Site.php:10, …/Nginx/Ssl/Site.php:13
provision_drupal_config($uri, $data) (:117) settings.php writer (Provision_Config_Drupal_Settings)

Apache-flavoured twins exist in the api file (:137-202); BOA deploys Nginx only, so treat them as dormant surface. Prefer the two operator-facing *_vhost_include.conf override points for config that does not need PHP logic — reserve these hooks for values computed from context properties.

Task/data lifecycle alters

Hook Contract
hook_provision_drupal_create_directories_alter(&$mkdir, $url) (:354) Add/skip site-dir entries; value = octal mode or FALSE to skip chmod
hook_provision_drupal_chgrp_directories_alter(&$chgrp, $url) (:367) Group-ownership map; FALSE skips
hook_provision_drupal_chgrp_not_recursive_directories_alter (:381) / …chmod_not_recursive… (:394) Opt dirs out of recursion
hook_provision_drupal_install_settings_alter(&$settings, $url) (:407) Mutate the profile-install settings array
hook_provision_deploy_options_alter(&$deploy_options, $context) (:420) Adjust options passed to provision-deploy inside restore/clone/migrate; $context is the invoking task type string
hook_provision_backup_exclusions_alter(&$directories) (:488) Add site-relative paths excluded from backup tarballs
hook_provision_platform_sync_path_alter(&$sync_path) (:333) Change what rsyncs to remote servers for composer-built platforms
hook_provision_prepare_environment() (:458) Runs right after sites/$URI/drushrc.php is written; DB creds are in $_SERVER

DB-layer alters

Hook Contract
hook_provision_mysql_regex_alter(&$regexes) (:437) Pattern ⇒ replacement map filtering mysqldump output; FALSE drops the line (defaults in Provision_Service_db_mysql::get_regexes())
hook_provision_db_options_alter(&$options, $dsn) (:505) PDO connection options (SSL certs etc.)
hook_provision_suggest_db_name_alter(&$database) (:536) Rewrite the generated DB name; keep ≤16 chars
hook_provision_db_username_alter(&$user, $host, $op) (:552) Rewrite DB usernames; $op distinguishes grant/revoke

drushrc $options knobs

The head of provision.api.php (:11-57) documents behaviour toggles set in local.drushrc.php rather than implemented as hooks: provision_backup_suffix (default .tar; .tar.gz, .tar.zst, … accepted), provision_verify_platforms_before_migrate (default TRUE), provision_create_local_settings_file (default TRUE), provision_mysqldump_suppress_gtid_restore (default FALSE), provision_composer_install_platforms (default TRUE), provision_composer_install_platforms_verify_always (default TRUE), provision_composer_install_command (default composer install --no-interaction --no-progress --no-dev).

Frontend-feature coupling

hook_drush_load() (:64) is the deprecated gate for "only load this backend extension when its frontend module is enabled". The current idiom is calling provision_hosting_feature_enabled($feature) (provision.drush.inc:508-511) inside your hooks — it reads the hosting_features option the frontend passes down on every backend invoke. In-tree example: the Nginx driver enables subdir config classes only when provision_hosting_feature_enabled('subdirs') (http/Provision/Service/http/nginx.php:37).

Contexts and d() from the backend side

There are exactly three context types — server, platform, site — instantiated as Provision_Context_{$type} (provision.context.inc:130; default type is server, provision_context_factory() :113-133). db_server is not a fourth type: it is a site property whose value names a server context.

d($name) (provision.context.inc:23) is a static registry:

  • d() — the current root context, set by provision_drush_init() from the @alias the command targets;
  • d('@site_foo.example.com') — load/instantiate any other context by alias (leading @ optional, normalised at provision.inc:1416);
  • d('all') — the raw instance cache;
  • first load runs method_invoke('init'), type_invoke('init'), then the provision_context_alter hook (provision.context.inc:70-75);
  • calling d() before Drush finishes bootstrapping is a hard error (DRUSH_BOOTSTRAPPING guard, :41-44) — only call it inside hooks.

Property model. Context properties are magic (Provision/Context.php:55-97): reads fall through to the $properties array; $context->options merges the saved alias record with stdin, options and CLI contexts (:56-57); setProperty($field, $default, $is_array) (:215) seeds a property from options-or-default during init_*. Properties registered with is_oid() auto-dereference through d()d()->platform returns the platform context object, d()->platform->server the server object behind it, and d()->db_server the DB server context (:60-65, wired in Provision/Context/site.php and db/Provision/Service/db.php:10-14).

Per-type defaults set in init_* (the properties your hooks can rely on):

Type Class Key properties
server Provision/Context/server.php:13 remote_host, aegir_root, script_user, ip_addresses, master_url, derived backup_path/config_path/include_path/clients_path (:46-71), plus one <service>_service_type per registered service
platform Provision/Context/platform.php:11 root, server, web_server, makefile, make_working_copy
site Provision/Context/site.php:7 uri, platform, db_server, site_path, language, aliases, redirection, client_name, profile (default standard), install_method (default profile), drush_aliases (:25-49)

Each type's option_documentation() static declares what provision-save accepts for it (server.php:21, platform.php:14, site.php:10); provision-save merges all three sets into its option list (provision.drush.inc:97-105).

Persistence. A context is a Drush alias file. write_alias() renders the properties through Provision_Config_Drushrc_Alias to ~/.drush/<name>.alias.drushrc.php (Provision/Config/Drushrc/Alias.php:34-37); site contexts additionally write one alias file per entry in drush_aliases (Provision/Context/site.php:54-61). Anything the frontend passes as a --option on provision-save and a setProperty() picks up survives as a property your backend hooks can read later — that is the entire data channel between the Hostmaster frontend and your extension (frontend half: Hosting API; full pattern: Extending Aegir).

Service-driver classes

Service types are advertised by hook_provision_services() and bound to concrete drivers per server:

  • Registration: db_provision_services() (db/db.drush.inc:16) and http_provision_services() (http/http.drush.inc:24) each return array('<type>' => NULL) and register their own directory on the PSR-0 autoloader for the Provision_ prefix (provision_autoload_register_prefix(), provision.inc:42; the core tree registers itself at provision.inc:66).
  • Driver selection: the server context spawns one driver per service type in load_services()/spawn_service() (Provision/Context/server.php:76-116). The class name is computed as Provision_Service_{$service}_{$type} (server.php:99), where $type comes from the server's <service>_service_type property (BOA: http_service_type nginx, db_service_type mysql). Missing class logs an error; type NULL/'NONE' gets the no-op Provision_Service_null.
  • Hierarchies at HEAD: Provision_Service extends Provision_ChainedState (Provision/Service.php:10; Provision/ChainedState.php:11) → Provision_Service_http (http/Provision/Service/http.php:4) → Provision_Service_http_publicProvision_Service_http_nginx (…/http/nginx.php:3), with apache, cluster, pack, ssl, public variants beside it; Provision_Service_db (db/Provision/Service/db.php:3) → Provision_Service_db_pdo (…/db/pdo.php:5) → Provision_Service_db_mysql (…/db/mysql.php:10).

A driver's surface, from the Nginx driver as the canonical example:

Method Job Example
init_server() Register Provision_Config classes into $this->configs['server'\|'site'\|…], seed server properties nginx.php:21-42 — config classes plus nginx_config_mode, nginx_has_http3, satellite_mode 'boa', …
save_server() Probe the host once at save time, persist findings as properties nginx.php:44-83 — locates the binary, feature-detects via nginx -V, flips basic/extended mode on the /etc/nginx/basic_nginx.conf control file
verify_<type>_cmd() Per-context verify verb, dispatched by command_invoke() (Context.php:171-173) http.php:17-29create_config() + parse_configs()
config() / create_config() / delete_config() Instantiate registered config classes and write/unlink them (Service.php:103-131,241-255)
restart() Run the <service>_restart_cmd server property (Service.php:276-302)
static subscribe_site() / subscribe_platform() Bind the service to non-server contexts db.php:10-14 sets db_server + service_subscribe('db', …); http.php:36-40 same for web_server
static option_documentation() Driver-specific provision-save options (Service.php:343-345)

Service resolution from any context walks subscriptions, then the parent chain, and falls back to Provision_Service_null (Provision/Context.php:270-280); on a server context it returns the spawned driver directly (server.php:121-124). So d()->service('http')->restart() works from a site context and lands on the right server's driver.

Legacy note: Provision_Context_server::option_documentation() still scans for {type}/{type}_service.inc files (server.php:28-42, self-described "file scanning nastiness"); the shipped drivers actually load through the PSR-0 autoloader, and db/mysql/mysql_service.inc survives only as an empty placeholder. Model new drivers on the PSR-0 tree (<ext>/Provision/Service/<type>/<driver>.php), not on *_service.inc.

Declaring provision-* commands

provision_drush_command() (provision.drush.inc:91-369) is the registry — and the model to copy. What it actually registers at HEAD: provision-save, -install, -install-backend (hidden), -import, -backup, -enable, -disable, -lock, -unlock, -dlock, -dunlock, -verify (aliases v, pv, verify), -restore, -deploy, -migrate, -clone, -delete, -login-reset, -backup-delete, plus hostmaster-migrate, hostmaster-install, hostmaster-uninstall, backend-parse. The file-head docblock also mentions stats — there is no provision-stats registration; that lives in an optional upstream-era module the fork does not ship. Trust the $items array, not the prose.

Entry anatomy, with the conventions that matter:

function mymod_drush_command() {
  $items['provision-widget'] = array(
    'description' => dt('Do the widget thing on a site.'),
    'arguments' => array(
      '@new_site' => dt('Drush alias of the target, as generated by provision-save.'),
    ),
    'options' => array(
      'widget_mode' => dt('Optional mode toggle.'),
    ),
    'examples' => array(
      'drush @site provision-widget' => 'Widget the site.',
    ),
    'allow-additional-options' => TRUE,
    'bootstrap' => DRUSH_BOOTSTRAP_DRUSH,
  );
  return $items;
}

// Primary callback: commandfile 'mymod' is not a prefix of
// 'provision-widget', so no prefix collapse applies.
function drush_mymod_provision_widget() {
  if (!d()->site_path) {
    return drush_set_error('WIDGET_NO_SITE', dt('Not a site context.'));
  }
  drush_log(dt('Widget applied.'), 'ok');
}

function drush_mymod_provision_widget_rollback() {
  // Undo partial work.
}
  • Alias arguments are aliases. Commands that take another context take its @alias (created by a prior provision-save), never a bare domain — see provision-clone's @new_site argument (provision.drush.inc:266-278).
  • allow-additional-options is load-bearing. Context properties travel as arbitrary --options; without it Drush rejects them.
  • Pick bootstrap deliberately. Shipped commands use DRUSH_BOOTSTRAP_DRUSH (save, verify, delete, dlock/dunlock, backup-delete) or DRUSH_BOOTSTRAP_DRUPAL_ROOT (install, import, backup, enable/disable, restore, deploy, migrate, clone, lock/unlock, login-reset). On D10+ platforms a high declared bootstrap can fatal at dispatch before your callback runs — the dlock/dunlock pair was deliberately lowered to DRUSH_BOOTSTRAP_DRUSH with an explicit in-callback root bootstrap for exactly that reason (provision.drush.inc:191-205; platform/dlock.provision.inc:21-60). Declare the minimum and bootstrap higher inside the callback when a branch needs it.
  • After adding or moving a command file: drush cache-clear drush.

Where the file may live. The backend discovers commandfiles from the BOA-managed trees — .drush/sys/ (Provision itself), .drush/usr/ and .drush/xts/ (contrib extensions; BOA stages registry_rebuild, drush_ecl, provision_boost there) — and from the Hostmaster platform's modules. The Drush 8 fork's extension filter default-denies *.drush.inc under tenant-writable paths for privileged backend identities: matching /data/disk/<user>/{static,distro,platforms}/ and the non-{sys,usr,xts} parts of /data/disk/<user>/.drush/ (drush/includes/boa_extension_filter.inc:65-83 at HEAD, with per-instance control-file opt-ins under /data/conf/). A command file dropped into a denied path silently never loads during backend tasks. Ship into .drush/usr/<ext>/ or the platform's module tree; mechanics and history on Drush fork internals.

Extension skeleton

The in-tree reference extension is http_basic_auth from the hosting_tasks_extra satellite repo — a paired module: a hosting_* D7 module on the frontend, one backend commandfile at http_basic_auth/drush/http_basic_auth.drush.inc. Reduced to its moves:

<?php
// mymod.drush.inc — backend half of a paired Aegir extension.
// PHP 5.6 floor: no ??, no scalar type hints, no arrow functions.

/**
 * Implements hook_drush_init().
 * Locate provision via the commandfile list and register our
 * Provision_ class dir — required only if we ship driver/config classes.
 * Pattern: http_basic_auth.drush.inc:6-27.
 */
function mymod_drush_init() {
  static $loaded = FALSE;
  if (!$loaded) {
    $loaded = TRUE;
    $list = drush_commandfile_list();
    $provision_dir = dirname($list['provision']);
    if (is_readable($provision_dir . '/provision.inc')) {
      include_once($provision_dir . '/provision.inc');
      if (function_exists('provision_autoload_register_prefix')) {
        provision_autoload_register_prefix('Provision_', dirname(__FILE__));
      }
    }
  }
}

/**
 * Implements hook_provision_services().
 * Only needed when defining a NEW service type with drivers under
 * mymod/Provision/Service/mymod/. Pattern: http_basic_auth.drush.inc:35-38.
 */
function mymod_provision_services() {
  return array('mymod' => NULL);
}

/**
 * Implements hook_provision_nginx_vhost_config().
 * Named <commandfile>_<hook> — invoked via drush_command_invoke_all()
 * from http/Provision/Config/Nginx/Site.php:10. Return lines land in
 * the vhost's extra_config block.
 */
function mymod_provision_nginx_vhost_config($uri, $data) {
  // Context properties put here by the frontend via provision-save.
  if (!d()->mymod_enabled) {
    return '';
  }
  return "# mymod for {$uri}\n";
}

/**
 * Lifecycle participation: fires on every provision-verify,
 * any context type — branch on d()->type.
 */
function drush_mymod_pre_provision_verify() {
  if (d()->type === 'site') {
    drush_log(dt('mymod: checking @uri', array('@uri' => d()->uri)), 'notice');
  }
}

Rules of the road, all enforced by the machinery above:

  • Log through Drush. drush_log() output and drush_set_error() failures ride the backend channel into the frontend task log; print/echo does not, and a raw exit skips rollback.
  • Fail by returning. return drush_set_error(...) (or FALSE) triggers the reverse-order _rollback walk; throwing is caught and converted to an error by the engine, but gives you no message control.
  • Branch on d()->type, not on argument shape — every lifecycle hook fires for servers, platforms and sites alike.
  • State goes through context properties, written by the frontend at provision-save time — never read Hostmaster's database from the backend.
  • PHP 5.6 is the floor, syntax and runtime both, for every line loaded by the Drush 8 fork.

The frontend half — declaring the feature, injecting context options from the task node, reacting to task results — is the Hosting API leaf; the full paired-module walkthrough with the contexts data model is Extending Aegir.

Related