Skip to content

1.2 Step | CreateOdooUser

Omisen edited this page Aug 14, 2026 · 5 revisions

Creates the odoo system user (useradd --system), its dedicated group, and makes odoo:odoo the owner of the /opt/odoo home. Reversible. It lives in src/steps/create_odoo_user.rs. It follows the model of 1.1 PrepareOptRoot on a richer resource (user + group + ownership).

A port of create_odoo_user / _verify_odoo_user_homedir from lib/system.sh.


Life cycle

Phase Behaviour
snapshot detects two independent things: (1) does the user already exist? → Preexisting/Untracked; (2) the current owner of /opt/odoo before our chown (saved for the undo). Then it applies the precondition described below
run Preexisting → skip useradd, no aggressive chown; absent → useradd + an explicit chown odoo:odoo + chmod 0750, then CreatedByUs. dry_run → log only
undo CreatedByUs only: userdel without -r + groupdel (best-effort) + restore the home's original owner. Preexisting/Untracked → no-op

useradd arguments (least privilege, as in Bash):

useradd --system --create-home --home-dir /opt/odoo --user-group --shell /bin/false odoo

--shell /bin/false → no interactive shell; --system → UID < 1000 with no password; --user-group → a dedicated odoo group.


Coordinating with PrepareOptRoot (the delicate point)

This is the first case of two steps both touching the same artifact (/opt/odoo). One creates it (PrepareOptRoot), the other becomes its owner (CreateOdooUser). The rule that unties the knot:

Every step owns the removal of what it created.

Concretely:

  • CreateOdooUser.undo runs userdel without -r: it does NOT remove the home. With -r it would delete /opt/odoo, which is PrepareOptRoot's business.
  • the home is removed by PrepareOptRoot.undo, which runs later in the reverse order (SetupLogDir → CreateOdooUser → PrepareOptRoot).
  • if the home was Preexisting (not ours) and our chown changed its owner, undo restores the original owner saved at snapshot — so it does not stay owned by a user we are deleting.

Invariant from CLAUDE.md: never userdel -r on a Preexisting user. Here the undo acts only on CreatedByUs users, and even then without -r. A pre-existing user is never touched.

This shape — ownership of removal plus reverse order — is the model for future cases where two steps share an artifact (for instance the role that owns the database in PostgreSQL).


The testable SystemOps boundary

The privileged commands (useradd/userdel/groupdel/chown/chmod) are not called directly: they go through a SystemOps trait held inside the step. That way the Step trait and the engine do not change.

Impl Use
RealSystemOps production: useradd via Command, chown/mkdir via nix + std::fs
MockSystemOps (tests) records which operation would run and with which arguments, without root

The tests check the decision logic (the PreState branch, the exact useradd arguments, userdel without -r, owner restoration) without executing anything and without touching the system.


Persisted snapshot

struct CreateUserSnapshot {
    user_prestate: PreState,               // Preexisting | Untracked | CreatedByUs
    home_original_owner: Option<OwnerId>,  // the home's owner before our chown
}

Serialised (snapshot_value) and persisted, and read back by rehydrate: the rollback knows whether the user is ours and whom to restore the home to — even when it runs from disk months later.


Design notes

  • No aggression towards pre-existing things: if the user was already there, no useradd and no chown — it is not ours to reconfigure.
  • useradd does not re-chown a pre-existing home: the chown odoo:odoo is explicit, after creation (like _verify_odoo_user_homedir in Bash).
  • Every undo step is best-effort: a failure logs a warn and carries on, without blocking the other steps' cleanup.
  • Tests: CreatedByUs (useradd+chown / userdel without -r), Preexisting (never touched), owner restoration on a pre-existing home, dry_run (no operation).

Precondition: a pre-existing user with a home it cannot use

If the user already exists and /opt/odoo belongs to root without having been created by this installation, the installer stops here, before mutating.

The reason: this step does not chown (the directory is not ours), and three steps later SetupCacheDir would run sudo -u odoo mkdir -p /opt/odoo/.cache on a root-owned directory. The error used to be a Permission denied on a mkdir, naming neither the cause (the home belongs to root) nor the condition that makes it a problem (the user already exists, so nobody hands it over).

It is a precondition, not an undo: it is not a mutation to reverse, it is a mutation not to begin. The message names the home, its owner and the two ways out — chown if that directory is meant for Odoo, or remove it if it is a leftover.

The case where we created the home never reaches here: the handover already happened in PrepareOptRoot, the only place that knows who created it.

Clone this wiki locally