Metadata-Version: 2.1
Name: odoo-instance-api
Version: 0.1.0
Summary: FastAPI service exposing Odoo instance status and module/package diagnostics
Author-email: lai <pelayo.garcia@coopdevs.org>
Maintainer-email: lai <pelayo.garcia@coopdevs.org>
License: GNU AFFERO GENERAL PUBLIC LICENSE
        Version 3, 19 November 2007
        
        Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
        Everyone is permitted to copy and distribute verbatim copies
        of this license document, but changing it is not allowed.
        
        This program is free software: you can redistribute it and/or modify
        it under the terms of the GNU Affero General Public License as
        published by the Free Software Foundation, either version 3 of the
        License, or (at your option) any later version.
        
        This program is distributed in the hope that it will be useful,
        but WITHOUT ANY WARRANTY; without even the implied warranty of
        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
        GNU Affero General Public License for more details.
        
        You should have received a copy of the GNU Affero General Public License
        along with this program. If not, see <https://www.gnu.org/licenses/>.
        
Project-URL: Homepage, https://git.coopdevs.org/coopdevs/sysadmin/monitoring/odoo-instance-api
Project-URL: Repository, https://git.coopdevs.org/coopdevs/sysadmin/monitoring/odoo-instance-api
Project-URL: Source, https://git.coopdevs.org/coopdevs/sysadmin/monitoring/odoo-instance-api
Keywords: odoo,fastapi,postgresql,diagnostics,monitoring
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: System Administrators
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: System :: Monitoring
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi
Requires-Dist: uvicorn
Requires-Dist: psycopg2-binary
Requires-Dist: requests
Requires-Dist: packaging
Requires-Dist: jinja2
Provides-Extra: release
Requires-Dist: build>=1.2.2; extra == "release"
Requires-Dist: twine>=5.1.0; extra == "release"

# Odoo Instance API

A **small FastAPI service** that exposes a single endpoint (`/status`) for retrieving diagnostic information about a local Odoo installation in a structured JSON format.

It is designed to run on the same host as Odoo, read the Odoo configuration (`/etc/odoo/odoo.conf` by default), and query Postgres for installed databases and non-core modules. It also checks the Odoo `pyenv` environment for installed packages and queries PyPI for available updates (including pre-releases).

Finally, the app maps installed Odoo packages to database modules using the manifest path heuristic and reports version drifts across DB module version, installed package version, and PyPI latest version.

The intended use case is machine consumption, but the endpoint also supports HTML rendering for easy human inspection when needed

## Rationale

Despite we can list modules with a Prometheus exporter, this is an antipattern for several reasons:

- **Performance**: Querying `ir_module_module` across multiple databases and making PyPI API calls on every scrape would be very expensive (around ~20 seconds per scrape) and could lead to timeouts or high latency in Prometheus, with no real benefit since this data is not needed in real-time for alerting or monitoring.
- **Cardinality**: The number of non-core modules can vary widely across databases and instances. Exposing this via a Prometheus exporter would require complex label management and could lead to high cardinality issues in Prometheus.
- **Execute on demand**: This service is intended to be hit on-demand (e.g., via a button in `Odoo Instances`) rather than being scraped frequently. It can provide a comprehensive snapshot of the Odoo instance state without impacting performance.

---

## 🚀 Getting started (dev)

### 1) Create an app `pyenv` virtualenv and install dependencies

```bash
pyenv virtualenv 3.11.8 odoo-instance-api
pyenv activate odoo-instance-api
pip install -r requirements.txt
export ODOO_VENV_PATH=/home/odoo/pyenv/versions/odoo-16/bin/python
```

### 2) Run the service

```bash
uvicorn odoo_instance_api.main:app --host 127.0.0.1 --port 8000
```

Or via the package CLI entrypoint:

```bash
odoo-instance-api --host 127.0.0.1 --port 8000
```

Run with debug logs:

```bash
odoo-instance-api --host 127.0.0.1 --port 8000 --log-level debug
```

### 3) Hit the endpoint

- JSON: `curl http://127.0.0.1:8000/status`
- HTML: `curl "http://127.0.0.1:8000/status?format=html"`

In HTML mode, these same filters are available as in-page controls (checkbox/select/input) and are synced to the URL query string for shareable links.

Filter examples (work for both JSON and HTML):

- Only DB↔Installed drifts: `curl "http://127.0.0.1:8000/status?only_drift=true"`
- One database only: `curl "http://127.0.0.1:8000/status?database=odoov16"`
- Limit relation rows: `curl "http://127.0.0.1:8000/status?limit=50"`
- Combined: `curl "http://127.0.0.1:8000/status?database=odoov16&only_drift=true&limit=20"`

---

## 🔧 How it works (core design)

- **Single endpoint**: `/status` returns a JSON payload (or HTML when requested).
- **Entrypoint implementation**: `odoo_instance_api/main.py`
- **Odoo inspection environment**: Uses `ODOO_VENV_PATH` (defaults to `/home/odoo/pyenv/versions/odoo-16/bin/python`) to run `pip list --format=json` and detect installed Odoo packages.
- **PyPI checks**: For any `odoo*` packages found, it queries `https://pypi.org/pypi/<package>/json` and compares versions (including pre-releases) using `packaging.version`.
- **Database discovery**: Reads Postgres credentials from `/etc/odoo/odoo.conf` (override via `ODOO_CONF_PATH`) and lists non-template databases, excluding `postgres`.
- **Non-core module listing**: For each database, it queries `ir_module_module` with an Odoo-core author filter and supports DB schemas using either `installed_version` or `latest_version`.
- **Package↔module mapping (Phase A)**: It inspects installed `odoo*` distributions in the Odoo `pyenv` env, derives addon names from `__manifest__.py`/`__openerp__.py` paths, and matches those names against DB module names.
- **Version drift diagnostics**: For matched pairs, it reports whether versions differ across DB module version, installed package version, and PyPI latest version.

---

## 🧩 Environment overrides

Runtime model:

- The API itself runs in an **app pyenv** (for FastAPI/Uvicorn/dependencies).
- Odoo package inspection runs in the **Odoo pyenv** pointed by `ODOO_VENV_PATH`.

| Env var | Purpose | Default |
| ------- | ------- | ------- |
| `ODOO_VENV_PATH` | Path to the Odoo Python executable (typically from `pyenv`) | `/home/odoo/pyenv/versions/odoo-16/bin/python` |
| `ODOO_CONF_PATH` | Path to the Odoo config file (Postgres credentials) | `/etc/odoo/odoo.conf` |
| `ODOO_SOURCE_CODE_PATH` | Odoo source root or addons dir; modules under addons are treated as core-distributed | auto-discovered from `ODOO_VENV_PATH` when unset |
| `OIA_LOG_LEVEL` | Application log level (`critical`, `error`, `warning`, `info`, `debug`) | `INFO` |

### Centralized assumptions

Schema/runtime assumptions are centralized in [`odoo_instance_api/assumptions.py`](./odoo_instance_api/assumptions.py).

When Odoo/Postgres conventions change (table names, preferred version columns, core author filters, default paths), update assumptions once instead of patching multiple modules.

You can also override assumptions at runtime via environment variables:

- `OIA_DEFAULT_ODOO_PYTHON_PATH`
- `OIA_DEFAULT_ODOO_CONF_PATH`
- `OIA_DEFAULT_ODOO_SOURCE_CODE_PATH`
- `OIA_IR_MODULE_TABLE`
- `OIA_IR_MODULE_VERSION_COLUMNS` (comma-separated)
- `OIA_ODOO_CORE_AUTHORS` (comma-separated)
- `OIA_ODOO_CORE_MODULES` (comma-separated)
- `OIA_ODOO_PACKAGE_NAME_PREFIXES` (comma-separated)
- `OIA_ADDON_MANIFEST_FILENAMES` (comma-separated)
- `OIA_PYPI_PACKAGE_JSON_URL_TEMPLATE`
- `OIA_PYPI_HTTP_TIMEOUT_SECONDS`

Compatibility shortcuts remain supported:

- `ODOO_VENV_PATH` (takes precedence over `OIA_DEFAULT_ODOO_PYTHON_PATH`)
- `ODOO_CONF_PATH` (takes precedence over `OIA_DEFAULT_ODOO_CONF_PATH`)

### Logging

- CLI flag: `--log-level {critical,error,warning,info,debug}`
- Environment override: `OIA_LOG_LEVEL` (used as default when `--log-level` is not provided)

Example:

```bash
export OIA_LOG_LEVEL=debug
odoo-instance-api --host 127.0.0.1 --port 8000
```

---

## 📦 Mapping output in `/status`

### Optional query params

- `only_drift=true`: keeps only rows where DB module and installed package versions are mismatched.
- `database=<name>`: filters output to a single database name.
- `limit=<n>`: limits `package_module_relation_table.rows` to first `n` rows.

Defaults remain unchanged when params are omitted.

The JSON response includes two mapping-oriented blocks:

- `package_module_mapping`: detailed diagnostics grouped by database
- `package_module_relation_table`: flattened table with one row per (`database`, `pypi_package`, `db_module`) relation
- `uninstalled_module_matches`: packages whose candidate modules exist in DB but are currently uninstalled
- `core_module_exclusions`: configured module names treated as core and excluded from unmatched-module accounting

`package_module_mapping` includes:

- `method`: currently `manifest_path_match`
- `by_database[]`: mapping diagnostics per database, including `package_to_modules[]`, `matched_modules[]`, `unmatched_packages[]`, and `unmatched_modules[]`
- `stats`: global counters (`matched_pairs`, `unmatched_packages`, `unmatched_modules`)

`mapping_summary.by_database[]` also includes explicit unmatched details (not only counts):

- `unmatched_package_names[]`
- `unmatched_module_names[]`

`mapping_summary.totals` includes deduplicated global lists:

- `unmatched_package_names[]`
- `unmatched_module_names[]`

Each matched module includes `version_drift` flags:

- `db_vs_package_installed`
- `db_vs_pypi_latest`
- `package_installed_vs_pypi_latest`

`package_module_relation_table` includes:

- `count`
- `rows[]` with: `database`, `pypi_package`, `db_module`, `db_module_version`, `package_installed_version`, `package_pypi_latest_version`, drift flags, and mapping confidence/reason.

Quick inspect command:

```bash
curl -s http://127.0.0.1:8000/status | jq '.package_module_relation_table.rows[] | {database, pypi_package, db_module, db_module_version, package_installed_version, package_pypi_latest_version, drift_db_vs_package_installed, drift_db_vs_pypi_latest, drift_package_installed_vs_pypi_latest}'
```

---

## 🔒 Recommended proxy configuration (NGINX)

This service is intended to run behind a proxy (e.g., NGINX) that can handle authentication, TLS, and routing.

Example NGINX snippet:

```nginx
location /status {
    proxy_pass http://127.0.0.1:8000/status;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 60;
    proxy_connect_timeout 10;
}
```

---

## 🪲 Troubleshooting

- **No databases returned?** Verify the service user can read `/etc/odoo/odoo.conf` and that the Postgres user/credentials can connect.
- **No Odoo packages listed?** Verify `ODOO_VENV_PATH` points at the Odoo runtime `pyenv` environment and that `pip list` works there.
- **The service crashes on startup?** It should not crash; errors are printed and the service continues with partial results.
- **Need more diagnostics?** Start with `--log-level debug` (or set `OIA_LOG_LEVEL=debug`) and inspect logs for package counts, interpreter path selection, candidate counts, database discovery, and mapping stats.

---

## 🗺 Roadmap (next improvements)

1. **Add optional fuzzy fallback mapping**
    - Add a secondary heuristic layer for packages that do not expose manifest paths in wheel metadata.
    - Keep confidence tiering explicit (`high`/`medium`/`low`) and avoid auto-accepting ambiguous matches.

## ✅ Running as a systemd service

This repo includes an example systemd unit file: [`odoo-instance-api.service`](./odoo-instance-api.service).

### Install / enable

1. Copy the unit file to systemd's directory:

```bash
sudo cp odoo-instance-api.service /etc/systemd/system/
```

1. Reload systemd and enable the service:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now odoo-instance-api.service
```

1. Check the service status:

```bash
sudo systemctl status odoo-instance-api.service
sudo journalctl -u odoo-instance-api.service -f
```

### Notes

- The unit file assumes the code lives at `/opt/odoo_instance_api`, runs Uvicorn from the app pyenv (`/home/odoo/pyenv/versions/odoo-instance-api/bin/uvicorn`) with module `odoo_instance_api.main:app`, and sets `ODOO_VENV_PATH` to the Odoo pyenv Python (`/home/odoo/pyenv/versions/odoo-16/bin/python`).
- Update `User=` and the paths in `ExecStart=` as needed for your environment.
- Authentication and TLS should be handled by a reverse proxy (e.g., NGINX) in front of this service.
