-
Notifications
You must be signed in to change notification settings - Fork 0
1.2 Step | CreateOdooUser
Creates the
odoosystem user (useradd --system), its dedicated group, and makesodoo:odoothe owner of the/opt/odoohome. Reversible. It lives insrc/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_homedirfromlib/system.sh.
| Phase | Behaviour |
|---|---|
| snapshot | detects three independent things: (1) does the user already exist? → Preexisting/Untracked; (2) the current owner of the home before our chown; (3) its mode, likewise. Then it applies the precondition described below |
| run |
Preexisting → skip useradd, no aggressive chown; absent → useradd, then CreatedByUs immediately, then chown odoo:odoo + chmod 0750. dry_run → log only |
| undo |
CreatedByUs only: userdel without -r + groupdel (best-effort) + restore the home's original owner and mode. Preexisting/Untracked → no-op |
The order inside run is not cosmetic: the verdict is claimed the moment the user exists, not
once the home is tidy. A chown or chmod that fails after useradd would otherwise leave a system
user nobody will ever remove — the step is not in the completed list, so its undo would only run
because the engine now undoes the failing step too, and it would find Untracked and do nothing.
Owner and mode, likewise: run sets 0750 on a home that may have been somebody else's with
permissions of their choosing, and handing a directory back to its owner with permissions we picked
is not handing it back. The mode used to be missing, and it was the model that found it — once the
test double started remembering chmods, the end-to-end assertion “and that state is the virgin
system” stopped holding.
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.
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.undorunsuserdelwithout-r: it does NOT remove the home. With-rit would delete/opt/odoo, which isPrepareOptRoot'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 ourchown/chmodchanged it,undorestores the original owner and mode saved atsnapshot— so it does not stay owned by a user we are deleting, nor with permissions we chose.
Invariant from
CLAUDE.md: neveruserdel -ron aPreexistinguser. Here the undo acts only onCreatedByUsusers, 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 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.
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.
-
No aggression towards pre-existing things: if the user was already there, no
useraddand nochown— it is not ours to reconfigure. -
useradddoes not re-chown a pre-existing home: thechown odoo:odoois explicit, after creation (like_verify_odoo_user_homedirin Bash). - Every undo step is best-effort: a failure logs a
warnand 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).
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.
Start here
Key concepts
References
For developers
Technical detail — how it works inside
Steps:
- 1.1 PrepareOptRoot
- 1.2 CreateOdooUser
- 1.3 SetupLogDir
- 1.3b SetupCacheDir
- 1.4 AptPackages (delta)
- 1.5 InstallWkhtmltopdf
- 1.6 SetupPostgres
- 1.7 CreateDbRole
- 1.8 CreateDatabase
- 1.9 CloneOdooRepo
- 1.10 CreateVirtualenv
- 1.11 InstallPythonRequirements
- 1.12 GenerateConfig
- 1.12b SetupDataDir
- 1.13 InitializeOdooDatabase
- 1.14 SetupSystemd
- 1.15 Nginx (6 sub-steps)
- 1.16 WriteControlScript + PatchBashrc
Cross-cutting: