Skip to content

Powered by Grav

Entity & service model

Entity & service model

Ægir's state lives in a handful of Drupal node types: server, platform, site, client, plus the task node that drives every transition. This page is the operator-side reference for those entities — their on-disk shape, their real status constants, their permissions, and the provision-* backend each one maps to. The task mechanics are in Task queue engine.

Server / service / service-type

A server is any host that provides one or more services; each service has a service type (the implementation). This three-layer model lets Ægir express heterogeneous topologies without hardcoding "MySQL on localhost, Nginx everywhere." In BOA practice most installs are flat (web + db on one Devuan VM), but the model still carries the topology.

TXT
Server (host)
  ├── Service: HTTP
  │     └── Service type: Nginx        ← BOA always uses Nginx
  └── Service: Database
        └── Service type: MySQL/Percona ← BOA always uses Percona

The task queue is not a server service. Ægir's hook_hosting_service types are web / db / SSL only; there is no "queue" service type. BOA processes tasks with a cron job on the Master (runner.shhosting-dispatch), not a server-attached daemon.

The hosting modules behind this:

  • hosting_server — base server node type + admin UI; hosting.ip.inc handles per-server multi-IP.
  • hosting_db_server — DB-server node form + the MySQL service interface. Every BOA host has one db_server pointing at local Percona; remote DB hosts use the same node type.
  • hosting_web_server + hosting_nginx + hosting_ssl — the web-service backend. hosting_nginx binds the abstract web service to the concrete Nginx config generation in lib/functions/nginx.sh.inc; hosting_ssl runs cert issuance/renewal/toggle as Ægir tasks.

Each server node has a matching Drush alias on disk under .drush/: server_master.alias.drushrc.php (master server) and server_localhost.alias.drushrc.php (db server, usually the same host). These carry the context data Provision reads for every provision-* call.

The distributed-topology modules hosting_web_cluster / hosting_web_pack ship in the fork but are N-graded in BOA — not exercised on a typical install (see Discontinued features).

Platform

A platform is a Drupal codebase on disk, hosting one or more sites.

Where a platform lives depends on how it was created:

  • A custom (UI-uploaded) platform lives under the instance's hosting_platform_base_path, <root>/static/ — any path under /data/disk/oN/static/ is valid. A per-platform subdirectory of static/platforms/ is the recommended layout, purely to keep static/'s standard entries (control/, usage, trash, files, goaccess) from being buried under platform directories.
  • A BOA-bundled platform lives under /data/disk/oN/distro/NNN/<codebase>/, where NNN is a zero-padded numeric revision counter (the same _nextnum mechanism as the Hostmaster codebase, not a catalogue symbol) and <codebase> is the built Drupal tree.

Which codebases get built is selected by the per-instance platform token list (_PLATFORMS_LIST / static/control/platforms.info, e.g. DE2 DX5 SOC UC7), but those tokens never appear as the directory name. The tokens are BOA's own platform symbols:

  • base Drupal cores (DE1DE3 for D11.x, DX0DX6 for D10.x, DL9/DL7/DL6 for D9/D7/D6),
  • plus distribution symbols (CK1/CK2/CK3, SOC, UC7, …),
  • or the keyword ALL for every available platform.

A platform tree is the standard Drupal layout — index.php, modules/, themes/, profiles/, and a sites/ tree with all/ (shared modules/themes/drush) and per-site folders.

Platform states surfaced in the UI:

  • Queued — new, awaiting Verify.
  • Verified — usable.
  • Locked — no new sites; existing sites still work (HOSTING_PLATFORM_LOCKED).
  • Deleted — soft-deleted, record kept.

Note that "locked" is a platform concept: hosting_platform.module defines HOSTING_PLATFORM_LOCKED = -1. There is no site-level locked state.

BOA treats a platform as immutable once any site is hosted on it. In-place Composer/Drush changes to a live platform's code (adding modules, upgrading core) are unsupported; the supported lifecycle is build a new platform, migrate sites to it, delete the old. See Migration & cloning.

Site

A site is a deployed Drupal instance, referencing a platform, a DB server, a client (ownership), and zero or more aliases.

Status constants

hosting_site.module defines exactly four status constants — there is no INSTALLED and no site-level LOCKED:

PHP
HOSTING_SITE_DELETED   = -2   // soft-deleted (record kept, files gone)
HOSTING_SITE_DISABLED  = -1   // operator-disabled
HOSTING_SITE_QUEUED    =  0   // new, awaiting Install
HOSTING_SITE_ENABLED   =  1   // installed + enabled (the "live" state)

A site reaches the live state as HOSTING_SITE_ENABLED (value 1), not a fabricated HOSTING_SITE_INSTALLED. Each transition is driven by a Task node:

TXT
QUEUED   ─→ (Install task)  ─→ ENABLED
ENABLED  ─→ (Disable task)  ─→ DISABLED
ENABLED  ─→ (Delete task)   ─→ DELETED
ENABLED  ─→ (Migrate task)  ─→ ENABLED   (on a new platform)
ENABLED  ─→ (Backup task)   ─→ ENABLED   (tarball generated)

On-disk site files

Per site, under <platform-root>/sites/<domain>/:

  • settings.php — Drupal site settings, BOA-generated.
  • drushrc.php — Ægir Drush config.
  • modules/ / themes/ / files/ / private/.

On current BOA, a new site installed on an Octopus (/data/disk) account gets files/ and private/ moved into the owning account's static store — /data/disk/<account>/static/files/<domain>/{files,private} — and replaced with symlinks as part of installation: Provision's install hook delegates the root-privileged move to the autosymlink tool.

This is scoped to /data/disk accounts only (the master hostmaster account keeps plain directories) and fail-open — any failure leaves plain real directories and the install still succeeds.

Kill-switches:

  • box-wide /data/conf/disable_native_files_symlink.cnf, or
  • per-account static/control/no_native_files_symlink.info.

Existing sites whose files/private are still real directories are not auto-converted by a normal Install/Verify. Full subsystem detail — store layout, tools, nightly automation — in Files symlinking.

The optional per-site override surface is a boa_site_control.ini placed in the site's modules/ subfolder (sites/<domain>/modules/boa_site_control.ini) — copied from the /data/conf/default.boa_site_control.ini template, which a Verify seeds into the site as default.boa_site_control.ini for reference.

Edits take ~60 s to apply (FPM opcache). Use that INI for overrides; settings.php itself is regenerated on every Verify, so direct edits to it are wiped.

Permissions & ownership (BOA values)

The per-site permissions matrix as BOA actually sets it (verified against provision-private, not the upstream architecture doc — BOA diverges from upstream on two of these):

Path Ownership Mode Note
settings.php aegir:www-data 0440 (-r--r-----) Web server reads, cannot write.
drushrc.php aegir:aegir 0440 (-r--r-----) Set by Provision/Config/Drushrc.php ($mode = 0440).
files/ aegir:www-data 02775 Setgid for group inheritance.
files/* www-data:www-data 0644 Web-server-created files.
private/ aegir:www-data 02775 Same model as files/.
sites/all/{modules,themes,libraries} aegir:aegir 02775 Set by provision_drupal.drush.inc.

Two upstream-vs-BOA divergences to note: upstream's architecture doc lists drushrc.php as 0400 and files/ as 2770, but BOA uses 0440 and 02775 respectively. Use the BOA values above.

If permissions drift (Composer wrote with wrong ownership, etc.), the operator fix-drupal-{platform,site}-{permissions,ownership}.sh scripts restore the canonical state.

Client

A client owns one or more sites (hosting_client). It:

  • powers multi-tenancy access control — which UI user may administer which sites;
  • ties into BOA's per-Octopus tenant isolation (each Octopus instance has its own clients, isolated from other instances on the same host);
  • backs per-client quotas (hosting_quota).

A single-tenant install does not need clients; a multi-tenant install makes every external customer a client so per-client access control prevents cross-tenant visibility.

Packages (module & theme registry)

Alongside the server/platform/site/client nodes, Ægir keeps a registry of the code on your platforms:

  • A package (hosting_package, a package node) is one installable unit — a module, theme, or install profile — tracked network-wide.
  • A package instance (hosting_package_instance) records that package as it exists on one platform or site: its version, a computed version code, its installed schema version, its enabled/disabled status, and the release it came from.
  • A third small table, hosting_package_languages, tracks translations.

Nothing populates this by hand. Platform Verify enumerates the codebase and calls hosting_package_sync() / hosting_package_instance_sync() for the platform's base, sites/all, and each profile (hosting_platform.drush.inc:76-131); site Verify records a per-site instance of what that site has enabled.

The registry is the data behind two visible behaviours:

  • the migrate/clone upgrade-safety comparison (which reads instance schema versions to decide where a site may move), and
  • the platform node's module/theme listing.

On Drupal 10+ / Composer platforms the registry still populates — the schema version comes from Drupal's update.update_hook_registry service rather than the D7 API — so package tracking is not silently empty on modern cores, though the detected package set is core-version dependent.

Entity → backend mapping

Every entity has a Provision backend counterpart that runs when a task dispatches:

  • Sitesprovision/platform/{install,backup,clone,migrate,…}.provision.inc. A per-site backup writes a flat tarball <backup_path>/<uri>-<timestamp>.tar.gz (where backup_path = <aegir_root>/backups), e.g. ~/backups/foo.example.com-20260623.101500.tar.gz — not a per-site <site>/<timestamp>.tar.gz subdirectory.
  • Platformsprovision/platform/verify.provision.inc plus the platform/drupal/* per-Drupal-major helpers.
  • Serversprovision/Provision/Context/server.php plus the service-type backends under provision/db/ and provision/http/.

The CLI verbs that operate on these entities take Drush aliases, not bare domains. For example provision-clone is drush @site provision-clone @new_site @platform_name, where @new_site must be a Drush alias already generated by provision-save — a bare target domain will not work.

Renaming the Ægir hostname (renameaegirhost)

When a host's FQDN changes, the Ægir hostname baked into Drush aliases, Nginx vhosts and the Ægir DB must follow. BOA ships a single universal tool, renameaegirhost, that renames in place for either the Master or an Octopus account — the --aegir-root argument selects which:

TXT
renameaegirhost --aegir-root /var/aegir       ← BOA Master
renameaegirhost --aegir-root /data/disk/oN    ← Octopus account oN

It validates the path as a real Ægir root, then:

  1. detects the old hostname from <aegir-root>/.drush/server_master.alias.drushrc.php and the new one from hostname -f;
  2. rewrites every Drush alias under <aegir-root>/.drush/;
  3. renames and rewrites the vhost files under <aegir-root>/config/server_master/nginx/vhost.d/;
  4. renames the hostname inside the Ægir DB with serialize-safe, targeted statements — identity rows keyed by hosting_context nid, REPLACE() sweeps over plain-varchar columns, and an unserialize/replace/re-serialize pass over the variable table; a plain dump is taken first as a backup only, never sed-patched or re-imported (a blanket dump+sed would corrupt PHP-serialized byte lengths when the FQDN length changes);
  5. reloads Nginx;
  6. flushes the Drush cache and runs a 5-pass Ægir task queue to regenerate aliases, vhosts and db-host entries from the updated database.

On Octopus roots the tool also rewrites _DOMAIN (and any other old-FQDN values) in the account's /root/.oN.octopus.cnf, which drives future octopus runs. The master root has no octopus cnf, so that step no-ops there.

Pre-flight for a standalone in-place rename. The tool takes the new hostname from the system FQDN (hostname -f), and on a BOA box that identity is cnf-driven: any later barracuda run — including an install's cron-fired final phase — re-asserts _MY_HOSTN back onto the running hostname (and on a DHCP-managed host a background monitor restores it from /etc/hostname within seconds). A stale cnf therefore reverts the rename mid-flight, flipping Provision into remote-host mode against the old identity — the observed collateral is a failed self-rsync and a regenerated legacy Nginx config that fails nginx -t. Before running the tool on an existing box, set the new identity in all four places first: /etc/hostname, /etc/hosts, /etc/mailname, and _MY_HOSTN (plus _MY_FRONT) in /root/.barracuda.cnf. The tool warns when _MY_HOSTN disagrees with the detected new hostname — treat that warning as a stop sign. Migration targets are unaffected: their fresh install writes the cnf with the final FQDN.

Flags:

  • --dry-run — print planned changes, modify nothing.
  • --force-old FQDN — override the auto-detected old hostname.

Re-running renameaegirhost is convergent: already-renamed values are guard-skipped, so a repeat over a completed rename changes nothing (including a subdomain-augmenting rename, where the new FQDN contains the old one). After a partial or aborted rename the on-disk aliases may already carry the new hostname, which defeats old-hostname auto-detection (old == new, a silent no-op) — resume by pinning the old hostname explicitly: --force-old old.example.com.

The tool assumes BOA cron is stopped — the caller must stop it first.

Reading the residual report. The in-DB rename ends by reporting a count of remaining old-hostname references in the variable and task-history (hosting_task_arguments) tables. What lands there stays behind by design and is not a failure: task-history rows keep their old-hostname arguments forever, and values the rename deliberately leaves untouched — serialized variables embedding PHP objects, and values that already fail to unserialize — are each printed by name during the run for manual review. In a subdomain-augmenting rename the count also excludes any value already carrying the new hostname, so a zero residual does not prove zero old-hostname references — a mixed value holding both forms is neither rewritten nor counted, and must be found by hand if it matters.

There is no separate renamemaster tool: an earlier transitional renamemaster was folded into the universal renameaegirhost.

Migration tooling calls it automatically — an xoct import and an xmass cutover both invoke it (the cutover runs it for /var/aegir and every /data/disk/oN), so a host move regenerates contexts without a manual rename. See Migration & cloning.

  • Architecture overview — the layered model and file-system layout.
  • Task queue engine — how each status transition is actually driven.
  • Multi-Octopus model — per-instance isolation and the client/tenant mapping.
  • Database — Percona host-side setup that hosting_db_server references.
  • Nginx internals — the Nginx generation that hosting_nginx references.
  • Reference appendix — the consolidated variable and command indexes, including the provision-* backend commands this page cites (provision-clone, provision-save).

© 2026 BOA Documentation. All rights reserved.