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:
- 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. drush_command_invoke_all('<hook>', …)API hooks — the named hooks documented inprovision.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-73—PROVISION_IS_ROOT); - aborts when system load is critical
(
_provision_drush_check_load(),:80-86;provision_load_critical():489— thresholdncpus × critical_load_multiplier(default 5) or flat 10; on BOA the CPU count comes from/data/all/cpuinfobefore/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 byprovision_drush_init()from the@aliasthe command targets;d('@site_foo.example.com')— load/instantiate any other context by alias (leading@optional, normalised atprovision.inc:1416);d('all')— the raw instance cache;- first load runs
method_invoke('init'),type_invoke('init'), then theprovision_context_alterhook (provision.context.inc:70-75); - calling
d()before Drush finishes bootstrapping is a hard error (DRUSH_BOOTSTRAPPINGguard,: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) andhttp_provision_services()(http/http.drush.inc:24) each returnarray('<type>' => NULL)and register their own directory on the PSR-0 autoloader for theProvision_prefix (provision_autoload_register_prefix(),provision.inc:42; the core tree registers itself atprovision.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 asProvision_Service_{$service}_{$type}(server.php:99), where$typecomes from the server's<service>_service_typeproperty (BOA:http_service_type nginx,db_service_type mysql). Missing class logs an error; typeNULL/'NONE'gets the no-opProvision_Service_null. - Hierarchies at HEAD:
Provision_ServiceextendsProvision_ChainedState(Provision/Service.php:10;Provision/ChainedState.php:11) →Provision_Service_http(http/Provision/Service/http.php:4) →Provision_Service_http_public→Provision_Service_http_nginx(…/http/nginx.php:3), withapache,cluster,pack,ssl,publicvariants 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-29 — create_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 priorprovision-save), never a bare domain — seeprovision-clone's@new_siteargument (provision.drush.inc:266-278). allow-additional-optionsis 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) orDRUSH_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 toDRUSH_BOOTSTRAP_DRUSHwith 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 anddrush_set_error()failures ride the backend channel into the frontend task log;print/echodoes not, and a rawexitskips rollback. - Fail by returning.
return drush_set_error(...)(orFALSE) triggers the reverse-order_rollbackwalk; 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-savetime — 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
- Provision backend internals — command registry,
framework core, per-service orchestrator
.incfiles. - Hosting API — the frontend hook surface these extensions pair with.
- Extending Aegir: contexts, hooks, module pairs — the end-to-end extension pattern.
- Drush fork internals — commandfile discovery and the extension deny-filter this page's shipping rules come from.
- Aegir backend APIs overview — the task round-trip that invokes all of this.
- Commands reference — the consolidated CLI table.