Database provisioning and administrator recovery internals
This page explains the implementation shared by the creation command and administrator recovery command. Those component READMEs own operator instructions; this page owns the developer map, lifecycle boundaries, and maintenance guidance. Comments and docstrings in the entry points describe the individual helper contracts.
Ownership and entry points
| File | Responsibility |
|---|---|
scripts/create/create_database.py |
Creation CLI, initialization workflow, and shared runtime/password helpers. |
scripts/admin/reset_admin_password.py |
Recovery CLI and the transaction that updates user ID 2. |
config/create_database.conf |
Directly maintained, non-secret INI defaults for creation's module list. |
scripts/completion/pmiverse-completion.bash |
Option and input-path completion for both commands. |
tests/test_create_database.py |
Creation orchestration and shared password/runtime helper contracts. |
tests/test_reset_admin_password.py |
Recovery target selection, credential ordering, and failure reporting. |
tests/test_script_cli.py |
Isolated executable help, completion registrations, and value suggestions. |
The recovery command imports the creation module as a helper library. Its
load_helpers() adds the checkout root to sys.path when invoked directly from
another working directory. Neither Python module imports Odoo at module load
time, runs operations on import, or requires a package installation. Preserve
that boundary so help remains available when the runtime is unavailable.
Configuration and argument precedence
Creation requires only a database name. The administrator login defaults to
admin, its display name to Administrator, and its password is generated and
saved when no password input file is supplied. Identity flags override these
defaults without changing which built-in account is configured.
| Input | Source and behavior |
|---|---|
| Odoo runtime | --config selects the instance's Odoo configuration; --odoo-root selects its checkout. Defaults target _pmisuite18. |
| Creation modules | An explicit --modules replaces config/create_database.conf; omission reads its [create_database] modules value. |
| Mandatory modules | Creation prepends base and web, validates technical names, and deduplicates in order. An explicit empty list still installs these two. |
| Password input | --admin-password-file reads an existing private file. Omission generates and saves a password without prompting. |
| Generated files | /srv/pmiverse/_pmiprivate is an absolute runtime location, independent of checkout and working directory. |
| Recovery activation | --activate explicitly permits setting active=True; omission refuses an archived target. |
ConfigParser reads module defaults with interpolation disabled. No shared shell
configuration is sourced. --help exits before defaults are read. Creation's
dry run resolves module defaults but does not import Odoo, check manifests,
connect to PostgreSQL, or access credentials. Recovery's dry run does not even
load the shared helper module. Neither preview validates runtime readiness.
Both parsers restrict database names because they also become filename components. SQL catalog checks still use query parameters; name validation is not a replacement for SQL parameterization. The shared helpers assume database names have passed a parser and filename suffixes come from trusted internal code.
Creation lifecycle
- Parse arguments and resolve modules. Preview stops here.
load_odoo()imports the selected Odoo 18 checkout and overrides inherited initialization, update, demo, language, test, HTTP/cron, and logging options. Only base initialization is initially requested.preflight()checks selected module manifests, refuses existing filestore paths (including dangling symlinks), and queries the PostgreSQL catalog.- Read a supplied password or persist a generated one before database creation.
- Call Odoo 18's
_create_empty_database()usingtemplate0. configure_database()builds the base registry, setsbase.user_adminand optional company/country/currency values, and commits those settings.- Install requested modules through
button_immediate_install(). Dependencies and auto-install behavior remain Odoo's responsibility. - Reopen the potentially replaced registry. Require every selected module to be installed, reapply login/password with the now-installed model extensions, ensure the account is active with Settings access, and commit.
The private creation primitive is an intentional Odoo 18 coupling. The higher-level service creation path catches initialization failures internally; using the primitive allows this command to handle and verify later steps itself. When supporting another major Odoo version, review this call and registry behavior against that version's implementation before widening the version guard.
Creation is not atomic: PostgreSQL database creation, registry/module work, filestore writes, and credential publication have separate persistence boundaries. The initial catalog query is not a lock; PostgreSQL's creation operation remains the authoritative protection against a concurrent database-name collision. Keep the target unavailable to Odoo services until all phases succeed.
Recovery lifecycle
- Parse the target and options; preview stops before runtime loading.
- Load Odoo with
initialize=False, clearing initialization, upgrade, and language-loading requests inherited from the runtime configuration. - Require an existing database and initialized Odoo schema, then build a registry
with
update_module=False. - Create an ORM environment as Odoo superuser ID 1, but browse only user ID 2 as the account being changed. Include archived accounts for explicit validation.
- Require the account to exist and have
base.group_system. Refuse an archived account unless--activatewas supplied. - Read or persist the password, write it and optional activation in the same ORM transaction, and commit. Report success only after commit returns.
Creation identifies the administrator through base.user_admin; recovery follows
the explicit user-ID-2 contract. Neither command uses the database-manager master
password. Recovery does not repair groups, replace login names, bypass MFA/SSO,
install modules, or create missing users. Registry loading still imports installed
addons, so a broken checkout can prevent recovery before the account is changed.
Credential publication and errors
read_password() opens with O_NOFOLLOW | O_NONBLOCK, then checks the actual
descriptor with fstat: a file must be regular, owned by the executing account,
and have no group/other access bits. Nonblocking open prevents a FIFO from hanging
before its type can be rejected. The bounded read includes room beyond 4,096
characters plus CRLF, preventing newline trimming from accepting truncated input.
generate_password() uses 32 random bytes from secrets.token_urlsafe. It opens
the private directory without following a symlink, verifies its ownership/mode,
and uses that directory descriptor for exclusive file creation. File content and
the directory entry are synced before the password is returned to the caller.
The caller can then change the database knowing password persistence succeeded.
Creation uses <database>_init_pass. Recovery uses
<database>_reset_pass_<UTC timestamp> with microseconds. O_EXCL still refuses
collisions: timestamps do not authorize replacement. Generated files remain even
if later writes fail. Do not add automatic deletion or reuse without accounting
for uncertain commit outcomes and ownership of existing artifacts.
ProvisioningError carries only deliberately constructed, safe-to-display text.
Unexpected exception messages and tracebacks are withheld at the CLI boundary
because custom password policies may include submitted values. Keep secrets out
of logs, exception messages, examples, and filenames. Third-party addon logging
is outside this wrapper's complete control.
| Outcome | CLI result | Persistence implications |
|---|---|---|
| Help or preview | Exit 0 | No database/password operation; creation may read module defaults. |
| Argument/default-config error | Exit 2 | No Odoo load or operational writes. |
| Successful operation | Exit 0 | Creation verified its modules/account; recovery's user write committed. |
| Operational failure or caught keyboard interrupt | Exit 1 | No success claim; inspect retained database/filestore/credential state. |
Recovery's ordinary pre-commit ORM failure rolls back the account transaction. A lost connection during commit can make its outcome uncertain. Creation has earlier commits regardless of the last step's outcome. There is no automatic rollback across PostgreSQL and the filesystem in either command.
Development and validation
When changing options, update parser help, both relevant completion functions, the component README, and focused CLI expectations. Completion reads only option names and path candidates for these commands; keep it free of password reads, Odoo imports, and live database queries.
The tests replace Odoo with explicit doubles and redirect credentials and module
defaults to temporary paths. Never replace these fixtures with runtime paths.
The shared load_odoo() helper mutates process-global configuration and is meant
for one target per process; its own test uses a fake module and restores sys.path.
Mock transactions prove call order and refusal behavior, not actual PostgreSQL
rollback, ORM hashing, addon compatibility, session invalidation, or UI login.
From the repository root, using its configured interpreter:
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 /srv/pmiverse/_pmisuite18/venv/bin/python \
-m pytest tests/test_create_database.py tests/test_reset_admin_password.py \
tests/test_script_cli.py -k 'create_database or reset_admin_password' \
-q -p no:cacheprovider
Use repository Ruff settings for the Python files, ShellCheck for shared Bash completion, and the IDE's file inspections. Live Odoo/PostgreSQL acceptance is a separate, explicitly authorized operation against a disposable target.