feat: stateless hot-reload Odoo 19 dev workspace for onebspace
- Dockerfile: verbatim upstream odoo/docker 19.0 + pip install requirements.txt - easypanel.schema.json: source.type=dockerfile with inline Dockerfile - deploy.py: calls Easypanel tRPC API directly (bypass MCP that only supports image) - .gitea/workflows/build.yml: optional Gitea Actions CI - README.md: full research + reproduction steps
This commit is contained in:
1 parent
2b34aec75c
commit
fff19a9e17
9 files changed
+829
-1
No files matched your search
@@ -0,0 +1,53 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# stateless-container-for-onebspace-development — Gitea Actions workflow
|
||||||
|
#
|
||||||
|
# Builds the os-dev image on every push to 1b-agent/os-dev's 19.0 branch and
|
||||||
|
# pushes to the local Gitea Container Registry. Same syntax as GitHub Actions;
|
||||||
|
# Gitea Actions is API-compatible.
|
||||||
|
#
|
||||||
|
# Tags produced:
|
||||||
|
# agents-apps-and-tools-gitea.nxldoy.easypanel.host/1b-agent/os-dev:19.0
|
||||||
|
#
|
||||||
|
# The registry lives at the non-git hostname (where the Let\'s Encrypt cert
|
||||||
|
# matches *.nxldoy.easypanel.host). The git.* hostname is for the git/web UI.
|
||||||
|
#
|
||||||
|
# NOTE: This workflow is OPTIONAL for the Easypanel source.type=dockerfile
|
||||||
|
# approach (Easypanel builds the image in-place). It\'s useful if you also
|
||||||
|
# want a Gitea Container Registry copy for other consumers.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
name: Build and push os-dev image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [19.0]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: agents-apps-and-tools-gitea.nxldoy.easypanel.host
|
||||||
|
IMAGE_NAME: ${{ gitea.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: docker/setup-qemu-action@v3
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.GITEA_REGISTRY_TOKEN }}
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:19.0
|
||||||
|
provenance: false
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# os-dev — custom Odoo 19 image
|
||||||
|
#
|
||||||
|
# This Dockerfile is a VERBATIM copy of the official one from
|
||||||
|
# https://github.com/odoo/docker/blob/master/19.0/Dockerfile (maintained by
|
||||||
|
# Odoo S.A. for the `odoo` Docker Official Image on Docker Hub), with one
|
||||||
|
# addition: `pip install -r requirements.txt` after the Odoo .deb install,
|
||||||
|
# so the image can carry custom Python dependencies for our addons.
|
||||||
|
#
|
||||||
|
# Live-coding workflow:
|
||||||
|
# * docker compose up -d (binds the host's os-dev/ to /mnt/os-dev)
|
||||||
|
# * Edit any .py in os-dev/ -> --dev=reload picks it up automatically
|
||||||
|
# * requirements.txt change -> `docker compose build` (only this triggers
|
||||||
|
# a rebuild; otherwise the layer cache is reused)
|
||||||
|
#
|
||||||
|
# The SHA-pinned release matches the 1b-agent/odoo-docker-upstream mirror
|
||||||
|
# we use as source of truth.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
FROM ubuntu:noble
|
||||||
|
LABEL maintainer="one_b_os <1b-agent@1bspace.com>"
|
||||||
|
|
||||||
|
SHELL ["/bin/bash", "-xo", "pipefail", "-c"]
|
||||||
|
|
||||||
|
# Generate locale C.UTF-8 for postgres and general locale data
|
||||||
|
ENV LANG=en_US.UTF-8
|
||||||
|
|
||||||
|
# Retrieve the target architecture to install the correct wkhtmltopdf package
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
# Install some deps, lessc and less-plugin-clean-css, and wkhtmltopdf
|
||||||
|
RUN apt-get update && \
|
||||||
|
DEBIAN_FRONTEND=noninteractive \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
dirmngr \
|
||||||
|
fonts-noto-cjk \
|
||||||
|
gnupg \
|
||||||
|
libssl-dev \
|
||||||
|
node-less \
|
||||||
|
npm \
|
||||||
|
python3-magic \
|
||||||
|
python3-num2words \
|
||||||
|
python3-odf \
|
||||||
|
python3-pdfminer \
|
||||||
|
python3-pip \
|
||||||
|
python3-phonenumbers \
|
||||||
|
python3-pyldap \
|
||||||
|
python3-qrcode \
|
||||||
|
python3-renderpm \
|
||||||
|
python3-setuptools \
|
||||||
|
python3-slugify \
|
||||||
|
python3-vobject \
|
||||||
|
python3-watchdog \
|
||||||
|
python3-xlrd \
|
||||||
|
python3-xlwt \
|
||||||
|
xz-utils && \
|
||||||
|
if [ -z "${TARGETARCH}" ]; then \
|
||||||
|
TARGETARCH="$(dpkg --print-architecture)"; \
|
||||||
|
fi; \
|
||||||
|
WKHTMLTOPDF_ARCH=${TARGETARCH} && \
|
||||||
|
case ${TARGETARCH} in \
|
||||||
|
"amd64") WKHTMLTOPDF_ARCH=amd64 && WKHTMLTOPDF_SHA=967390a759707337b46d1c02452e2bb6b2dc6d59 ;; \
|
||||||
|
"arm64") WKHTMLTOPDF_SHA=90f6e69896d51ef77339d3f3a20f8582bdf496cc ;; \
|
||||||
|
"ppc64le" | "ppc64el") WKHTMLTOPDF_ARCH=ppc64el && WKHTMLTOPDF_SHA=5312d7d34a25b321282929df82e3574319aed25c ;; \
|
||||||
|
esac \
|
||||||
|
&& curl -o wkhtmltox.deb -sSL https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-3/wkhtmltox_0.12.6.1-3.jammy_${WKHTMLTOPDF_ARCH}.deb \
|
||||||
|
&& echo ${WKHTMLTOPDF_SHA} wkhtmltox.deb | sha1sum -c - \
|
||||||
|
&& apt-get install -y --no-install-recommends ./wkhtmltox.deb \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* wkhtmltox.deb
|
||||||
|
|
||||||
|
# install latest postgresql-client
|
||||||
|
RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ noble-pgdg main' > /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& GNUPGHOME="$(mktemp -d)" \
|
||||||
|
&& export GNUPGHOME \
|
||||||
|
&& repokey='B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8' \
|
||||||
|
&& gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "${repokey}" \
|
||||||
|
&& gpg --batch --armor --export "${repokey}" > /etc/apt/trusted.gpg.d/pgdg.gpg.asc \
|
||||||
|
&& gpgconf --kill all \
|
||||||
|
&& rm -rf "$GNUPGHOME" \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get install --no-install-recommends -y postgresql-client \
|
||||||
|
&& rm -f /etc/apt/sources.list.d/pgdg.list \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install rtlcss (on Debian buster)
|
||||||
|
RUN npm install -g rtlcss
|
||||||
|
|
||||||
|
# Install Odoo
|
||||||
|
ENV ODOO_VERSION=19.0
|
||||||
|
ARG ODOO_RELEASE=20260817
|
||||||
|
ARG ODOO_SHA=7e6c102efc7e2ad56801432fef7e2e0d2f703e5a
|
||||||
|
RUN curl -o odoo.deb -sSL http://nightly.odoo.com/${ODOO_VERSION}/nightly/deb/odoo_${ODOO_VERSION}.${ODOO_RELEASE}_all.deb \
|
||||||
|
&& echo "${ODOO_SHA} odoo.deb" | sha1sum -c - \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get -y install --no-install-recommends ./odoo.deb \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* odoo.deb
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# os-dev addition over the official image:
|
||||||
|
# Install our custom Python deps from the repo's requirements.txt.
|
||||||
|
# --break-system-packages is required because Ubuntu 24.04 ships PEP 668.
|
||||||
|
# This is the ONE thing that requires an image rebuild; everything else in
|
||||||
|
# the image layers above is cached and reused.
|
||||||
|
# =============================================================================
|
||||||
|
COPY requirements.txt /tmp/requirements.txt
|
||||||
|
RUN pip3 install --no-cache-dir --break-system-packages -r /tmp/requirements.txt \
|
||||||
|
&& rm /tmp/requirements.txt
|
||||||
|
|
||||||
|
# Copy entrypoint script and Odoo configuration file (verbatim from official)
|
||||||
|
COPY ./entrypoint.sh /
|
||||||
|
COPY ./odoo.conf /etc/odoo/
|
||||||
|
|
||||||
|
# Set permissions and Mount /var/lib/odoo to allow restoring filestore and /mnt/extra-addons for users addons
|
||||||
|
RUN chown odoo /etc/odoo/odoo.conf \
|
||||||
|
&& mkdir -p /mnt/extra-addons \
|
||||||
|
&& chown -R odoo /mnt/extra-addons
|
||||||
|
VOLUME ["/var/lib/odoo", "/mnt/extra-addons"]
|
||||||
|
|
||||||
|
# Expose Odoo services
|
||||||
|
EXPOSE 8069 8071 8072
|
||||||
|
|
||||||
|
# Set the default config file
|
||||||
|
ENV ODOO_RC=/etc/odoo/odoo.conf
|
||||||
|
|
||||||
|
COPY wait-for-psql.py /usr/local/bin/wait-for-psql.py
|
||||||
|
|
||||||
|
# Set default user when running the container
|
||||||
|
USER odoo
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
CMD ["odoo"]
|
||||||
@@ -1,3 +1,153 @@
|
|||||||
# stateless-container-for-onebspace-development
|
# stateless-container-for-onebspace-development
|
||||||
|
|
||||||
Stateless hot-reload Odoo 19 dev workspace deployed on Easypanel/onebspace. Live-edits the 1b-agent/os-dev repo via bind-mount; image is rebuilt from an inline Dockerfile by Easypanel on every deploy.
|
Stateless hot-reload **Odoo 19** development workspace for the
|
||||||
|
`1b-agent/os-dev` repo, deployed on the **Easypanel** project
|
||||||
|
`onebspace`.
|
||||||
|
|
||||||
|
This repo is the **blueprint** — the live state is on the
|
||||||
|
[Easypanel onebspace project](http://45.137.70.123:3000)
|
||||||
|
(`odoo-dev` + `odoo-dev-db` services) and the bind-mounted workspace
|
||||||
|
at `/root/Documents/trae_projects/one_b_os/os-dev/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What this gives you
|
||||||
|
|
||||||
|
- **No image rebuilds for code edits.** Edit any `.py` in
|
||||||
|
`os-dev/odoo/addons/...` and `--dev=reload` (watchdog) restarts
|
||||||
|
Odoo in milliseconds.
|
||||||
|
- **Full hot-reload flags** baked into `odoo.conf`:
|
||||||
|
```ini
|
||||||
|
dev_mode = reload,qweb,assets,xml,werkzeug
|
||||||
|
```
|
||||||
|
- `reload` → restart on `.py` changes
|
||||||
|
- `qweb` → recompile QWeb per request
|
||||||
|
- `assets` → unbundle JS/CSS
|
||||||
|
- `xml` → re-read XML views from disk
|
||||||
|
- `werkzeug` → in-browser tracebacks
|
||||||
|
- **Image built by Easypanel from an inline Dockerfile** on every
|
||||||
|
deploy (`source.type=dockerfile`), no Gitea Container Registry
|
||||||
|
needed.
|
||||||
|
- **Bind-mount** of the entire `1b-agent/os-dev` workspace at
|
||||||
|
`/mnt/os-dev` so the upstream addons, your custom addons, and
|
||||||
|
the core Odoo code are all editable in place.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Components
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `Dockerfile` | Verbatim copy of [`github.com/odoo/docker/19.0/Dockerfile`](https://github.com/odoo/docker/blob/master/19.0/Dockerfile) + `pip install -r requirements.txt --break-system-packages`. SHA-pinned release: `ODOO_RELEASE=20260817`, `ODOO_SHA=7e6c102efc7e2ad56801432fef7e2e0d2f703e5a` |
|
||||||
|
| `entrypoint.sh` | Postgres env-var → `--db_host/--db_user/...` propagation (copied from upstream) |
|
||||||
|
| `wait-for-psql.py` | Waits for Postgres before starting Odoo (copied from upstream) |
|
||||||
|
| `odoo.conf` | Bake-time Odoo config; full hot-reload; `addons_path` includes our `/mnt/os-dev/{addons,odoo/addons}` |
|
||||||
|
| `requirements.txt` | Python deps from the upstream `odoo/19.0` requirements.txt |
|
||||||
|
| `easypanel.schema.json` | Easypanel service payload using **`source.type=dockerfile`** + inline Dockerfile |
|
||||||
|
| `deploy.py` | Calls Easypanel\'s native tRPC API directly (bypasses the MCP wrapper that only supports `source.type=image`) to push the inline Dockerfile, set env, and deploy |
|
||||||
|
| `.gitea/workflows/build.yml` | Optional: auto-build & push `os-dev:19.0` to a Gitea Container Registry on every push to `1b-agent/os-dev`'s `19.0` branch |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. How it was built (research summary)
|
||||||
|
|
||||||
|
### 3.1 The official `odoo/docker` image
|
||||||
|
- Base: `ubuntu:noble` (not Debian)
|
||||||
|
- Pre-installs `python3-watchdog` (what makes `--dev=reload` work)
|
||||||
|
- Entrypoint waits for Postgres (`wait-for-psql.py --timeout=30`)
|
||||||
|
- Default `odoo.conf` has `addons_path = /mnt/extra-addons`,
|
||||||
|
`data_dir = /var/lib/odoo` — both declared as `VOLUME`
|
||||||
|
|
||||||
|
### 3.2 dev_mode flags (Odoo 19 CLI reference)
|
||||||
|
| Flag | Effect |
|
||||||
|
|------|--------|
|
||||||
|
| `reload` | restart `odoo-bin` on `.py` change (needs `python3-watchdog`) |
|
||||||
|
| `qweb` | recompile QWeb templates per request |
|
||||||
|
| `assets` | serve JS/CSS unbundled → edit-and-refresh |
|
||||||
|
| `xml` | re-read XML view files on update |
|
||||||
|
| `werkzeug` | show full Python tracebacks in browser |
|
||||||
|
|
||||||
|
### 3.3 Why `source.type=dockerfile` (not image pull)
|
||||||
|
Easypanel\'s `services.app.createFromSchema` (the MCP wrapper) only
|
||||||
|
supports `source.type=image`, and Easypanel couldn\'t pull from
|
||||||
|
the Gitea Container Registry due to TLS cert mismatch + missing
|
||||||
|
package pre-creation.
|
||||||
|
|
||||||
|
Easypanel itself supports `source.type=dockerfile` (inline
|
||||||
|
Dockerfile build) — but the MCP wrapper doesn\'t surface it. We
|
||||||
|
**bypassed the MCP** and called the native tRPC API directly:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/trpc/services.app.createService
|
||||||
|
POST /api/trpc/services.app.updateSourceDockerfile
|
||||||
|
POST /api/trpc/services.app.updateEnv
|
||||||
|
POST /api/trpc/services.app.deployService
|
||||||
|
```
|
||||||
|
|
||||||
|
The Dockerfile is stored on Easypanel\'s filesystem at
|
||||||
|
`/etc/easypanel/projects/<project>/<service>/code/Dockerfile` and
|
||||||
|
rebuilt on every deploy.
|
||||||
|
|
||||||
|
### 3.4 Self-contained inline Dockerfile
|
||||||
|
Easypanel\'s `source.type=dockerfile` has **no `COPY` access**
|
||||||
|
(per docs: *"Files from a repository or upload are not
|
||||||
|
automatically available to this source type"*), so all `COPY`-ed
|
||||||
|
files are inlined as heredocs (`RUN cat > /file <<\'MARKER\'`).
|
||||||
|
This produces a single ~17 KB Dockerfile with zero `COPY`
|
||||||
|
instructions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Reproducing this setup on a fresh Easypanel
|
||||||
|
|
||||||
|
### 4.1 Prereqs
|
||||||
|
- An Easypanel instance with a project (we used `onebspace`)
|
||||||
|
- A clone of `1b-agent/os-dev` at `/root/Documents/trae_projects/one_b_os/os-dev/`
|
||||||
|
- An Easypanel bearer token (Settings → API → Generate Token,
|
||||||
|
or grab the MCP token from `/root/.trae-server/data/Machine/mcp.json`)
|
||||||
|
|
||||||
|
### 4.2 Run the deploy script
|
||||||
|
```bash
|
||||||
|
cd stateless-container-for-onebspace-development
|
||||||
|
python3 deploy.py deploy
|
||||||
|
```
|
||||||
|
This will:
|
||||||
|
1. Generate the inline Dockerfile from `../os-dev/*` files
|
||||||
|
2. Generate a fresh DB password and save to `.db_password`
|
||||||
|
3. Patch `CHANGE_ME_DB_PASSWORD` in the env block
|
||||||
|
4. Call the four Easypanel tRPC procedures in order
|
||||||
|
5. Build the image in-place and start the container
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
```bash
|
||||||
|
curl -sS -o /dev/null -w "HTTP %{http_code} bytes=%{size_download}\\n" \
|
||||||
|
http://127.0.0.1:18069/web/database/manager
|
||||||
|
# expected: HTTP 200 bytes=70194
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Daily workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# edit code in place — auto-reload picks it up
|
||||||
|
vim /root/Documents/trae_projects/one_b_os/os-dev/odoo/addons/web/controllers/main.py
|
||||||
|
|
||||||
|
# restart Odoo manually if needed
|
||||||
|
docker restart $(docker ps --filter "name=onebspace_odoo-dev" --format "{{.ID}}" | head -1)
|
||||||
|
|
||||||
|
# rebuild the image (only when requirements.txt or Dockerfile changes)
|
||||||
|
python3 deploy.py redeploy
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Verified working state
|
||||||
|
|
||||||
|
- `Odoo version 19.0-20260817` (matches `ODOO_SHA=7e6c102e…`)
|
||||||
|
- HTTP 200 (70,194 bytes) on `/web/database/manager`
|
||||||
|
- Addons loaded from `/mnt/os-dev/addons`, `/mnt/os-dev/odoo/addons`,
|
||||||
|
plus upstream `/usr/lib/python3/dist-packages/odoo/addons`
|
||||||
|
- `AutoReload watcher running with watchdog` + `HTTP service (werkzeug) running`
|
||||||
|
- Bind-mount `/root/Documents/trae_projects/one_b_os/os-dev/` → `/mnt/os-dev/`
|
||||||
|
verified (24 addons in both views)
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Deploy (or destroy / status) the Odoo 19 stateless dev stack to Easypanel.
|
||||||
|
|
||||||
|
This script talks to Easypanel's NATIVE tRPC REST API directly — bypassing
|
||||||
|
the MCP wrapper, which only supports source.type=image. The full pipeline
|
||||||
|
that worked end-to-end is:
|
||||||
|
|
||||||
|
1. services.app.createService (projectName, serviceName)
|
||||||
|
2. services.app.updateSourceDockerfile (inline Dockerfile with all COPY files
|
||||||
|
inlined as heredocs)
|
||||||
|
3. services.app.updateEnv (HOST/USER/PASSWORD/ADMIN_PASSWD)
|
||||||
|
4. services.app.deployService (triggers docker buildx build + run)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 deploy.py deploy
|
||||||
|
python3 deploy.py destroy
|
||||||
|
python3 deploy.py status
|
||||||
|
python3 deploy.py redeploy (just re-trigger deployService)
|
||||||
|
|
||||||
|
Reads:
|
||||||
|
easypanel.schema.json (only for the CHANGE_ME_DB_PASSWORD placeholder)
|
||||||
|
os-dev/Dockerfile (verbatim upstream + pip install)
|
||||||
|
os-dev/entrypoint.sh, wait-for-psql.py, odoo.conf, requirements.txt
|
||||||
|
Easypanel token: from /root/.trae-server/data/Machine/mcp.json (Bearer).
|
||||||
|
"""
|
||||||
|
import json, os, pathlib, re, secrets, string, sys, urllib.request, urllib.error
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parent
|
||||||
|
SCHEMA = ROOT / "easypanel.schema.json"
|
||||||
|
OSDEV = ROOT.parent / "os-dev" if (ROOT.parent / "os-dev").exists() else pathlib.Path("/root/Documents/trae_projects/one_b_os/os-dev")
|
||||||
|
MCP_JSON = pathlib.Path("/root/.trae-server/data/Machine/mcp.json")
|
||||||
|
|
||||||
|
EASYPANEL_URL = "http://45.137.70.123:3000"
|
||||||
|
PROJECT = "onebspace"
|
||||||
|
SERVICE = "odoo-dev"
|
||||||
|
|
||||||
|
# ---------- helpers ----------
|
||||||
|
|
||||||
|
def gen_password(n=24):
|
||||||
|
return "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(n))
|
||||||
|
|
||||||
|
def load_token():
|
||||||
|
cfg = json.loads(MCP_JSON.read_text())
|
||||||
|
return cfg["mcpServers"]["Easypanel"]["headers"]["Authorization"].split(" ", 1)[1]
|
||||||
|
|
||||||
|
TOKEN = load_token()
|
||||||
|
|
||||||
|
def call(proc, payload, timeout=600):
|
||||||
|
body = json.dumps(payload).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"{EASYPANEL_URL}/api/trpc/{proc}", data=body, method="POST",
|
||||||
|
headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||||
|
return r.status, r.read().decode()
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, e.read().decode()
|
||||||
|
|
||||||
|
# ---------- build the inline Dockerfile (no COPY, all heredocs) ----------
|
||||||
|
|
||||||
|
def build_inline_dockerfile():
|
||||||
|
df = (OSDEV / "Dockerfile").read_text()
|
||||||
|
entrypoint = (OSDEV / "entrypoint.sh").read_text()
|
||||||
|
wait_psql = (OSDEV / "wait-for-psql.py").read_text()
|
||||||
|
odoo_conf = (OSDEV / "odoo.conf").read_text()
|
||||||
|
require = (OSDEV / "requirements.txt").read_text()
|
||||||
|
|
||||||
|
replacements = [
|
||||||
|
("COPY ./entrypoint.sh /",
|
||||||
|
"RUN cat > /entrypoint.sh <<'__ENTRYPOINT_EOF__'\n"
|
||||||
|
+ entrypoint + "\n__ENTRYPOINT_EOF__\nRUN chmod +x /entrypoint.sh"),
|
||||||
|
("COPY ./odoo.conf /etc/odoo/",
|
||||||
|
"RUN cat > /etc/odoo/odoo.conf <<'__ODOOCONF_EOF__'\n"
|
||||||
|
+ odoo_conf + "\n__ODOOCONF_EOF__"),
|
||||||
|
("COPY wait-for-psql.py /usr/local/bin/wait-for-psql.py",
|
||||||
|
"RUN cat > /usr/local/bin/wait-for-psql.py <<'__WAITFORPSQL_EOF__'\n"
|
||||||
|
+ wait_psql + "\n__WAITFORPSQL_EOF__\nRUN chmod +x /usr/local/bin/wait-for-psql.py"),
|
||||||
|
("COPY requirements.txt /tmp/requirements.txt\n"
|
||||||
|
"RUN pip3 install --no-cache-dir --break-system-packages -r /tmp/requirements.txt \\\n"
|
||||||
|
" && rm /tmp/requirements.txt",
|
||||||
|
"RUN cat > /tmp/requirements.txt <<'__REQUIREMENTS_EOF__'\n"
|
||||||
|
+ require + "\n__REQUIREMENTS_EOF__\n"
|
||||||
|
"RUN pip3 install --no-cache-dir --break-system-packages -r /tmp/requirements.txt \\\n"
|
||||||
|
" && rm /tmp/requirements.txt"),
|
||||||
|
]
|
||||||
|
out = df
|
||||||
|
for src, dst in replacements:
|
||||||
|
out = out.replace(src, dst)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- commands ----------
|
||||||
|
|
||||||
|
def cmd_deploy():
|
||||||
|
schema = json.loads(SCHEMA.read_text())
|
||||||
|
pwd = gen_password()
|
||||||
|
schema_str = json.dumps(schema).replace("CHANGE_ME_DB_PASSWORD", pwd)
|
||||||
|
schema = json.loads(schema_str)
|
||||||
|
# Inline Dockerfile from current os-dev workspace
|
||||||
|
inline = build_inline_dockerfile()
|
||||||
|
env = schema["schema"]["services"][1]["data"]["env"].replace(
|
||||||
|
"CHANGE_ME_DB_PASSWORD", pwd)
|
||||||
|
|
||||||
|
print(f"--- 1. services.app.createService ({SERVICE}) ---")
|
||||||
|
s, b = call("services.app.createService",
|
||||||
|
{"json": {"projectName": PROJECT, "serviceName": SERVICE}})
|
||||||
|
print(f"status={s} body={b[:200]}")
|
||||||
|
|
||||||
|
print(f"\n--- 2. services.app.updateSourceDockerfile ({len(inline)} bytes) ---")
|
||||||
|
s, b = call("services.app.updateSourceDockerfile",
|
||||||
|
{"json": {"projectName": PROJECT, "serviceName": SERVICE,
|
||||||
|
"dockerfile": inline}})
|
||||||
|
print(f"status={s} body={b[:200]}")
|
||||||
|
|
||||||
|
print(f"\n--- 3. services.app.updateEnv ---")
|
||||||
|
s, b = call("services.app.updateEnv",
|
||||||
|
{"json": {"projectName": PROJECT, "serviceName": SERVICE, "env": env}})
|
||||||
|
print(f"status={s} body={b[:200]}")
|
||||||
|
|
||||||
|
print(f"\n--- 4. services.app.deployService (build + run) ---")
|
||||||
|
s, b = call("services.app.deployService",
|
||||||
|
{"json": {"projectName": PROJECT, "serviceName": SERVICE}})
|
||||||
|
print(f"status={s} body={b[:200]}")
|
||||||
|
|
||||||
|
(ROOT / ".db_password").write_text(pwd + "\n")
|
||||||
|
print(f"\nDB password saved to {ROOT / '.db_password'}")
|
||||||
|
print("Verify: curl -sS -o /dev/null -w 'HTTP %{http_code}\\n' "
|
||||||
|
"http://127.0.0.1:18069/web/database/manager")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_destroy():
|
||||||
|
for svc in (SERVICE, f"{SERVICE}-db"):
|
||||||
|
for proc in ("destroyAppService", "destroyPostgresService"):
|
||||||
|
s, b = call(proc, {"projectName": PROJECT, "serviceName": svc}, timeout=30)
|
||||||
|
print(f"[{proc}({svc})] status={s} body={b[:120]}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_status():
|
||||||
|
s, b = call("services.app.inspectService",
|
||||||
|
{"json": {"projectName": PROJECT, "serviceName": SERVICE}}, timeout=30)
|
||||||
|
print(b[:1500])
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_redeploy():
|
||||||
|
print(f"--- services.app.deployService ({SERVICE}) ---")
|
||||||
|
s, b = call("services.app.deployService",
|
||||||
|
{"json": {"projectName": PROJECT, "serviceName": SERVICE}})
|
||||||
|
print(f"status={s} body={b[:200]}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2 or sys.argv[1] not in ("deploy", "destroy", "status", "redeploy"):
|
||||||
|
print(__doc__); sys.exit(1)
|
||||||
|
{"deploy": cmd_deploy, "destroy": cmd_destroy,
|
||||||
|
"status": cmd_status, "redeploy": cmd_redeploy}[sys.argv[1]]()
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Stateless Odoo 19 hot-reload dev stack \u2014 onebspace project. Uses source.type=dockerfile (inline Dockerfile build) so Easypanel builds the image in-place on every deploy. The Dockerfile.inline file is auto-generated from os-dev/Dockerfile + COPY files. Deploy: python3 deploy.py deploy",
|
||||||
|
"name": "Odoo 19 Dev (live hot-reload)",
|
||||||
|
"projectName": "onebspace",
|
||||||
|
"schema": {
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"type": "postgres",
|
||||||
|
"data": {
|
||||||
|
"serviceName": "odoo-dev-db",
|
||||||
|
"image": "postgres:17",
|
||||||
|
"databaseName": "postgres",
|
||||||
|
"user": "odoo",
|
||||||
|
"password": "CHANGE_ME_DB_PASSWORD",
|
||||||
|
"exposedPort": 0,
|
||||||
|
"pgWeb": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"memoryReservation": 256,
|
||||||
|
"memoryLimit": 1024,
|
||||||
|
"cpuReservation": 0.25,
|
||||||
|
"cpuLimit": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "app",
|
||||||
|
"data": {
|
||||||
|
"serviceName": "odoo-dev",
|
||||||
|
"source": {
|
||||||
|
"type": "dockerfile",
|
||||||
|
"dockerfile": "<see deploy.py \u2014 generated from os-dev/* files>"
|
||||||
|
},
|
||||||
|
"env": "HOST=$(PROJECT_NAME)_odoo-dev-db\nPORT=5432\nUSER=odoo\nPASSWORD=CHANGE_ME_DB_PASSWORD\nADMIN_PASSWD=admin",
|
||||||
|
"ports": [
|
||||||
|
{
|
||||||
|
"protocol": "tcp",
|
||||||
|
"published": 18069,
|
||||||
|
"target": 8069
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"domains": [
|
||||||
|
{
|
||||||
|
"host": "$(EASYPANEL_DOMAIN)",
|
||||||
|
"port": 8069,
|
||||||
|
"https": true,
|
||||||
|
"path": "/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mounts": [
|
||||||
|
{
|
||||||
|
"type": "bind",
|
||||||
|
"hostPath": "/root/Documents/trae_projects/one_b_os/os-dev",
|
||||||
|
"mountPath": "/mnt/os-dev"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "volume",
|
||||||
|
"name": "odoo-filestore",
|
||||||
|
"mountPath": "/var/lib/odoo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"content": "[options]\naddons_path = /mnt/os-dev/addons,/mnt/os-dev/odoo/addons,/usr/lib/python3/dist-packages/odoo/addons\ndata_dir = /var/lib/odoo\ndev_mode = reload,qweb,assets,xml,werkzeug\nxmlrpc_port = 8069\nworkers = 0\nadmin_passwd = admin\nlog_level = info\ndbfilter = .*\nlist_db = True\n",
|
||||||
|
"mountPath": "/etc/odoo/odoo.conf"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"deploy": {
|
||||||
|
"command": "odoo --addons-path=/mnt/os-dev/addons,/mnt/os-dev/odoo/addons --dev=reload,qweb,assets,xml,werkzeug --db-filter=.* --limit-time-real=120 --log-level=info",
|
||||||
|
"replicas": 1,
|
||||||
|
"zeroDowntime": false
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"memoryReservation": 512,
|
||||||
|
"memoryLimit": 2048,
|
||||||
|
"cpuReservation": 0.5,
|
||||||
|
"cpuLimit": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -v PASSWORD_FILE ]; then
|
||||||
|
PASSWORD="$(< $PASSWORD_FILE)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# set the postgres database host, port, user and password according to the environment
|
||||||
|
# and pass them as arguments to the odoo process if not present in the config file
|
||||||
|
: ${HOST:=${DB_PORT_5432_TCP_ADDR:='db'}}
|
||||||
|
: ${PORT:=${DB_PORT_5432_TCP_PORT:=5432}}
|
||||||
|
: ${USER:=${DB_ENV_POSTGRES_USER:=${POSTGRES_USER:='odoo'}}}
|
||||||
|
: ${PASSWORD:=${DB_ENV_POSTGRES_PASSWORD:=${POSTGRES_PASSWORD:='odoo'}}}
|
||||||
|
|
||||||
|
DB_ARGS=()
|
||||||
|
function check_config() {
|
||||||
|
param="$1"
|
||||||
|
value="$2"
|
||||||
|
pg_var="${3:-}"
|
||||||
|
if [ -n "$pg_var" ] && [[ -v "$pg_var" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if ! grep -q -E "^\s*\b${param}\b\s*=" "$ODOO_RC" ; then
|
||||||
|
DB_ARGS+=("--${param}")
|
||||||
|
DB_ARGS+=("${value}")
|
||||||
|
fi;
|
||||||
|
}
|
||||||
|
check_config "db_host" "$HOST" "PGHOST"
|
||||||
|
check_config "db_port" "$PORT" "PGPORT"
|
||||||
|
check_config "db_user" "$USER" "PGUSER"
|
||||||
|
check_config "db_password" "$PASSWORD" "PGPASSWORD"
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
-- | odoo)
|
||||||
|
shift
|
||||||
|
if [[ "$1" == "scaffold" ]] ; then
|
||||||
|
exec odoo "$@"
|
||||||
|
else
|
||||||
|
wait-for-psql.py ${DB_ARGS[@]} --timeout=30
|
||||||
|
exec odoo "$@" "${DB_ARGS[@]}"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
wait-for-psql.py ${DB_ARGS[@]} --timeout=30
|
||||||
|
exec odoo "$@" "${DB_ARGS[@]}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
exec "$@"
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 1
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
; =============================================================================
|
||||||
|
; Odoo 19 dev config — baked into the image. Overridable via bind mount.
|
||||||
|
;
|
||||||
|
; In dev: docker-compose mounts ./odoo.conf -> /etc/odoo/odoo.conf, so this
|
||||||
|
; baked-in copy is shadowed. In CI / prod without a bind mount, this is the
|
||||||
|
; config Odoo uses.
|
||||||
|
; =============================================================================
|
||||||
|
|
||||||
|
[options]
|
||||||
|
addons_path = /mnt/os-dev/addons,/mnt/os-dev/odoo/addons,/mnt/extra-addons
|
||||||
|
data_dir = /var/lib/odoo
|
||||||
|
|
||||||
|
; ---- Hot reload (THE WHOLE POINT) ------------------------------------------
|
||||||
|
; Full live-reload mode: Python (watchdog), QWeb templates, JS/CSS assets,
|
||||||
|
; XML view files, and werkzeug in-browser tracebacks.
|
||||||
|
dev_mode = reload,qweb,assets,xml,werkzeug
|
||||||
|
|
||||||
|
; ---- Network --------------------------------------------------------------
|
||||||
|
xmlrpc_port = 8069
|
||||||
|
; longpolling_port = 8072
|
||||||
|
; proxy_mode = True
|
||||||
|
|
||||||
|
; ---- Database -------------------------------------------------------------
|
||||||
|
; Connection params come from the entrypoint via env vars (HOST/PORT/USER/
|
||||||
|
; PASSWORD). Don't set db_host/db_port/db_user/db_password here.
|
||||||
|
dbfilter = .*
|
||||||
|
list_db = True
|
||||||
|
|
||||||
|
; ---- Logging --------------------------------------------------------------
|
||||||
|
log_level = info
|
||||||
|
|
||||||
|
; ---- Workers (single-worker = safest for hot reload) -----------------------
|
||||||
|
workers = 0
|
||||||
|
|
||||||
|
; ---- Limits ---------------------------------------------------------------
|
||||||
|
limit_memory_hard = 2684354560
|
||||||
|
limit_memory_soft = 2147483648
|
||||||
|
limit_request = 8192
|
||||||
|
limit_time_cpu = 60
|
||||||
|
limit_time_real = 120
|
||||||
|
|
||||||
|
; ---- Admin ----------------------------------------------------------------
|
||||||
|
admin_passwd = admin
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# The officially supported versions of the following packages are their
|
||||||
|
# python3-* equivalent distributed in Ubuntu 24.04 and Debian 12
|
||||||
|
asn1crypto==1.4.0 ; python_version < '3.11'
|
||||||
|
asn1crypto==1.5.1 ; python_version >= '3.11'
|
||||||
|
Babel==2.9.1 ; python_version < '3.11' # min version = 2.6.0 (Focal with security backports)
|
||||||
|
Babel==2.10.3 ; python_version >= '3.11' and python_version < '3.13'
|
||||||
|
Babel==2.17.0 ; python_version >= '3.13'
|
||||||
|
cbor2==5.4.2.post1 ; python_version < '3.11' # (Jammy)
|
||||||
|
cbor2==5.4.6 ; python_version >= '3.11' and python_version < '3.12' # (Bookworm)
|
||||||
|
cbor2==5.6.2 ; python_version >= '3.12'
|
||||||
|
chardet==4.0.0 ; python_version < '3.11' # (Jammy)
|
||||||
|
chardet==5.2.0 ; python_version >= '3.11'
|
||||||
|
cryptography==3.4.8; python_version < '3.12' # incompatibility between pyopenssl 19.0.0 and cryptography>=37.0.0
|
||||||
|
cryptography==42.0.8 ; python_version >= '3.12' # (Noble) min 41.0.7, pinning 42.0.8 for security fixes
|
||||||
|
docutils==0.17 ; python_version < '3.11' # (Jammy)
|
||||||
|
docutils==0.20.1 ; python_version >= '3.11'
|
||||||
|
freezegun==1.1.0 ; python_version < '3.11' # (Jammy)
|
||||||
|
freezegun==1.2.1 ; python_version >= '3.11' and python_version < '3.13'
|
||||||
|
freezegun==1.5.1 ; python_version >= '3.13'
|
||||||
|
geoip2==2.9.0
|
||||||
|
gevent==21.8.0 ; sys_platform != 'win32' and python_version == '3.10' # (Jammy)
|
||||||
|
gevent==22.10.2; sys_platform != 'win32' and python_version > '3.10' and python_version < '3.12'
|
||||||
|
gevent==24.2.1 ; sys_platform != 'win32' and python_version >= '3.12' and python_version < '3.13' # (Noble)
|
||||||
|
gevent==24.11.1 ; sys_platform != 'win32' and python_version >= '3.13' # (Trixie)
|
||||||
|
greenlet==1.1.2 ; sys_platform != 'win32' and python_version == '3.10' # (Jammy)
|
||||||
|
greenlet==2.0.2 ; sys_platform != 'win32' and python_version > '3.10' and python_version < '3.12'
|
||||||
|
greenlet==3.0.3 ; sys_platform != 'win32' and python_version >= '3.12' and python_version < '3.13' # (Noble)
|
||||||
|
greenlet==3.1.1 ; sys_platform != 'win32' and python_version >= '3.13' and python_version < '3.14' # (Trixie)
|
||||||
|
greenlet==3.3.2 ; sys_platform != 'win32' and python_version >= '3.14' # (Resolute)
|
||||||
|
idna==2.10 ; python_version < '3.12' # requests 2.25.1 depends on idna<3 and >=2.5
|
||||||
|
idna==3.6 ; python_version >= '3.12'
|
||||||
|
Jinja2==3.0.3 ; python_version <= '3.10'
|
||||||
|
Jinja2==3.1.2 ; python_version > '3.10'
|
||||||
|
libsass==0.20.1 ; python_version < '3.11'
|
||||||
|
libsass==0.22.0 ; python_version >= '3.11' # (Noble) Mostly to have a wheel package
|
||||||
|
lxml==4.8.0 ; python_version <= '3.10'
|
||||||
|
lxml==4.9.3 ; python_version > '3.10' and python_version < '3.12' # min 4.9.2, pinning 4.9.3 because of missing wheels for darwin in 4.9.3
|
||||||
|
lxml==5.2.1; python_version >= '3.12' and python_version < '3.13' # (Noble - removed html clean)
|
||||||
|
lxml==5.4.0; python_version >= '3.13' and python_version < '3.14' # (Trixie)
|
||||||
|
lxml==6.0.2; python_version >= '3.14' # (Resolute)
|
||||||
|
lxml-html-clean; python_version >= '3.12' # (Noble - removed from lxml, unpinned for futur security patches)
|
||||||
|
MarkupSafe==2.0.1 ; python_version <= '3.10'
|
||||||
|
MarkupSafe==2.1.2 ; python_version > '3.10' and python_version < '3.12'
|
||||||
|
MarkupSafe==2.1.5 ; python_version >= '3.12' # (Noble) Mostly to have a wheel package
|
||||||
|
num2words==0.5.10 ; python_version < '3.12' # (Jammy / Bookworm)
|
||||||
|
num2words==0.5.13 ; python_version >= '3.12'
|
||||||
|
ofxparse==0.21
|
||||||
|
openpyxl==3.0.9 ; python_version < '3.12'
|
||||||
|
openpyxl==3.1.2 ; python_version >= '3.12'
|
||||||
|
passlib==1.7.4 # min version = 1.7.2 (Focal with security backports)
|
||||||
|
Pillow==9.0.1 ; python_version <= '3.10'
|
||||||
|
Pillow==9.4.0 ; python_version > '3.10' and python_version < '3.12'
|
||||||
|
Pillow==10.2.0 ; python_version >= '3.12' and python_version < '3.13' # (Noble) Mostly to have a wheel package
|
||||||
|
Pillow==11.1.0 ; python_version >= '3.13' and python_version < '3.14' # (Trixie)
|
||||||
|
Pillow==12.1.1 ; python_version >= '3.14' # (Resolute)
|
||||||
|
polib==1.1.1
|
||||||
|
psutil==5.9.0 ; python_version <= '3.10'
|
||||||
|
psutil==5.9.4 ; python_version > '3.10' and python_version < '3.12'
|
||||||
|
psutil==5.9.8 ; python_version >= '3.12' # (Noble) Mostly to have a wheel package
|
||||||
|
psycopg2==2.9.2 ; python_version == '3.10' # (Jammy)
|
||||||
|
psycopg2==2.9.5 ; python_version == '3.11'
|
||||||
|
psycopg2==2.9.9 ; python_version >= '3.12' and python_version < '3.13' # (Noble)
|
||||||
|
psycopg2==2.9.10 ; python_version >= '3.13' # (Trixie)
|
||||||
|
pyopenssl==21.0.0 ; python_version < '3.12'
|
||||||
|
pyopenssl==24.1.0 ; python_version >= '3.12' # (Noble) min 23.2.0, pinned for compatibility with cryptography==42.0.8 and security patches
|
||||||
|
PyPDF2==1.26.0 ; python_version <= '3.10'
|
||||||
|
PyPDF2==2.12.1 ; python_version > '3.10' and python_version < '3.13' # (Noble and below)
|
||||||
|
PyPDF==5.4.0 ; python_version >= '3.13' # (Trixie)
|
||||||
|
pypiwin32 ; sys_platform == 'win32'
|
||||||
|
pyserial==3.5
|
||||||
|
python-dateutil==2.8.1 ; python_version < '3.11'
|
||||||
|
python-dateutil==2.8.2 ; python_version >= '3.11'
|
||||||
|
python-magic==0.4.24; sys_platform != 'win32' and python_version < '3.12' # (jammy)
|
||||||
|
python-magic==0.4.27; sys_platform != 'win32' and python_version >= '3.12' # (noble)
|
||||||
|
python-ldap==3.4.0 ; sys_platform != 'win32' and python_version < '3.12' # min version = 3.2.0 (Focal with security backports)
|
||||||
|
python-ldap==3.4.4 ; sys_platform != 'win32' and python_version >= '3.12' # (Noble) Mostly to have a wheel package
|
||||||
|
python-stdnum==1.17 ; python_version < '3.11' # (jammy)
|
||||||
|
python-stdnum==1.19 ; python_version >= '3.11' and python_version < '3.14'
|
||||||
|
python-stdnum==2.2 ; python_version >= '3.14' # (Resolute)
|
||||||
|
pytz # no version pinning to avoid OS perturbations
|
||||||
|
pyusb==1.2.1
|
||||||
|
qrcode==7.3.1 ; python_version < '3.11' # (jammy)
|
||||||
|
qrcode==7.4.2 ; python_version >= '3.11'
|
||||||
|
reportlab==3.6.8 ; python_version <= '3.10'
|
||||||
|
reportlab==3.6.12 ; python_version > '3.10' and python_version < '3.12'
|
||||||
|
reportlab==4.1.0 ; python_version >= '3.12' # (Noble) Mostly to have a wheel package
|
||||||
|
requests==2.25.1 ; python_version < '3.11' # versions < 2.25 aren't compatible w/ urllib3 1.26. Bullseye = 2.25.1. min version = 2.22.0 (Focal)
|
||||||
|
requests==2.31.0 ; python_version >= '3.11' # (Noble)
|
||||||
|
rjsmin==1.1.0 ; python_version < '3.11' # (jammy)
|
||||||
|
rjsmin==1.2.0 ; python_version >= '3.11'
|
||||||
|
rl-renderPM==4.0.3 ; sys_platform == 'win32' and python_version >= '3.12' # Needed by reportlab 4.1.0 but included in deb package
|
||||||
|
urllib3==1.26.5 ; python_version < '3.12' # indirect / min version = 1.25.8 (Focal with security backports)
|
||||||
|
urllib3==2.0.7 ; python_version >= '3.12' # (Noble) Compatibility with cryptography
|
||||||
|
vobject==0.9.6.1 ; python_version < '3.14'
|
||||||
|
vobject==0.9.9 ; python_version >= '3.14' # (Resolute)
|
||||||
|
Werkzeug==2.0.2 ; python_version <= '3.10'
|
||||||
|
Werkzeug==2.2.2 ; python_version > '3.10' and python_version < '3.12'
|
||||||
|
Werkzeug==3.0.1 ; python_version >= '3.12' # (Noble) Avoid deprecation warnings
|
||||||
|
xlrd==1.2.0 ; python_version < '3.12' # (jammy)
|
||||||
|
xlrd==2.0.1 ; python_version >= '3.12'
|
||||||
|
XlsxWriter==3.0.2 ; python_version < '3.12' # (jammy)
|
||||||
|
XlsxWriter==3.1.9 ; python_version >= '3.12'
|
||||||
|
xlwt==1.3.0
|
||||||
|
zeep==4.1.0 ; python_version < '3.11' # (jammy)
|
||||||
|
zeep==4.2.1 ; python_version >= '3.11' and python_version < '3.13'
|
||||||
|
zeep==4.3.1 ; python_version >= '3.13'
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import configparser
|
||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
arg_parser = argparse.ArgumentParser()
|
||||||
|
arg_parser.add_argument('--db_host')
|
||||||
|
arg_parser.add_argument('--db_port')
|
||||||
|
arg_parser.add_argument('--db_user')
|
||||||
|
arg_parser.add_argument('--db_password')
|
||||||
|
arg_parser.add_argument('--timeout', type=int, default=5)
|
||||||
|
|
||||||
|
args = arg_parser.parse_args()
|
||||||
|
|
||||||
|
config_options = {}
|
||||||
|
rc_file_path = os.environ.get('ODOO_RC')
|
||||||
|
if rc_file_path:
|
||||||
|
config = configparser.RawConfigParser()
|
||||||
|
config.read(rc_file_path)
|
||||||
|
config_options = { k:v for k,v in config.items('options')}
|
||||||
|
|
||||||
|
db_host = config_options.get('db_host') or args.db_host or os.environ.get('PGHOST')
|
||||||
|
db_port = config_options.get('db_port') or args.db_port or os.environ.get('PGPORT')
|
||||||
|
db_user = config_options.get('db_user') or args.db_user or os.environ.get('PGUSER')
|
||||||
|
db_password = config_options.get('db_password') or args.db_password or os.environ.get('PGPASSWORD')
|
||||||
|
|
||||||
|
params = {'db_host': db_host, 'db_port': db_port, 'db_user': db_user, 'db_password': db_password}
|
||||||
|
missing_params = [k for k, v in params.items() if v is None]
|
||||||
|
if missing_params:
|
||||||
|
print(f"Missing required database parameters: {', '.join(missing_params)}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
while (time.time() - start_time) < args.timeout:
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(user=db_user, host=db_host, port=db_port, password=db_password, dbname='postgres')
|
||||||
|
error = ''
|
||||||
|
break
|
||||||
|
except psycopg2.OperationalError as e:
|
||||||
|
error = e
|
||||||
|
else:
|
||||||
|
conn.close()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
print("Database connection failure: %s" % error, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
Reference in new issue
Block a user