Provision backend internals
Provision is the Drush-8 command package that does every server-side action
Aegir takes — write vhosts, create databases, run installs, take backups,
deploy tarballs. It is a Drush commandfile set, not a Drupal module at
runtime: provision.info declares module identity (version=7.x-3.x,
read back by provision_version(), provision.inc:1371) but nothing
bootstraps it inside Drupal. On a BOA box it lives at
/var/aegir/.drush/sys/provision on the Master and
/data/disk/<oN>/.drush/sys/provision per Octopus satellite — cloned from
omega8cc/provision.git at the branch selected by ${_BRANCH_PRN}
(boa-private/lib/functions/master.sh.inc:793,
satellite.sh.inc:3242-3244). The historical bare .drush/provision path is
actively removed by the same code paths; never reference it.
Everything here runs as the unprivileged instance user (aegir or oN)
under the omega8cc Drush 8 fork
(Drush fork internals), and must stay parse-
and runtime-safe on PHP 5.6 — the CLI floor on legacy servers. No ??,
no scalar types, no arrow functions, no 7.x-only stdlib.
Entry point: the task round-trip
The Hostmaster frontend reaches provision through exactly one door.
drush_hosting_task() (hosting/task.hosting.inc:189) re-exports the
node's state as a backend context via provision-save
(task.hosting.inc:208), defaults the verb to
'provision-' . $task->task_type (task.hosting.inc:223), and dispatches it
with provision_backend_invoke() (task.hosting.inc:226). That helper is
provision's own (provision.inc:1274) — a drush_invoke_process() wrapper
with integrate + dispatch-using-alias, also used backend-side whenever one
provision command needs another (e.g. deploy unlocking the old platform).
Results and log lines ride the Drush backend channel back to the task node.
The frontend half of this contract is on the
Hosting API leaf.
Two gates run before any provision command, from provision_drush_init()
(provision.drush.inc:51):
_provision_drush_check_user()(:67) — refuses to run anyprovision-*command as root (PROVISION_IS_ROOT). Root work (BOA's own scripts, autosymlink) is always delegated, never done in-process._provision_drush_check_load()(:80) —exit(1)whenprovision_load_critical()(:489) trips: load₁ abovencpus × critical_load_multiplier(default 5,:466) or a flatcritical_load_thresholdof 10 when the CPU count is unknown.provision_count_cpus()(:445) prefers BOA's daily-generated/data/all/cpuinfoover/proc/cpuinfo.
provision_drush_init() also seeds the context registry: it loads the
invocation alias into d() as the root object, honouring the #name
option so provision-save can address a context whose alias file does not
exist yet (provision.drush.inc:55-56).
Repo layout: four commandfiles, per-verb orchestrators
Drush discovers four commandfiles inside the clone, and that split is the architecture:
| Commandfile | File | Role |
|---|---|---|
provision |
provision.drush.inc (521 L) |
command registry, init gates, misc helpers |
provision_drupal |
platform/provision_drupal.drush.inc |
everything Drupal: settings.php, site dirs, packages map, engines |
db |
db/db.drush.inc |
DB service: registers the db service + its autoload root |
http |
http/http.drush.inc |
HTTP service: registers the http service + its autoload root |
Per-command logic lives in orchestrator files loaded by Drush's
command-include rule (drush/includes/command.inc:1715,1723): for command
a-b-c the parts are reversed and dot-joined, and any c.b.a.inc sitting
next to a registered commandfile is include_onced at dispatch. So
provision-verify pulls in platform/verify.provision.inc;
provision-login-reset pulls in platform/reset.login.provision.inc;
provision-backup-delete pulls in platform/delete.backup.provision.inc;
hostmaster-install pulls in install.hostmaster.inc at the repo root.
The same verb name can exist in several subtrees and all get included —
deploy.provision.inc exists in platform/, db/ and http/, each
implementing its own slice as hooks (next section).
Counts at HEAD: platform/ carries 17 *.provision.inc orchestrators plus
provision_drupal.drush.inc and the platform/drupal/ engine set (38
*.inc engines + README.md); db/ and http/ carry 7 orchestrators each.
The Drush command registry
provision_drush_command() (provision.drush.inc:91) is the single
registry. The file-head docblock (provision.drush.inc:2-39) is the
canonical command-and-hooks enumeration — there is no api.drush.inc
in this fork; that filename is an upstream-doc fiction.
| Command | Bootstrap | Orchestrators |
|---|---|---|
provision-save |
DRUSH |
owner callback drush_provision_save() (:371) |
provision-verify (aliases v, pv, verify) |
DRUSH |
drush_provision_verify() (:385) + platform/verify.provision.inc + service verify_*_cmd |
provision-install |
DRUPAL_ROOT |
platform/install.provision.inc, db/, http/, engine install_N |
provision-install-backend (hidden) |
DRUPAL_SITE |
second-phase install worker |
provision-import |
DRUPAL_ROOT |
platform/import.provision.inc, engine import_N |
provision-backup |
DRUPAL_ROOT |
platform/backup.provision.inc, db/backup.provision.inc |
provision-backup-delete |
DRUSH |
platform/delete.backup.provision.inc |
provision-enable / provision-disable |
DRUPAL_ROOT |
platform/ + http/ enable/disable orchestrators |
provision-lock / provision-unlock |
DRUPAL_ROOT |
platform/lock.provision.inc / unlock.provision.inc |
provision-dlock / provision-dunlock |
DRUSH |
platform/dlock.provision.inc / dunlock.provision.inc |
provision-restore |
DRUPAL_ROOT |
platform/ + db/ restore orchestrators |
provision-deploy |
DRUPAL_ROOT |
deploy.provision.inc in platform/, db/, http/ |
provision-migrate |
DRUPAL_ROOT |
platform/migrate.provision.inc (funnels into deploy) |
provision-clone |
DRUPAL_ROOT |
platform/clone.provision.inc (funnels into deploy) |
provision-delete |
DRUSH |
delete.provision.inc in platform/, db/, http/ |
provision-login-reset |
DRUPAL_ROOT |
platform/reset.login.provision.inc, provision_generate_login_reset() (:516) |
hostmaster-install |
DRUSH |
install.hostmaster.inc:195 |
hostmaster-migrate |
DRUPAL_ROOT |
migrate.hostmaster.inc:172 |
hostmaster-uninstall |
DRUPAL_SITE |
uninstall.hostmaster.inc:37 |
backend-parse |
DRUSH |
parse.backend.inc |
Details that matter when adding or touching a command:
provision-savemerges its option table from the three context classes' staticoption_documentation()(provision.drush.inc:100-102) and setsallow-additional-options— unknown options are accepted and persisted, which is how extension modules smuggle their per-context properties in without touching the registry.- Bootstrap levels are load-bearing.
provision-verifyandprovision-deleterun atDRUSH_BOOTSTRAP_DRUSHbecause the target may not be a bootstrappable site; the site-level bootstrap is then performed manually inside the validate hook (see verify below).provision-dlock/provision-dunlockwere explicitly lowered toDRUSHwith an in-callback root bootstrap, because dispatching atDRUPAL_ROOTon an unlocked D10+ platform fatals (modern typed symfony/console vs Drush 8's untyped adapter). - Most
provision-*verbs have no owner callback at all. The work happens entirely in hooks contributed by the other commandfiles — that is the pluggability model. provision-statsfrom the upstream docblock is an optional external module; it is not registered here. Do not document it as a shipped verb.
Hook choreography per command
For command provision-X, Drush 8 calls, per commandfile CF, in order:
drush_CF_provision_X_validate() → drush_CF_pre_provision_X() →
drush_CF_provision_X() → drush_CF_post_provision_X(), with matching
*_rollback() functions unwound in reverse on failure. Every orchestrator
file in this repo is just a bag of these implementations —
drush_provision_drupal_pre_provision_verify() parses as commandfile
provision_drupal, phase pre, command provision-verify. Third-party
provision extensions hook the same chain the same way; that surface is
enumerated in provision.api.php and worked through on the
Provision API leaf.
verify
drush_provision_drupal_provision_verify_validate()(platform/verify.provision.inc:13) — for site contexts, performs the bootstrap theDRUSH-level dispatch skipped:DRUSH_BOOTSTRAP_DRUPAL_SITE, writesettings.php(_provision_drupal_create_settings_file(),provision_drupal.drush.inc:277), push site files (provision_drupal_push_site(),:204), then FULL bootstrap below D12 andDRUPAL_CONFIGURATIONfor D12+.drush_provision_drupal_pre_provision_verify()(:36) — server contexts: createconfig/,backups/,clients/underaegir_root, seedconfig/includes/global.incviaProvision_Config_Global_Settings; platform and site branches follow in the same function.- Owner callback
drush_provision_verify()(provision.drush.inc:385) re-runsprovision-savefor the context, thend()->command_invoke('verify')— the context/service fan-out (verify_{type}_cmdon every subscribed service; the Nginx driver's server pass ishttp/Provision/Service/http/nginx.php:102). This is where vhosts and server configs are actually re-rendered. drush_provision_drupal_post_provision_verify()(:485) caches the verified state back into the site/platform drushrc.
install
All in platform/install.provision.inc: validate :17, pre :66
(rollback :74), implementation :102, post :442.
- The implementation branches on Drupal major. D8+ installs run
site-installthrough the platform's site-local Drush (vendor/drush/drush/drush.phpunder the app root found byfindDrupalAppRoot()), because Aegir's Drush 8 cannot drive a modern installer. The Aegir core/console patches are reverted for the installer and re-applied after — gated by the/data/conf/_no_codebase_lock_unlock.ctrlkill-switch — and a failed or degraded install re-locks before returning so a shared platform vendor is never left in the state the patched web cannot serve. A non-zerosite-installexit registersPROVISION_DRUPAL_INSTALL_FAILEDand returnsFALSE, so the frontend marks the task failed and rollback runs. - The post hook rebuilds caches (
_provision_drupal_rebuild_d8plus(),provision_drupal.drush.inc:665, on D10+ via site-localdrush cr;_provision_drupal_rebuild_caches(),:629, below), saves the packages map (provision_drupal_system_map(),:961), creates the client symlink, and includes thecron_keyengine. - BOA-specific tail:
_provision_drupal_native_symlink()(provision_drupal.drush.inc:503), called at the end of the post hook, moves the new site'sfiles/andprivate/into the per-account static store and symlinks them back — by delegating to the rootautosymlinktool through a hardened wrapper, since the store is root-managed and provision is unprivileged. Scoped to/data/disk/accounts only, warn-not-fail, with box-wide (/data/conf/disable_native_files_symlink.cnf) and per-account (static/control/no_native_files_symlink.info) kill-switches, and fail-closed token validation before anything reaches the root tool. The clone (platform/clone.provision.inc) and migrate (platform/migrate.provision.inc) paths call the same helper withforce_unshareto re-home inherited stores after cross-name copies.
deploy — the engine under clone / migrate / restore
platform/deploy.provision.inc: validate :24, pre :53 (unpack the
backup; rollback :149), implementation :174, post :182.
Clone, migrate and restore all end up here — deploy is "take a site
package, land it at this URI on this platform, run the upgrade path".
The post hook is the dangerous part on modern Drupal, and three mechanisms in it are deliberate:
- Chmod-only exec window.
updatedbon D10+ runs through the site-local Drush and recompiles the service container. The container must compile in the same patched state the web serves, so the post hook never reverts the Aegir patches for it — it flips only the exec bits via_provision_drupal_chmod_drush_vnd_for_cr()(provision_drupal.drush.inc:754) and restores the at-rest0400posture afterwards. Compiling in the reverted state is what used to bakeDrush\Log\DrushLoginto the persisted container and fatal every site on the platform. - Old-platform unlock/re-verify. Cross-platform moves unlock the source
platform before
updatedband re-verify (re-lock) it after, both viaprovision_backend_invoke()against theold_platformoption (deploy.provision.inc:209-217and:339-343). - Update-tracker clear.
_provision_drupal_clear_update_fetch_tasks()(deploy.provision.inc:467) drops the re-importedupdate_fetch_taskkey-value collection after every deploy-updatedb path, so update.module never wedges on orphaned tracker rows. Non-fatal on every path.
When a D10+ platform has no executable site-local Drush, the post hook
skips updatedb with an explicit recovery warning instead of falling back
to Aegir's Drush 8 (which cannot bootstrap D10+ and would only fail
noisily).
Version engines
provision_drupal_drush_engine_drupal() (provision_drupal.drush.inc:32)
declares the drupal engine types clear, cron_key, deploy, import,
install, packages, verify. drush_include_engine('drupal', 'install')
and friends resolve to platform/drupal/<type>[_<major>].inc — 38 engine
*.inc files that run inside the target site's own bootstrap, so each
must be written for its Drupal major (see platform/drupal/README.md). This is where per-major
divergence belongs; the orchestrators stay version-agnostic.
Finally, provision_drupal_drush_exit() (provision_drupal.drush.inc:52)
persists successful option state to the site's drushrc.php after any
provision-* command, unless provision_save_config is off.
The context system: d() and provision-save
Contexts are provision's data model: named server/platform/site objects
persisted as Drush alias files. There is no database backend-side — the
alias files under ~/.drush/ are the store, and the frontend refreshes
them through provision-save before every task.
d()(provision.context.inc:23) — the accessor. A static registry keyed by normalised alias name (provision_normalise_context_name(),provision.inc:1416).d('@name')returns (by reference) the context, creating it on first access viaprovision_context_factory()(provision.context.inc:113);d()returns the root object set at init;d('all')returns the whole registry. A guard refuses to run before Drush finishes bootstrapping (:41) — callingd()at file scope in a commandfile is a classic three-day bug. On creation the factory result getsmethod_invoke('init'),type_invoke('init'), then theprovision_context_alterDrush hook (:74), the supported way to swap in a context subclass.- Factory — the alias record (cached via
provision_sitealias_get_record(),:89) or the CLIcontext_typeoption picks the type; the class is literally"Provision_Context_{$type}"(:130). Provision_Context(Provision/Context.php:12) — magic__get/__setback a$propertiesarray, which is exactly whatwrite_alias()(:240) hands toProvision_Config_Drushrc_Aliasto write the alias file.is_oid($name)(:83) marks a property as a context reference: reading it returnsd($value), which is howd('@site')->platform->server->aegir_rootchains lazily across alias files.setProperty($field, $default)(:215) is the init-time idiom: take the CLI/alias option if present, else the default.- Invocation plumbing —
method_invoke()(:136) calls the method if it exists (via the staticprovision::method_invoke(),provision.inc:1449— silent no-op otherwise) and then mirrors the call onto all subscribed services (services_invoke(),:290).type_invoke('save')→save_site()etc.;command_invoke('verify')→verify_site_cmd()etc. These name conventions are the extension points service classes implement. - Subclasses (
Provision/Context/):server.phpderivesbackup_path,config_path,include_path,clients_pathfromaegir_root(:63-66) and spawns services;platform.phpsetsparent_key = 'server'and ownsroot/makefile;site.phpsetsparent_key = 'platform', ownsuri, computessite_path = root . '/sites/' . uri, and runsprovision_auto_fix_platform_root()so a Composer-managed web root is never mistaken for the platform root. Each class's staticoption_documentation()feeds theprovision-saveoption table — adding a persisted property means adding it there. drush_provision_save()(provision.drush.inc:371) —type_invoke('save')+write_alias(), orunlink()of the alias on--delete.
Services: subscription, drivers, dispatch
The service layer decouples "a site needs a database" from "this server runs Percona":
- Declaration.
hook_provision_services()—db(db/db.drush.inc:16) andhttp(http/http.drush.inc:24). The hook returnsarray('<service>' => <default type>). - Driver spawn (server side).
Provision_Context_server::load_services()(Provision/Context/server.php:76) iterates declared services andspawn_service()(:86) instantiatesProvision_Service_{$service}_{$type}from the server's{$service}_service_typeproperty —http_service_type=nginx→Provision_Service_http_nginx,db_service_type=mysql→Provision_Service_db_mysql. No type →Provision_Service_null. The type value is set by the frontend when it saves the server context. - Subscription (consumer side). Static
subscribe_<type>methods run during context init (Provision/Context.php:201):Provision_Service_db::subscribe_site()(db/Provision/Service/db.php:10) wiresdb_server(default@server_master) andservice_subscribe('db', …);Provision_Service_http::subscribe_platform()(http/Provision/Service/http.php:36) wiresweb_serverthe same way.Context::service('db')(Provision/Context.php:270) then resolves through the subscription, else up theparent_keychain, ending at@server_master's spawned drivers. - Base classes.
Provision_Service(Provision/Service.php:10) extendsProvision_ChainedState(Provision/ChainedState.php) — the->succeed('…')->fail('…')chaining idiom shared withProvision_FileSystem, reachable as theprovision_file()singleton (provision.file.inc:12). A service carries a$configsregistry of config classes per context level,restart()(:276) with a driver-supplieddefault_restart_cmd(), and theverify_server_cmd/verify_platform_cmd/verify_site_cmdfamily the verify fan-out calls. - Driver chains. DB:
Provision_Service_db→db/pdo.php→db/mysql.php— MySQL/Percona is the only driver BOA runs (db/mysql/mysql_service.incis an empty legacy shim). HTTP:http/public.php→http/nginx.php(registersProvision_Config_Nginx_{Server,Inc_Server,Site}, setssatellite_mode = 'boa'and thenginx_has_*capability properties ininit_server();http/nginx/ssl.phpon top for SSL). Theapache*,clusterandpackdrivers are pre-fork upstream residue — retained, not exercised by BOA; do not treat their behaviour as authority.
Class loading
provision.inc:10 requires vendor/autoload.php (Composer-pinned deps —
Symfony Process for provision_process(), provision.inc:1307). The
Provision_* classes themselves resolve through a second, dedicated
\Composer\Autoload\ClassLoader created by provision_autoload()
(provision.inc:19): provision_autoload_register_prefix('Provision_', …)
(:42) is called with the repo root at provision.inc:66, and again by
db_provision_register_autoload() (db/db.drush.inc:55) and
http_provision_register_autoload() (http/http.drush.inc:13) to add
db/ and http/ as additional roots for the same prefix. That multi-root
PEAR-style mapping is how Provision_Service_http_nginx resolves to
http/Provision/Service/http/nginx.php. The psr-0 entry in
composer.json:28-31 maps the prefix one directory too deep
(Provision/Provision/…) and never matches — the runtime registration is
the operative mechanism. Extensions register their own class roots the
same way (the db/http commandfiles are the template).
Config render pipeline
Provision_Config (Provision/Config.php) is the template writer every
generated file goes through. write() (:195): ensure directory →
load_template() → process() → render_template() (:169) →
atomic-ish write with $mode/$group applied. Three alter points, all
plain Drush hooks:
hook_provision_config_load_templates()/…_load_templates_alter()(Config.php:120,126) — replace the template file outright; otherwise the class walks its own inheritance chain looking for$this->templatenext to each class file.hook_provision_config_variables_alter()(:173) — mutate the variable bag just beforeextract()+ bufferedeval('?>' . $template).- The Nginx config classes additionally append
drush_command_invoke_all('provision_nginx_server_config' / 'provision_nginx_vhost_config')output into$data['extra_config'](http/Provision/Config/Nginx/Server.php:10,Site.php:10, and theSsl/twins) — free-form config injection without touching templates.
The template set itself (server.tpl.php, vhost*.tpl.php,
vhost_include.tpl.php) and what BOA renders into it are web-stack
material, indexed from the topic chapter.
The API surface: provision.api.php
provision.api.php (577 L) is documentation-as-code: the $options
recognised from drushrc (provision_backup_suffix,
provision_composer_install_platforms,
provision_mysqldump_suppress_gtid_restore, …) in the file head
(:11-57), then every hook — hook_provision_services() (:82),
hook_provision_context_alter() (:96), the Nginx/Apache render hooks
(:220, :239, :260), the template/variable alters (:274-:311), the
directory/permission/settings alters (:354-:407),
hook_provision_deploy_options_alter() (:420),
hook_provision_prepare_environment() (:458), and the DB
name/user/options alters (:505-:552). If you are extending provision
rather than maintaining it, start on the
Provision API leaf, which pairs this surface with a
working extension skeleton; the frontend counterpart hooks are on
Hosting API, and the paired-module pattern is on
Extending Aegir.
Codebase lock state (BOA-fork addition)
The D10/D11 "Aegir-patched platform" model adds a state machine the upstream never had, and several commands consult it:
provision_check_codebase_status()(provision.inc:153) — setsd()->appRoot,d()->symfonyDirand theisCorePatched/isConsolePatched/isValidatorPatchedsentinels by inspecting the actual tree.provision_drush_local_lock_state()(provision.inc:243) — deriveslocked | unlocked | partial | unknownpurely from on-disk reality: patch sentinels plus the0400(locked) /0775(unlocked) bits onvendor/drushand the symfony/consoleInput/Styledirs, resolving local-vs-above vendor layouts. No marker files.- Lock/unlock bodies at
provision.inc:677/:300; theprovision-dlock/provision-dunlockcommands are thin wrappers over the same canonical functions (platform/dlock.provision.inc:21-60), so the UI task and Platform Verify share one battle-tested path, and re-running in the already-target state is a clean no-op.
Supporting and legacy files
provision.context.inc(133 L) —d(), the factory, the alias cache; included from the tail ofprovision.inc(:1456-1458) together withprovision.service.inc(5-line compat shim) andprovision.file.inc.parse.backend.inc—drush_provision_backend_parse(), thebackend-parsecallback that renders--backendJSON from stdin human-readable.- Pre-fork residue retained in the fork but unused by BOA: the
aegir*.makefamily (BOA builds Hostmaster from its own make chain — see Hostmaster frontend internals),upgrade.sh.txt,debian/,scripts/ci-aegir-*,Vagrantfile,provision-tests/, and the Apache config/service classes. Verify against BOA code before citing any of them; retired pieces are tracked in Discontinued features.
Related
- Aegir backend APIs — topic chapter — the task round-trip diagram this page zooms into.
- Provision API — the hook surface for extension authors, with a skeleton.
- Hosting module suite map — the frontend modules whose task nodes become these commands.
- Drush fork internals — the Drush 8 fork provision runs under, including the extension deny-filter.
- Install & staged-setup internals — how this clone lands on a box during AegirSetup.
- Commands reference and
Variables reference — consolidated
CLI and
_VARtables.