Skip to content

1.1 Step | PrepareOptRoot

Omisen edited this page Aug 15, 2026 · 5 revisions

The installer's first real step: it creates /opt/odoo (the ctx.odoo_home value) when missing, reversibly. It lives in src/steps/prepare_opt_root.rs. It is the simplest possible mutation — a directory — chosen deliberately as the reference model for the steps that follow (user, packages, postgres…): all of them are richer variants of the same snapshot → run → undo shape.

It comes out of correction C4: the mkdir that Bash hid inside check_disk becomes a step with an undo here. See 1. Checks and 0. Engine.


Life cycle

Since instances exist the step owns two levels, snapshotted separately because ownership is per level: the shared root /opt/odoo, which every instance lives under and is created once, and — for a named instance — that instance's own home, which is also the home of its system user. For the unnamed instance the two are the same directory and everything below behaves exactly as it always did. One PreState for both would have made a second instance's rollback either destroy the shared root or spare its own home.

Phase Behaviour
snapshot for each level: it exists → PreState::Preexisting; otherwise Untracked (run has not created it yet). Plus, for a named instance, whether the shared root has to be widened — see below
run Preexisting → no-op (not ours: neither owner nor permissions); Untrackedmkdir + 0755, then PreState::CreatedByUs before anything else. If the user already exists, the directory is handed over at once (chown + 0750). A named instance leaves the shared root root-owned 0755: handing it to one instance's user would give that user the ground the others stand on. In dry_run it only logs
undo deepest first — the instance home, then the shared root. Acts only on CreatedByUs and removes a directory only if empty (rmdir, never rm -rf). With another instance installed the shared root is left alone whoever created it

All three PreStates are handled and tested. The post-run PreState is serialised (snapshot_value) and persisted, so the rollback knows whether the directory is ours — including a rollback run days later from the manifest, which rebuilds this step through rehydrate.


Details that matter

Owned by root, for the moment

At this point the odoo user does not exist yet — it is created by the next step. So the directory is created owned by root with 0755 permissions, and the chown odoo:odoo happens in the user-creation step, which is the correct order. The dependency is documented in the code.

mkdir, not mkdir -p

Only the missing level is created (create_dir, not create_dir_all). The rollback must restore exactly what we added: if we also created /opt we would not remove it again, leaving a residue. The parent (/opt) is assumed to exist on the system.

The shared root has to be walkable by everybody under it

The unnamed instance's home is /opt/odoo, so it is handed over as odoo:odoo 0750 — right while it is one instance's private home, wrong the moment a second one moves in. Adding --instance cliente-x to that machine creates the user odoo-cliente-x, which is neither the owner nor in the group: with 0750 it cannot traverse /opt/odoo, so it never reaches its own home.

That is the migration path of every existing customer, and in the field it failed three steps later with mkdir: cannot create directory '/opt/odoo': Permission denied — a directory that exists, which sends whoever reads it looking for the wrong problem.

So a named instance that finds the shared root not traversable widens it by exactly one bit (o+x: walk through, still not list) and records the mode it found. The widening is an artifact like any other — the same treatment the nginx default site gets: we touch what the customer owns only by writing down what it was. The undo puts the mode back, and only if it is still the one we left; if no traversal is needed any more, which is the case once the only instance left is the one that owns the root.

If the mode cannot even be read, the installation stops there, before touching anything, with a message that names the permission. An unreadable mode is “I do not know”, never “it is fine”: widening without having read what was there is a mutation with no undo.

undo is best-effort, never destructive

  • Idempotent: if the directory is already gone → no-op.
  • It removes only if empty. If it contains artifacts from later steps, those steps have their own undos which run first (reverse order), so by the time PrepareOptRoot's undo runs the directory should be empty. If it is not → a warning, and it is not forced. Never rm -rf.
  • A Preexisting directory is never removed: it is not ours to destroy.

Place in the flow

parse config
  └─▶ preflight checks (non-mutating)
       └─▶ Installer::execute([ PrepareOptRoot, … ])   ← first mutation, reversible

It is the first of the 25 steps, which means its undo is the last one to run in a rollback — the reason its directory should be empty by then.


Design notes

  • An exemplary, deliberately minimal implementation: if the pattern is clean here, the complex steps copy it.
  • No .unwrap()/.expect(): every failure is a Result.
  • Round-trip tests: CreatedByUs (creates → removes), Preexisting (survives the undo), non-empty directory (the undo does not force), dry_run (creates nothing, undo is a no-op).

If the odoo user already exists, the home is handed over here

owned root is not the right state for the home: it is a waiting state, and it makes sense only while the user does not exist. When the user is already there — a distro package, the leftover of a manual setup, a company convention — there is nothing to wait for, and leaving it to root broke the installation three steps later: CreateOdooUser sees a Preexisting user and returns immediately without the chown (a deliberate choice: we do not touch what is not ours), and SetupCacheDir then runs sudo -u odoo mkdir -p /opt/odoo/.cache on a root-owned directory → Permission denied, with an error that says nothing about the cause.

The handover belongs here and not there because here is where the information is: this step knows it created the directory itself. CreateOdooUser cannot know that — at its snapshot the home always exists, because we just created it — so any attempt to infer it from there would be a check that in production always answers the same way.

The opposite case — a pre-existing root-owned home with an already-existing user — stays out: that directory is not ours and does not get chowned. There the installation stops with an explicit precondition in CreateOdooUser.

Clone this wiki locally