Compare commits

..
3 Commits
Author SHA1 Message Date
oliver 66ddb386a8 c 2026-09-27 10:41:54 -03:00
oliver 9a59b2932a c 2026-09-27 10:39:39 -03:00
oliver 2e9c775599 c 2026-09-27 10:39:11 -03:00
64 changed files with 912 additions and 2346 deletions

No files matched your search

+4 -3
View File
@@ -18,12 +18,11 @@ RUN apk add --no-cache \
sshfs \
device-mapper \
borgbackup
WORKDIR /root
RUN ln -sf /usr/bin/nvim /usr/bin/vim
RUN ln -sf /4server/vault/open /usr/bin/open
COPY known_hosts /root/.ssh/
COPY config /root/.ssh/
@@ -40,5 +39,7 @@ COPY mount_volume /usr/bin/
COPY .bashrc /root/.bashrc
WORKDIR /app
RUN ln -s /app/vault/open /sbin/open
CMD ["bash"]
+6 -6
View File
@@ -1,3 +1,8 @@
#Host dev
# Hostname dev
# User oliver
# Port 2222
Host dev
Hostname dev
User 4server
@@ -25,7 +30,7 @@ Host meppel
IdentityFile /app/host_vars/meppel/meppel
Host sydney
Hostname 192.168.9.14
Hostname 192.168.9.22
User 4server
IdentityFile /app/host_vars/sydney/sydney
@@ -33,8 +38,3 @@ Host saopaulo
Hostname 192.168.9.11
User 4server
IdentityFile /app/host_vars/saopaulo/saopaulo
Host sydney2
Hostname 192.168.9.22
User 4server
IdentityFile /app/host_vars/sydney2/sydney2
-3
View File
@@ -15,6 +15,3 @@
192.168.9.21 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDtexcutExMflSRPJOwGPuZx49c+ZUMoElNPNntXONOuVZ8jM+zzA/4s+HCj+RWdHMnarLcNJjwRZRq7JWruqmr1EN9eNmvu13cLCKZr6Ape1gt8VmMhvQ9nA7eMx3UdvnWgXjLCfJy+BKh2XVAneM5lOSPMcyQlcACrLQVkse9EKhxhV5nEjXKYxSNetIPK9PxglXznpC8IpuWlXnDH2R8vDhdvBmBFTKOjN5Wa+GrMfElWeQOjyCpNcMVYohvV87VN6FxHQTADFTm2CKy/W7pnM8rI55ZbBIMfvLyhpM+vtFh1sJlIZSFnn+ytMUImNEAgBDI3fCpwZ99zeLViGhQJlBkdCUI+JJK9XJjpcOMNydeWh142RGU6juPuSLOZPqKNc3BpZYgPY0vMDgDWgT9AcYup3rAJ/UnUnhsxiT2h/gx7+t/j9xg01BQF5S6mezaueuavHCh9MvAzXiJXR8zanhl/Hh9BKgIAFrZh71CzP/PYG33BKCe9DXA09aJrf8=
192.168.9.22 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMO5VhveQe76kEOdVqrdoTwW1fNzmgDzZNBznFEC4ohtXes0VMtJR02lhc+55XTIUX+EDxBtAh8U+kJpFeLetMU=
192.168.9.21 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGlzLZWNjSaDuDd4e66LoBizY5j+QCsifIIxkvX4CrzP/AqAgWDEEgT+pAXBFkNJlBR6TDFJ0bIdwZTbcq8/72E=
192.168.9.14 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBoHCfS8EQBWo2v5m/D2S3Sy97yZcVbbLJyW2sf+Rd7c
192.168.9.14 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC3LOvVI7S+/YYRpe/SxElLVn/bRg85PtH6MeYS7DMot6Uj3SnAmOxyVmU5j9dxS+039EjoLjMr+ppx+sRqVtr78q8GhKXmCKmp2EDcieEbJ9h66Drb8JoOuQJFyL3TiXwa9MNFy5qkBahwnhF+1wuV2a82/irnqYvGFFDuNuzXRpzJlZPPQddMFryj/9J3Qu2E1vlXw30Ap1EQD0+I/Zlifj5BQpJMA6ZpOnmXPY4k3qnY/tnyufqfZJQE7DOdxY2NOPOOOYt8JsG0gaydk7+Kt8/01TLBGVKgfmlbKTkKQYQUgfc87Rx1of4AaZMkBnBjTto/x+DIaFh3BvivEUSHbF/9NYL9/cj+Alb9x5qXo4bW6TFFhC/RVPnQ0XWKaH0cDFUZ7sgQ6XXapXYvojZ4TL8SZ3xzRt3cB2t10BQGgkEOKSJ7sDltxEthHhQDzy2pQg6iuFPAL1ss4oz/UiZwTRJ3lp1kto2wVU2JzIci46GfDEV63wOsw8aubQ2LXQM=
192.168.9.14 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMnz/R4sp9QPwJ4VRnsrz9EBjf1E0YuzUEC0TlptUXuyhVcUB4ZSbvd67PsXOuVxQiIIppqnZWzNL1w5dCLJoyU=
+2 -7
View File
@@ -2,12 +2,7 @@
cmd="$*"
# Build a temporary hosts file without comments (# ...) and blank lines
tmp_hosts=$(mktemp)
trap 'rm -f "$tmp_hosts"' EXIT
sed -e 's/#.*$//' -e 's/[[:space:]]*$//' -e '/^[[:space:]]*$/d' /app/host_vars/hosts > "$tmp_hosts"
echo "Running on hosts: $cmd"
echo "Running on hosts: $(tr '\n' ' ' < "$tmp_hosts")"
echo "Command: $cmd"
pssh -h /app/host_vars/hosts -t 0 "$cmd"
pssh -h "$tmp_hosts" -t 0 "$cmd"
+5 -12
View File
@@ -7,7 +7,7 @@ fi
host_vars_dir="/app/host_vars"
keys=(NEBULA_CA API_KEY HOSTNAME NEBULA_CRT NEBULA_KEY SSH_PRIVATE SSH_PUBLIC PROVIDER COST CONTRACT_END )
keys=(NEBULA_CA API_KEY HOSTNAME NEBULA_CRT NEBULA_KEY SSH_PRIVATE SSH_PUBLIC)
NEBULA_CA=$(<"$host_vars_dir/ca.crt")
@@ -15,18 +15,8 @@ localfile="$1"
remotefile="$2"
remotetmp_base="/var/tmp/4server"
# If the destination is a directory (trailing slash), keep the local file name
case "$remotefile" in
*/) remotefile="${remotefile}$(basename "$localfile")" ;;
esac
# Read hosts from file descriptor 3 to prevent ssh from consuming stdin
while read -r host <&3 || [ -n "$host" ]; do
# Strip comments (# ...) and surrounding whitespace; skip empty lines
host="${host%%#*}"
host="${host//[[:space:]]/}"
[ -z "$host" ] && continue
while read -r host <&3; do
host_env_file="$host_vars_dir/$host/$host.env"
if [ ! -f "$host_env_file" ]; then
@@ -44,6 +34,7 @@ while read -r host <&3 || [ -n "$host" ]; do
SSH_PRIVATE=$(<"$host_vars_dir/$host/$host")
SSH_PUBLIC=$(<"$host_vars_dir/$host/$host.pub")
content=$(<"$localfile")
for key in "${keys[@]}"; do
@@ -51,6 +42,7 @@ while read -r host <&3 || [ -n "$host" ]; do
# Replace placeholder {{KEY}} with value using Bash's parameter expansion
content="${content//\{\{$key\}\}/$value}"
done
# Copy content to remote temporary file
remotetmp="${remotetmp_base}_${host}"
echo "Copying to $host:$remotefile"
@@ -60,3 +52,4 @@ while read -r host <&3 || [ -n "$host" ]; do
ssh "$host" "doas mv '$remotetmp' '$remotefile'"
done 3< /app/host_vars/hosts
+6 -5
View File
@@ -6,11 +6,12 @@ rex doas apk update
rex doas apk add bash doas rsync openssh linux-lts
### activate lts kerner
#template templates/extlinux.conf /boot/extlinux.conf
#rex doas chown root:root /boot/extlinux.conf
#rex doas chmod 644 /boot/extlinux.conf
template templates/extlinux.conf /boot/extlinux.conf
rex doas chown root:root /boot/extlinux.conf
rex doas chmod 644 /boot/extlinux.conf
# ass swap file ????
rex doas mkdir -p /4server
rex doas chmod 777 /4server
@@ -19,13 +20,13 @@ rex doas chmod 777 /4server
# ----- install nabula
echo "prsync nebula bin"
prsync -h "/app/host_vars/hosts" -avz ./sbin/nebula /4server/nebula
rex doas mv /4server/nebula /usr/bin/
rex doas mv /4server/nebula /usr/bin/
rex doas mkdir -p /etc/nebula
rex doas chmod 700 /etc/nebula
template templates/nebula/config.yml /etc/nebula/config.yml
template templates/nebula/host.key /etc/nebula/host.key
template templates/nebula/host.key /etc/nebula/host.key
template templates/nebula/host.crt /etc/nebula/host.crt
template templates/nebula/ca.crt /etc/nebula/ca.crt
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
rex doas docker system prune -a -f
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
"$(dirname "$(realpath "${BASH_SOURCE[0]:-$0}")")/listModules" "$1"
File renamed without changes.
@@ -17,7 +17,7 @@ RANDOM_FILE="/4server/tmp/import_$(date '+%Y%m%d_%H%M').zip"
gdown "$1" -O "$RANDOM_FILE"
# Execute dbVersion on the downloaded file and capture output
VERSION=$(/4server/sbin/ODOO_19/dbVersion "$RANDOM_FILE")
VERSION=$("$(dirname "$(realpath "${BASH_SOURCE[0]:-$0}")")/dbVersion" "$RANDOM_FILE")
# Output JSON to original stdout
cat <<EOF >&3
@@ -29,4 +29,3 @@ EOF
# Close saved stdout
exec 3>&-
@@ -1,22 +1,19 @@
#!/bin/bash
export PATH=/4PROJECTS/bin:$PATH
if [ ! -n "$1" ]; then
echo "Missing Parameters <UUID>"
exit 0
fi
UUID=$1
source /4server/sbin/helpers
CONTAINER_ID="$1"
_self="$(realpath "${BASH_SOURCE[0]:-$0}")"; SBIN_ROOT="$(dirname "$(dirname "$_self")")" # parent of ODOO/
source "$SBIN_ROOT/helpers"
[ -z "$CONTAINER_ID" ] && die "Usage: $0 <container_id>"
load_container_config "$CONTAINER_ID"
get_contract_info
DB_NAME="${DB_NAME:-$CONTAINER_ID}"
# Query installed modules from database and save CSV in variable
DB_MODULES_CSV=$(PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" PAGER= psql \
-h "$POSTGRES_HOST" \
-U "$POSTGRES_ADMIN_USER" \
-p "$POSTGRES_PORT" \
-d "$UUID" \
-d "$DB_NAME" \
--csv \
-c "
SELECT
@@ -30,26 +27,26 @@ ORDER BY name;
")
# Check if container exists and is running
if ! doas docker ps --format "{{.Names}}" | grep -q "^${UUID}$"; then
echo "Error: Container $UUID is not running" >&2
if ! doas docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_ID}$"; then
echo "Error: Container $CONTAINER_ID is not running" >&2
exit 1
fi
# Get addons_path from odoo.conf in container (try multiple possible locations)
ADDONS_PATH_RAW=""
for ODOO_CONF in "/etc/odoo.conf" "/etc/odoo/odoo.conf" "/opt/odoo/etc/odoo.conf"; do
ADDONS_PATH_RAW=$(doas docker exec "$UUID" grep "^addons_path" "$ODOO_CONF" 2>/dev/null | sed 's/.*=//' | xargs)
ADDONS_PATH_RAW=$(doas docker exec "$CONTAINER_ID" grep "^addons_path" "$ODOO_CONF" 2>/dev/null | sed 's/.*=//' | xargs)
[ -n "$ADDONS_PATH_RAW" ] && break
done
# Also check standard Odoo library locations
ODOO_LIB_PATHS=""
# Check standard docker installation path
if doas docker exec "$UUID" test -d "/lib/python3/dist-packages/odoo/addons" 2>/dev/null; then
if doas docker exec "$CONTAINER_ID" test -d "/lib/python3/dist-packages/odoo/addons" 2>/dev/null; then
ODOO_LIB_PATHS="/lib/python3/dist-packages/odoo/addons"
fi
# Also check other possible locations
for lib_path in $(doas docker exec "$UUID" find /usr/lib /usr/local/lib -maxdepth 3 -type d -path "*/python3*/site-packages/odoo/addons" 2>/dev/null); do
for lib_path in $(doas docker exec "$CONTAINER_ID" find /usr/lib /usr/local/lib -maxdepth 3 -type d -path "*/python3*/site-packages/odoo/addons" 2>/dev/null); do
ODOO_LIB_PATHS="$ODOO_LIB_PATHS $lib_path"
done
@@ -67,7 +64,7 @@ if [ -n "$ADDONS_PATH_RAW" ]; then
[ -z "$addons_dir" ] && continue
# Get all directories in this addons path
doas docker exec "$UUID" find "$addons_dir" -maxdepth 1 -type d 2>/dev/null | \
doas docker exec "$CONTAINER_ID" find "$addons_dir" -maxdepth 1 -type d 2>/dev/null | \
sed "s|^$addons_dir/||" | sed "s|^$addons_dir$||" | grep -v "^$" >> "$FILESYSTEM_MODULES"
done
fi
@@ -79,7 +76,7 @@ if [ -n "$ODOO_LIB_PATHS" ]; then
[ -z "$lib_path" ] && continue
# Get all directories in this library path
doas docker exec "$UUID" find "$lib_path" -maxdepth 1 -type d 2>/dev/null | \
doas docker exec "$CONTAINER_ID" find "$lib_path" -maxdepth 1 -type d 2>/dev/null | \
sed "s|^$lib_path/||" | sed "s|^$lib_path$||" | grep -v "^$" >> "$FILESYSTEM_MODULES"
done
fi
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
export PATH=/4PROJECTS/bin:$PATH
CONTAINER_ID="$1"
_self="$(realpath "${BASH_SOURCE[0]:-$0}")"; SBIN_ROOT="$(dirname "$(dirname "$_self")")" # parent of ODOO/
source "$SBIN_ROOT/helpers"
[ -z "$CONTAINER_ID" ] && die "Usage: $0 <container_id>"
load_container_config "$CONTAINER_ID"
get_contract_info
DB_NAME="${DB_NAME:-$CONTAINER_ID}"
log "shell: CONTAINER_ID=$CONTAINER_ID DB_NAME=$DB_NAME"
doas docker exec -it "$CONTAINER_ID" odoo shell --db_host beedb --db_password="$ODOO_DB_PASSWORD" -d "$DB_NAME" --db_user="$DB_NAME"
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
export PATH=/4PROJECTS/bin:$PATH
# --------------------------------------------------
# Validate parameter
# --------------------------------------------------
CONTAINER_ID="$1"
_self="$(realpath "${BASH_SOURCE[0]:-$0}")"; SBIN_ROOT="$(dirname "$(dirname "$_self")")" # parent of ODOO/
source "$SBIN_ROOT/helpers"
[ -z "$CONTAINER_ID" ] && die "Usage: $0 <container_id>"
load_container_config "$CONTAINER_ID"
get_contract_info
DB_NAME="${DB_NAME:-$CONTAINER_ID}"
doas docker ps -a --format "{{.Names}}" | grep -q "^${CONTAINER_ID}$" \
|| die "Container $CONTAINER_ID does not exist"
log "Updating all Odoo modules: DB=$DB_NAME host=$POSTGRES_HOST"
doas docker exec -u odoo "$CONTAINER_ID" odoo \
-u all \
-d "$DB_NAME" \
--stop-after-init \
--db_host="$POSTGRES_HOST" \
--db_port="$POSTGRES_PORT" \
--db_user="$POSTGRES_ADMIN_USER" \
--db_password="$POSTGRES_ADMIN_PASSWORD" \
|| die "Odoo module update failed for $CONTAINER_ID"
log "Module update complete — restarting $CONTAINER_ID"
"$SBIN_ROOT/startContainer" "$CONTAINER_ID"
-144
View File
@@ -1,144 +0,0 @@
#!/bin/bash
dump_config (){
echo "$(date '+%Y-%m-%d %H:%M:%S') ========== Odoo Container Configuration =========="
echo "UUID: $UUID"
echo "BRANCH: $BRANCH"
echo "STAGING: $STAGING"
echo
echo "PostgreSQL Host: $POSTGRES_HOST"
echo "PostgreSQL Port: $POSTGRES_PORT"
echo "PostgreSQL Admin: $POSTGRES_ADMIN_USER"
echo "ODOO DB User: $ODOO_DB_USER"
echo
echo "BASEURL: $BASEURL"
echo "DATA_DIR: $DATA_DIR"
echo "CUSTOM_DIR: $CUSTOM_DIR"
echo "ENTERPRISE_DIR: $ENTERPRISE_DIR"
echo "LOGS_DIR: $LOGS_DIR"
echo "CONFIG_DIR: $CONFIG_DIR"
echo "CC_DIR: $CC_DIR"
echo "BACKUP_DIR: $BACKUP_DIR"
echo "GIT_DIR: $GIT_DIR"
echo "ETC_DIR: $ETC_DIR"
echo "INSTALL_DIR: $INSTALL_DIR"
echo "SSH_DIR: $SSH_DIR"
echo "HUGO_DIR: $HUGO_DIR"
echo
echo "SERVER_IP: $SERVER_IP"
echo "=================================================="
}
# -----------------------------
# Function: Create PostgreSQL user
# -----------------------------
# Run a query as postgres admin. Returns psql's exit status so callers can tell
# "query returned nothing" apart from "could not connect".
pg_admin() {
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql -h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres -tAc "$1"
}
# Wait until Postgres accepts connections (default: up to 60s).
wait_for_postgres() {
local tries="${1:-30}"
local i
for ((i = 1; i <= tries; i++)); do
if pg_admin "SELECT 1;" >/dev/null 2>&1; then
return 0
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') Postgres at $POSTGRES_HOST:$POSTGRES_PORT not reachable yet ($i/$tries), retrying in 2s"
sleep 2
done
return 1
}
check_and_create_db() {
echo "$(date '+%Y-%m-%d %H:%M:%S') Checking user and database for $UUID (DB user $ODOO_DB_USER) as $POSTGRES_ADMIN_USER on $POSTGRES_HOST:$POSTGRES_PORT"
# -----------------------------
# Never guess: if Postgres cannot be queried we must NOT assume the DB is
# missing, because a restore wipes the filestore.
# -----------------------------
if ! wait_for_postgres; then
echo "$(date '+%Y-%m-%d %H:%M:%S') ERROR: cannot connect to Postgres at $POSTGRES_HOST:$POSTGRES_PORT - skipping DB check, NO restore performed"
return 1
fi
# -----------------------------
# Check if user exists
# -----------------------------
USER_EXISTS=$(PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql -h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='$ODOO_DB_USER';" | grep -q 1 && echo "yes" || echo "no")
if [ "$USER_EXISTS" = "no" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') PostgreSQL user $ODOO_DB_USER does not exist, creating it"
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql -h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres -c "CREATE USER \"$ODOO_DB_USER\" WITH PASSWORD '$ODOO_DB_PASSWORD';"
echo "$(date '+%Y-%m-%d %H:%M:%S') CREATE USER $ODOO_DB_USER finished with exit code $?"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') PostgreSQL user $ODOO_DB_USER already exists"
fi
# -----------------------------
# Check if database exists
# -----------------------------
DB_RESULT=$(pg_admin "SELECT 1 FROM pg_database WHERE datname='$UUID';")
DB_RC=$?
if [ $DB_RC -ne 0 ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') ERROR: existence query for database $UUID failed (rc=$DB_RC) - NO restore performed"
return 1
fi
if [ "$DB_RESULT" = "1" ]; then
DB_EXISTS="yes"
else
DB_EXISTS="no"
fi
if [ "$DB_EXISTS" = "no" ]; then
FILESTORE_HOST="/4server/data/$UUID/odoo/odoo-web-data/filestore"
if [ -d "$FILESTORE_HOST" ] && [ -n "$(ls -A "$FILESTORE_HOST" 2>/dev/null)" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') ERROR: database $UUID not found but a filestore exists at $FILESTORE_HOST - refusing automatic restore of default.zip (would delete the filestore). Restore manually if intended."
return 1
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') Database $UUID does not exist and no filestore found, restoring default.zip"
/4server/sbin/ODOO_19/restore $UUID default.zip
local restore_rc=$?
echo "$(date '+%Y-%m-%d %H:%M:%S') Restore of default.zip for $UUID finished with exit code $restore_rc"
return $restore_rc
else
echo "$(date '+%Y-%m-%d %H:%M:%S') Database $UUID already exists, nothing to do"
fi
}
# -----------------------------
# Function: Check DNS and build Traefik labels
# -----------------------------
check_domains() {
local domains="$1"
local server_ip="$2"
echo "Checking DNS resolution for domains: $domains"
local filtered_domains=""
for domain in $domains; do
ns_ip=$(nslookup "$domain" 2>/dev/null | grep -Eo 'Address: ([0-9]{1,3}\.){3}[0-9]{1,3}' | awk '{print $2}' | tail -n1)
if [[ "$ns_ip" == "$server_ip" ]]; then
filtered_domains+=" $domain"
fi
done
filtered_domains=$(echo "$filtered_domains" | xargs)
DOMAIN_LABEL=""
for domain in $filtered_domains; do
if [ -z "$DOMAIN_LABEL" ]; then
DOMAIN_LABEL="traefik.http.routers.$UUID.rule=Host(\`$domain\`)"
else
DOMAIN_LABEL+=" || Host(\`$domain\`)"
fi
done
echo "$DOMAIN_LABEL"
}
-3
View File
@@ -1,3 +0,0 @@
#!/bin/bash
/4server/sbin/ODOO_19/listModules $1
-101
View File
@@ -1,101 +0,0 @@
#!/bin/bash
export PATH=/4PROJECTS/bin:$PATH
if [ ! -n "$2" ]; then
echo "Missing Parameters <UUID> <FILE>"
exit 0
fi
UUID=$1
echo "UUID: $UUID"
source /4server/sbin/helpers
get_contract_info
#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
echo "PASSWORD $ODOO_DB_PASSWORD"
echo "Restoring $FILENAME to $UUID"
echo "status of container"
doas docker ps -a --filter "id=$UUID"
echo "POSTGRES HOST: $POSTGRES_HOST"
BACKUP="/mnt/backup/$2"
TEMPLATE="/mnt/db_images/$2"
doas docker exec "$UUID" /bin/bash -c "[ -f $TEMPLATE ]"
if doas docker exec "$UUID" /bin/bash -c "[ -f $BACKUP ]"; then
FILENAME="$BACKUP"
elif doas docker exec "$UUID" /bin/bash -c "[ -f $TEMPLATE ]"; then
FILENAME="$TEMPLATE"
else
echo "File not exists"
exit 0
fi
source /4server/sbin/ODOO_19/ODOO_19.lib
# Abort BEFORE touching anything if Postgres is not reachable
if ! wait_for_postgres; then
echo "ERROR: cannot connect to Postgres at $POSTGRES_HOST:$POSTGRES_PORT - restore aborted, nothing was changed"
exit 1
fi
### DELETE AND CREATE DATABASE
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql -h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -d postgres -c "
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE datname = '$UUID' AND pid <> pg_backend_pid();
"
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql -h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -d postgres -c "
DROP DATABASE IF EXISTS \"$UUID\";
"
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres \
-c "ALTER ROLE \"$UUID\" CREATEDB;"
# Move the old filestore aside instead of deleting it; it is only removed after a successful load
OLD_FS="/home/odoo/.local/share/Odoo/.filestore.pre-restore"
doas docker exec "$UUID" rm -rf "$OLD_FS"
doas docker exec "$UUID" sh -c "[ -d /home/odoo/.local/share/Odoo/filestore ] && mv /home/odoo/.local/share/Odoo/filestore $OLD_FS; true"
doas docker exec "$UUID" rm -rf /root/.local/share/Odoo/filestore
if ! doas docker exec "$UUID" odoo db --db_host beedb -w "$ODOO_DB_PASSWORD" -r "$UUID" load "$UUID" $FILENAME -f; then
echo "ERROR: odoo db load failed - putting the old filestore back"
# Restrict database access to its UUID owner
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" \
-p "$POSTGRES_PORT" -d postgres \
-v ON_ERROR_STOP=1 \
-c "REVOKE CONNECT, TEMPORARY ON DATABASE \"$UUID\" FROM PUBLIC;
GRANT CONNECT ON DATABASE \"$UUID\" TO \"$UUID\";
ALTER DATABASE \"$UUID\" OWNER TO \"$UUID\";
ALTER ROLE \"$UUID\" NOCREATEDB;"
doas docker exec "$UUID" sh -c "[ -d $OLD_FS ] && rm -rf /home/odoo/.local/share/Odoo/filestore && mv $OLD_FS /home/odoo/.local/share/Odoo/filestore; true"
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres \
-c "ALTER ROLE \"$UUID\" NOCREATEDB;"
exit 1
fi
doas docker exec "$UUID" rm -rf "$OLD_FS"
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres \
-c "ALTER ROLE \"$UUID\" NOCREATEDB;"
doas docker exec "$UUID" cp -r /root/.local/share/Odoo/filestore /home/odoo/.local/share/Odoo/filestore
doas docker exec "$UUID" chown -R odoo:odoo /home/odoo/.local
doas docker exec "$UUID" mkdir -p /var/lib/odoo/.local/share/Odoo/
doas docker exec "$UUID" ln -s /home/odoo/.local/share/Odoo/filestore /var/lib/odoo/.local/share/Odoo/filestore
docker restart "$UUID"
-22
View File
@@ -1,22 +0,0 @@
#!/bin/bash
export PATH=/4PROJECTS/bin:$PATH
if [ ! -n "$1" ]; then
echo "Missing Parameters <UUID>"
exit 0
fi
UUID=$1
echo "UUID: $UUID"
source /4server/sbin/helpers
get_contract_info
#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
echo "PASSWORD $ODOO_DB_PASSWORD"
echo "POSTGRES HOST: $POSTGRES_HOST"
doas docker exec -it "$UUID" odoo shell --db_host beedb --db_password="$ODOO_DB_PASSWORD" -d "$UUID" --db_user="$UUID"
-60
View File
@@ -1,60 +0,0 @@
#!/bin/bash
export PATH=/4PROJECTS/bin:$PATH
# --------------------------------------------------
# Validate parameter
# --------------------------------------------------
if [ -z "$1" ]; then
echo "Missing Parameters <UUID>"
exit 1
fi
UUID=$1
# --------------------------------------------------
# Load environment helpers
# --------------------------------------------------
source /4server/sbin/helpers
if ! get_contract_info "$UUID"; then
echo "Error: Failed to load contract info for $UUID" >&2
exit 1
fi
# --------------------------------------------------
# Verify container exists
# --------------------------------------------------
if ! doas docker ps -a --format "{{.Names}}" | grep -q "^${UUID}$"; then
echo "Error: Container $UUID does not exist" >&2
exit 1
fi
echo "Updating all Odoo modules for database: $UUID"
echo "Using DB host: $POSTGRES_HOST"
# --------------------------------------------------
# Run Odoo update inside container
# --------------------------------------------------
if ! doas docker exec -u odoo "$UUID" odoo \
-u all \
-d "$UUID" \
--stop-after-init \
--db_host="$POSTGRES_HOST" \
--db_port="$POSTGRES_PORT" \
--db_user="$POSTGRES_ADMIN_USER" \
--db_password="$POSTGRES_ADMIN_PASSWORD"
then
echo "Error: Odoo module update failed for $UUID" >&2
exit 1
fi
echo "Module update completed successfully for $UUID"
# --------------------------------------------------
# Restart container
# --------------------------------------------------
echo "Starting container $UUID"
/4server/sbin/startContainer "$UUID"
echo "Done."
exit 0
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env bash
# List containers whose contract has not expired yet, next to expire first.
# Output: UUID, email, expires (header + aligned columns).
DB_PATH="/4server/data/contracts.db"
if [ ! -f "$DB_PATH" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') ERROR: database not found at $DB_PATH" >&2
exit 1
fi
sqlite3 -header -column "$DB_PATH" "
SELECT UUID AS uuid, email, expires
FROM containers
WHERE expires > date('now')
ORDER BY expires ASC;
"
+76 -127
View File
@@ -17,17 +17,9 @@ from fastapi.responses import PlainTextResponse, RedirectResponse
from fastapi.security.api_key import APIKeyHeader
from pydantic import BaseModel
import logging
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
)
log = logging.getLogger("4server.api")
# ---------------------- Constants ----------------------
DB_PATH = "/4server/data/contracts.db"
BIN_PATH = "/4server/sbin"
BIN_PATH = os.getenv("SBIN_PATH", "/usr/bin")
API_KEY = os.getenv("API_KEY", "your-secret-api-key")
VERSION = "API: 0.0.9"
@@ -66,11 +58,11 @@ def init_db():
CREATE TABLE IF NOT EXISTS containers (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
UUID CHAR(50) UNIQUE,
container_id CHAR(50) UNIQUE,
email CHAR(100),
expires DATE,
tags TEXT,
env TEXT,
affiliate CHAR(30),
image CHAR(50),
history TEXT,
comment TEXT,
@@ -97,29 +89,13 @@ def init_db():
def execute_db(query: str, params: tuple = (), fetch: bool = False):
log.debug("DB open: path=%s fetch=%s", DB_PATH, fetch)
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
try:
cursor.execute(query, params)
conn.commit()
rowcount = cursor.rowcount
data = cursor.fetchall() if fetch else None
except Exception as exc:
log.error(
"DB error: %s | query=%s | params=%s", exc, query, params
)
raise
finally:
conn.close()
log.debug(
"DB done: rowcount=%s rows=%s | query=%s | params=%s",
rowcount,
len(data) if data else 0,
query,
params,
)
cursor.execute(query, params)
conn.commit()
data = cursor.fetchall() if fetch else None
conn.close()
if data and fetch:
return [dict(row) for row in data]
return None
@@ -134,7 +110,7 @@ class ContractItem(BaseModel):
class ContainerModel(BaseModel):
UUID: str
container_id: str
email: Optional[str] = None
expires: Optional[str] = None
tags: Optional[str] = None
@@ -157,12 +133,12 @@ class ContainerModel(BaseModel):
env_list: Optional[str] = None
class UUIDRequest(BaseModel):
UUID: str
class ContainerRequest(BaseModel):
container_id: str
class CommandRequest(BaseModel):
uuid: str
container_id: str
method: int
@@ -177,13 +153,29 @@ class MoveRequest(BaseModel):
class AddKeyRequest(BaseModel):
key: str
uuid: str
container_id: str
class ImageRequest(BaseModel):
image: str
# ---------------------- Routes ----------------------
@app.get("/", include_in_schema=False)
def redirect_to_odoo():
return RedirectResponse(url="https://ODOO4PROJECTS.com")
import json
from typing import Any, Dict, Optional
from fastapi import Depends, FastAPI
from fastapi.responses import RedirectResponse
from pydantic import BaseModel
app = FastAPI()
# ---------------------- Routes ----------------------
@app.get("/", include_in_schema=False)
def redirect_to_odoo():
@@ -192,34 +184,22 @@ def redirect_to_odoo():
@app.post("/container/update", dependencies=[Depends(verify_api_key)])
def update_container(request: ContainerModel):
payload = (
request.model_dump()
if hasattr(request, "model_dump")
else request.dict()
)
log.info("update_container called: UUID=%r payload=%s", request.UUID, payload)
# Fetch existing record
existing = execute_db(
"SELECT * FROM containers WHERE UUID = ?", (request.UUID,), fetch=True
)
log.info(
"update_container lookup: UUID=%r existing=%s",
request.UUID,
existing,
"SELECT * FROM containers WHERE UUID = ?", (request.container_id,), fetch=True
)
if not existing:
log.info("update_container: no record for UUID=%r -> INSERT", request.UUID)
# If record does not exist, insert a new one with all given fields
execute_db(
"""
INSERT INTO containers (UUID, email, expires, tags, image, history,
INSERT INTO containers (UUID, container_id, email, expires, tags, image, history,
comment, domains, status, created, bump, git, backup_slots, hdd, workers,
utm_source, utm_medium, utm_campaign, domains_slots, secret_list, env_list)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
request.UUID,
request.container_id,
request.container_id,
request.email,
request.expires,
request.tags,
@@ -242,86 +222,73 @@ def update_container(request: ContainerModel):
request.env_list,
),
)
inserted = execute_db(
"SELECT * FROM containers WHERE UUID = ?", (request.UUID,), fetch=True
)
log.info("update_container INSERT result: %s", inserted)
return {"UUID": request.UUID, "status": "created"}
return {"container_id": request.container_id, "status": "created"}
# Existing record found, do partial update
existing = existing[0] # Assuming fetch returns list of dicts
log.info("update_container: existing row before update=%s", existing)
updates = {}
params = []
# Only add fields that are not None in the request
model_fields = getattr(ContainerModel, "model_fields", None) or ContainerModel.__fields__
for field in model_fields:
if field == "UUID":
for field in ContainerModel.__fields__:
if field == "container_id":
continue
value = getattr(request, field)
if value is not None:
updates[field] = value
params.append(value)
log.info("update_container: computed updates=%s params=%s", updates, params)
if updates:
# Build SQL dynamically
# Build SQL dynamically; always keep UUID in sync with container_id
set_clause = ", ".join(f"{k}=?" for k in updates.keys())
params.append(request.UUID) # UUID for WHERE clause
sql = f"UPDATE containers SET {set_clause} WHERE UUID=?"
log.info("update_container: executing SQL=%s params=%s", sql, tuple(params))
execute_db(sql, tuple(params))
after = execute_db(
"SELECT * FROM containers WHERE UUID = ?", (request.UUID,), fetch=True
)
log.info("update_container: row after update=%s", after)
set_clause += ", UUID=?"
params.append(request.container_id) # value for UUID=?
params.append(request.container_id) # value for WHERE UUID=?
execute_db(f"UPDATE containers SET {set_clause} WHERE UUID=?", tuple(params))
return {
"UUID": request.UUID,
"container_id": request.container_id,
"status": "updated",
"fields_updated": list(updates.keys()),
}
log.info("update_container: no non-None fields supplied -> no_change")
return {"UUID": request.UUID, "status": "no_change"}
return {"container_id": request.container_id, "status": "no_change"}
@app.post("/container/start", dependencies=[Depends(verify_api_key)])
def start_container(request: UUIDRequest):
return {"message": run_command([f"{BIN_PATH}/startContainer", request.UUID])}
def start_container(request: ContainerRequest):
return {"message": run_command([f"{BIN_PATH}/startContainer", request.container_id])}
@app.post("/container/restartAllContainers", dependencies=[Depends(verify_api_key)])
def restart_all_containers_by_image(request: ImageRequest):
def start_container(request: ImageRequest):
return {"message": run_command([f"{BIN_PATH}/restartContainers", request.image])}
@app.post("/container/stop", dependencies=[Depends(verify_api_key)])
def stop_container(request: UUIDRequest):
def stop_container(request: ContainerRequest):
try:
return {"message": run_command([f"{BIN_PATH}/stopContainer", request.UUID])}
return {"message": run_command([f"{BIN_PATH}/stopContainer", request.container_id])}
except HTTPException:
# Command failed, but continue without error to the API
return {"message": "Container stop command executed (may have failed)"}
@app.post("/container/nuke", dependencies=[Depends(verify_api_key)])
def nuke_container(request: UUIDRequest):
def nuke_container(request: ContainerRequest):
status = execute_db(
"SELECT status FROM containers WHERE UUID=?", (request.UUID,), fetch=True
"SELECT status FROM containers WHERE UUID=?", (request.container_id,), fetch=True
)
if not status or status[0]["status"] != "nuke":
raise HTTPException(400, "Container status is not 'nuke'")
return {"message": run_command([f"{BIN_PATH}/nukeContainer", request.UUID])}
return {"message": run_command([f"{BIN_PATH}/nukeContainer", request.container_id])}
@app.post("/container/info", dependencies=[Depends(verify_api_key)])
def info_container(request: Optional[UUIDRequest] = None):
def info_container(request: Optional[ContainerRequest] = None):
# Fields to select
fields = [
"ID",
"UUID",
"container_id",
"email",
"expires",
"tags",
@@ -347,7 +314,7 @@ def info_container(request: Optional[UUIDRequest] = None):
if request:
rows = execute_db(
f"SELECT {field_str} FROM containers WHERE UUID=?",
(str(request.UUID),),
(str(request.container_id),),
fetch=True,
)
else:
@@ -370,20 +337,20 @@ def info_container(request: Optional[UUIDRequest] = None):
@app.post("/container/bump", dependencies=[Depends(verify_api_key)])
def bump_container(request: UUIDRequest):
def bump_container(request: ContainerRequest):
today = datetime.utcnow().strftime("%Y-%m-%d")
execute_db("UPDATE containers SET bump=? WHERE UUID=?", (today, request.UUID))
msg = run_command([f"{BIN_PATH}/bumpContainer", request.UUID])
execute_db("UPDATE containers SET bump=? WHERE UUID=?", (today, request.container_id))
msg = run_command([f"{BIN_PATH}/bumpContainer", request.container_id])
return {"message": msg, "bump_date": today}
@app.post("/container/quota", dependencies=[Depends(verify_api_key)])
def container_quota(request: UUIDRequest):
def container_quota(request: ContainerRequest):
output = run_command(
[
"docker",
"stats",
request.UUID,
request.container_id,
"--no-stream",
"--format",
"{{.MemUsage}},{{.BlockIO}}",
@@ -456,9 +423,9 @@ def restart_all_containers(request: ImageRequest):
@app.post("/client/git", dependencies=[Depends(verify_api_key)])
def git_tool(request: CommandRequest):
if request.method == 1:
command = [f"{BIN_PATH}/gitPull", request.uuid]
command = [f"{BIN_PATH}/gitPull", request.container_id]
elif request.method == 2:
command = [f"{BIN_PATH}/gitRevert", request.uuid]
command = [f"{BIN_PATH}/gitRevert", request.container_id]
else:
raise HTTPException(status_code=400, detail="Invalid method")
@@ -468,10 +435,10 @@ def git_tool(request: CommandRequest):
@app.post("/client/addGitKey", dependencies=[Depends(verify_api_key)])
def add_git_key(request: AddKeyRequest):
if not request.key or not request.uuid:
raise HTTPException(status_code=400, detail="Key and uuid are required")
if not request.key or not request.container_id:
raise HTTPException(status_code=400, detail="Key and container_id are required")
file_path = f"/4server/data/{request.uuid}/git-server/keys/id_rsa.pub"
file_path = f"/4server/data/{request.container_id}/git-server/keys/id_rsa.pub"
try:
# Ensure the directory exists
@@ -482,26 +449,26 @@ def add_git_key(request: AddKeyRequest):
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to save key: {str(e)}")
return {"message": f"Key saved for container {request.uuid}", "file": file_path}
return {"message": f"Key saved for container {request.container_id}", "file": file_path}
@app.get("/client/audit/{uuid}", dependencies=[Depends(verify_api_key)])
def audit_odoo(uuid: str):
return {"message": run_command([f"{BIN_PATH}/ODOO_19/listModules", uuid])}
@app.get("/client/audit/{container_id}", dependencies=[Depends(verify_api_key)])
def audit_odoo(container_id: str):
return {"message": run_command([f"{BIN_PATH}/ODOO/listModules", container_id])}
@app.get("/client/logs/{uuid}", dependencies=[Depends(verify_api_key)])
async def get_odoo_log_summary(uuid: str):
if not re.fullmatch(r"[0-9a-fA-F\-]+", uuid):
@app.get("/client/logs/{container_id}", dependencies=[Depends(verify_api_key)])
async def get_odoo_log_summary(container_id: str):
if not re.fullmatch(r"[a-zA-Z0-9_-]+", container_id):
raise HTTPException(
status_code=400,
detail="Invalid UUID format. Only numbers, letters a-f, and '-' are allowed.",
detail="Invalid container_id format. Only letters, numbers, hyphens, and underscores are allowed.",
)
BASE_LOG_DIR = "/4server/data"
# Build file paths as strings
project_dir = os.path.join(BASE_LOG_DIR, uuid)
project_dir = os.path.join(BASE_LOG_DIR, container_id)
odoo_log_file = os.path.join(project_dir, "logs", "odoo.log")
git_log_file = os.path.join(project_dir, "logs", "git.log")
@@ -546,7 +513,7 @@ async def get_odoo_log_summary(uuid: str):
last_50_git_lines = read_last_lines(git_log_file, 50)
return {
"uuid": str(uuid),
"container_id": str(container_id),
"important_odoo_log_lines": important_odoo_lines,
"last_git_log_lines": last_50_git_lines,
}
@@ -560,7 +527,7 @@ def backup_import(request: ImportRequest):
if not request.filename:
raise HTTPException(status_code=400, detail="Filename is required")
command = [f"{BIN_PATH}/ODOO_19/import", request.filename]
command = [f"{BIN_PATH}/ODOO/import", request.filename]
output = run_command(command)
return {"message": output}
@@ -587,30 +554,12 @@ def backup_move(request: MoveRequest):
class BackupCheckRequest(BaseModel):
uuid: Optional[str] = None
container_id: str
@app.post("/backup/check", dependencies=[Depends(verify_api_key)])
def backup_check(request: BackupCheckRequest):
if request.uuid:
return {"message": run_command([f"{BIN_PATH}/checkBackup", request.uuid])}
# No uuid: check all active containers. checkBackup -a exits 1 when any
# backup is NOK, but its table is still the valid result, so don't raise.
result = subprocess.run(
[f"{BIN_PATH}/checkBackup", "-a"], capture_output=True, text=True
)
if result.returncode != 0 and not result.stdout.strip():
raise HTTPException(
status_code=500,
detail=f"checkBackup -a failed: {result.stderr.strip() or 'no output'}",
)
return {"message": result.stdout}
@app.get("/containers/active", dependencies=[Depends(verify_api_key)])
def containers_active():
return {"message": run_command([f"{BIN_PATH}/activeContainers"])}
return {"message": run_command([f"{BIN_PATH}/checkBackup", request.container_id])}
@app.get("/backup/all", dependencies=[Depends(verify_api_key)])
-591
View File
@@ -1,591 +0,0 @@
#!/usr/bin/env python3
import json
import os
import re
import sqlite3
import subprocess
import time
from collections import deque
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional
import psutil
import uvicorn
from fastapi import Depends, FastAPI, HTTPException, Response
from fastapi.responses import PlainTextResponse, RedirectResponse
from fastapi.security.api_key import APIKeyHeader
from pydantic import BaseModel
# ---------------------- Constants ----------------------
DB_PATH = "/4server/data/contracts.db"
BIN_PATH = "/4server/sbin"
API_KEY = os.getenv("API_KEY", "your-secret-api-key")
VERSION = "API: 0.0.9"
# ---------------------- FastAPI App ----------------------
app = FastAPI()
api_key_header = APIKeyHeader(name="X-API-Key")
def verify_api_key(key: str = Depends(api_key_header)):
if key != API_KEY:
raise HTTPException(status_code=403, detail="Unauthorized")
# ---------------------- Helpers ----------------------
def run_command(cmd: list[str]) -> str:
"""Run a shell command and return stdout or raise HTTPException on error."""
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
error_msg = (
f"Command failed\n"
f"Command: {' '.join(cmd)}\n"
f"Return code: {result.returncode}\n"
f"Stdout: {result.stdout.strip()}\n"
f"Stderr: {result.stderr.strip() or 'None'}"
)
raise HTTPException(status_code=500, detail=error_msg)
return result.stdout.strip()
def init_db():
"""Initialize the database with containers table."""
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS containers (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
UUID CHAR(50) UNIQUE,
email CHAR(100),
expires DATE,
tags TEXT,
env TEXT,
affiliate CHAR(30),
image CHAR(50),
history TEXT,
comment TEXT,
domains TEXT,
status CHAR(20),
created DATE,
bump DATE,
secret TEXT,
contract TEXT,
git integer,
backup_slots integer,
hdd integer,
workers integer,
utm_source text,
utm_medium text,
utm_campaign text,
domains_slots integer,
secret_list TEXT,
env_list TEXT
)
""")
conn.commit()
conn.close()
def execute_db(query: str, params: tuple = (), fetch: bool = False):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(query, params)
conn.commit()
data = cursor.fetchall() if fetch else None
conn.close()
if data and fetch:
return [dict(row) for row in data]
return None
# ---------------------- Models ----------------------
class ContractItem(BaseModel):
quantity: int
name: str
product_id: int
features: Dict[str, Any]
class ContainerModel(BaseModel):
UUID: str
email: Optional[str] = None
expires: Optional[str] = None
tags: Optional[str] = None
image: Optional[str] = None
history: Optional[str] = None
comment: Optional[str] = None
domains: Optional[str] = None
status: Optional[str] = None
created: Optional[str] = None
bump: Optional[str] = None
git: Optional[int] = None
backup_slots: Optional[int] = None
hdd: Optional[int] = None
workers: Optional[int] = None
utm_source: Optional[str] = None
utm_medium: Optional[str] = None
utm_campaign: Optional[str] = None
domains_slots: Optional[int] = None
<<<<<<< HEAD
secret_list: Optional[str] = None
=======
>>>>>>> c583649 (hmm)
env_list: Optional[str] = None
class UUIDRequest(BaseModel):
UUID: str
class CommandRequest(BaseModel):
uuid: str
method: int
class ImportRequest(BaseModel):
filename: str
class MoveRequest(BaseModel):
source: str
destination: str
class AddKeyRequest(BaseModel):
key: str
uuid: str
class ImageRequest(BaseModel):
image: str
# ---------------------- Routes ----------------------
@app.get("/", include_in_schema=False)
def redirect_to_odoo():
return RedirectResponse(url="https://ODOO4PROJECTS.com")
import json
from typing import Any, Dict, Optional
from fastapi import Depends, FastAPI
from fastapi.responses import RedirectResponse
from pydantic import BaseModel
app = FastAPI()
# ---------------------- Routes ----------------------
@app.get("/", include_in_schema=False)
def redirect_to_odoo():
return RedirectResponse(url="https://ODOO4PROJECTS.com")
@app.post("/container/update", dependencies=[Depends(verify_api_key)])
def update_container(request: ContainerModel):
# Fetch existing record
existing = execute_db(
"SELECT * FROM containers WHERE UUID = ?", (request.UUID,), fetch=True
)
if not existing:
# If record does not exist, insert a new one with all given fields
execute_db(
"""
INSERT INTO containers (UUID, email, expires, tags, image, history,
comment, domains, status, created, bump, git, backup_slots, hdd, workers,
<<<<<<< HEAD
utm_source, utm_medium, utm_campaign, domains_slots, secret_list, env_list)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
=======
utm_source, utm_medium, utm_campaign, domains_slots, env_list)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
>>>>>>> c583649 (hmm)
""",
(
request.UUID,
request.email,
request.expires,
request.tags,
request.image,
request.history,
request.comment,
request.domains,
request.status,
request.created,
request.bump,
request.git,
request.backup_slots,
request.hdd,
request.workers,
request.utm_source,
request.utm_medium,
request.utm_campaign,
request.domains_slots,
<<<<<<< HEAD
request.secret_list,
=======
>>>>>>> c583649 (hmm)
request.env_list,
),
)
return {"UUID": request.UUID, "status": "created"}
# Existing record found, do partial update
existing = existing[0] # Assuming fetch returns list of dicts
updates = {}
params = []
# Only add fields that are not None in the request
for field in ContainerModel.__fields__:
if field == "UUID":
continue
value = getattr(request, field)
if value is not None:
updates[field] = value
params.append(value)
if updates:
# Build SQL dynamically
set_clause = ", ".join(f"{k}=?" for k in updates.keys())
params.append(request.UUID) # UUID for WHERE clause
execute_db(f"UPDATE containers SET {set_clause} WHERE UUID=?", tuple(params))
return {
"UUID": request.UUID,
"status": "updated",
"fields_updated": list(updates.keys()),
}
return {"UUID": request.UUID, "status": "no_change"}
@app.post("/container/start", dependencies=[Depends(verify_api_key)])
def start_container(request: UUIDRequest):
return {"message": run_command([f"{BIN_PATH}/startContainer", request.UUID])}
@app.post("/container/restartAllContainers", dependencies=[Depends(verify_api_key)])
def start_container(request: ImageRequest):
return {"message": run_command([f"{BIN_PATH}/restartContainers", request.image])}
@app.post("/container/stop", dependencies=[Depends(verify_api_key)])
def stop_container(request: UUIDRequest):
try:
return {"message": run_command([f"{BIN_PATH}/stopContainer", request.UUID])}
except HTTPException:
# Command failed, but continue without error to the API
return {"message": "Container stop command executed (may have failed)"}
@app.post("/container/nuke", dependencies=[Depends(verify_api_key)])
def nuke_container(request: UUIDRequest):
status = execute_db(
"SELECT status FROM containers WHERE UUID=?", (request.UUID,), fetch=True
)
if not status or status[0]["status"] != "nuke":
raise HTTPException(400, "Container status is not 'nuke'")
return {"message": run_command([f"{BIN_PATH}/nukeContainer", request.UUID])}
@app.post("/container/info", dependencies=[Depends(verify_api_key)])
def info_container(request: Optional[UUIDRequest] = None):
# Fields to select
fields = [
"ID",
"UUID",
"email",
"expires",
"tags",
"image",
"history",
"comment",
"domains",
"status",
"created",
"git",
"backup_slots",
"hdd",
"workers",
"utm_source",
"utm_medium",
"utm_campaign",
"domains_slots",
"env_list",
]
field_str = ", ".join(fields)
# Execute query
if request:
rows = execute_db(
f"SELECT {field_str} FROM containers WHERE UUID=?",
(str(request.UUID),),
fetch=True,
)
else:
rows = execute_db(f"SELECT {field_str} FROM containers", fetch=True)
# Map rows to dicts safely
containers = []
for row in rows:
if isinstance(row, dict):
# Already a dict (e.g., some DB wrappers)
containers.append(row)
else:
# Tuple/list -> map with fields
containers.append(dict(zip(fields, row)))
# Wrap in n8n JSON format
n8n_items = [{"json": container} for container in containers]
return n8n_items
@app.post("/container/bump", dependencies=[Depends(verify_api_key)])
def bump_container(request: UUIDRequest):
today = datetime.utcnow().strftime("%Y-%m-%d")
execute_db("UPDATE containers SET bump=? WHERE UUID=?", (today, request.UUID))
msg = run_command([f"{BIN_PATH}/bumpContainer", request.UUID])
return {"message": msg, "bump_date": today}
@app.post("/container/quota", dependencies=[Depends(verify_api_key)])
def container_quota(request: UUIDRequest):
output = run_command(
[
"docker",
"stats",
request.UUID,
"--no-stream",
"--format",
"{{.MemUsage}},{{.BlockIO}}",
]
)
mem_usage, disk_usage = output.split(",")
return {"memory_usage": mem_usage, "disk_io": disk_usage}
# ---------------------- SYSTEM ----------------------
@app.get("/system/containers", dependencies=[Depends(verify_api_key)])
def get_containers():
return Response(
content=run_command([f"{BIN_PATH}/getContainers"]),
media_type="application/json",
)
@app.get("/system/images", dependencies=[Depends(verify_api_key)])
def list_images():
images = run_command(["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"])
return {"images": images.split("\n")}
@app.get("/system/cpu", dependencies=[Depends(verify_api_key)])
def get_cpu_log():
CPU_LOG_PATH = Path("/4server/data/log/cpu.log")
if not CPU_LOG_PATH.exists():
raise HTTPException(status_code=404, detail="CPU log file not found")
try:
with CPU_LOG_PATH.open("r") as f:
content = f.read()
return PlainTextResponse(content)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error reading CPU log: {e}")
@app.get("/system/info", dependencies=[Depends(verify_api_key)])
def get_system_info():
try:
alpine_version = None
last_update = None
bump_dates = execute_db(
"SELECT MAX(bump) AS latest_bump FROM containers", fetch=True
)[0]["latest_bump"]
if os.path.exists("/4server/data/update"):
with open("/4server/data/update") as f:
last_update = f.read().strip()
if os.path.exists("/etc/alpine-release"):
with open("/etc/alpine-release") as f:
alpine_version = f.read().strip()
mem = psutil.virtual_memory()
disk = psutil.disk_usage("/")
cpu_count = psutil.cpu_count(logical=True)
return {
"alpine_version": alpine_version,
"last_update": last_update,
"latest_bump": bump_dates,
"version": VERSION,
"resources": {
"memory": {
"total": mem.total,
"available": mem.available,
"used": mem.used,
},
"disk": {"total": disk.total, "used": disk.used, "free": disk.free},
"cpu_count": cpu_count,
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/system/pull", dependencies=[Depends(verify_api_key)])
def pull_all_images():
return {"message": run_command([f"{BIN_PATH}/pullAllContainers"])}
@app.post("/system/restartAllContainers", dependencies=[Depends(verify_api_key)])
def restart_all_containers(request: ImageRequest):
if not request.image:
raise HTTPException(status_code=400, detail="Image name is required")
return {"message": run_command([f"{BIN_PATH}/restartContainers", request.image])}
@app.post("/client/git", dependencies=[Depends(verify_api_key)])
def git_tool(request: CommandRequest):
if request.method == 1:
command = [f"{BIN_PATH}/gitPull", request.uuid]
elif request.method == 2:
command = [f"{BIN_PATH}/gitRevert", request.uuid]
else:
raise HTTPException(status_code=400, detail="Invalid method")
output = run_command(command)
return {"message": output}
@app.post("/client/addGitKey", dependencies=[Depends(verify_api_key)])
def add_git_key(request: AddKeyRequest):
if not request.key or not request.uuid:
raise HTTPException(status_code=400, detail="Key and uuid are required")
file_path = f"/4server/data/{request.uuid}/git-server/keys/id_rsa.pub"
try:
# Ensure the directory exists
os.makedirs(os.path.dirname(file_path), exist_ok=True)
# Write the key to the file
with open(file_path, "w", encoding="utf-8") as f:
f.write(request.key)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to save key: {str(e)}")
return {"message": f"Key saved for container {request.uuid}", "file": file_path}
@app.get("/client/audit/{uuid}", dependencies=[Depends(verify_api_key)])
def audit_odoo(uuid: str):
return {"message": run_command([f"{BIN_PATH}/ODOO_19/listModules", uuid])}
@app.get("/client/logs/{uuid}", dependencies=[Depends(verify_api_key)])
async def get_odoo_log_summary(uuid: str):
if not re.fullmatch(r"[0-9a-fA-F\-]+", uuid):
raise HTTPException(
status_code=400,
detail="Invalid UUID format. Only numbers, letters a-f, and '-' are allowed.",
)
BASE_LOG_DIR = "/4server/data"
# Build file paths as strings
project_dir = os.path.join(BASE_LOG_DIR, uuid)
odoo_log_file = os.path.join(project_dir, "logs", "odoo.log")
git_log_file = os.path.join(project_dir, "logs", "git.log")
if not os.path.isfile(odoo_log_file):
raise HTTPException(status_code=404, detail="Odoo log file not found")
# --- Helper variables and functions ---
IMPORTANT_PATTERNS = [
re.compile(r"odoo\.addons\."), # Any Odoo addon log
re.compile(r"Job '.*' starting"), # Cron job start
re.compile(r"Job '.*' fully done"), # Cron job end
re.compile(r"ERROR"), # Errors
re.compile(r"WARNING"), # Warnings
re.compile(r"Traceback"), # Tracebacks
re.compile(r"Error"),
]
def is_important_line(line: str) -> bool:
return any(p.search(line) for p in IMPORTANT_PATTERNS)
def read_last_lines(file_path: str, max_lines: int) -> list[str]:
"""
Read the last `max_lines` from the file efficiently.
"""
if not os.path.isfile(file_path):
return []
last_lines = deque(maxlen=max_lines)
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
last_lines.append(line.strip())
return list(last_lines)
# --- Main logic ---
try:
# Last 500 lines from Odoo log
last_500_lines = read_last_lines(odoo_log_file, 500)
important_odoo_lines = [
line for line in last_500_lines if is_important_line(line)
]
# Last 50 lines from git.log
last_50_git_lines = read_last_lines(git_log_file, 50)
return {
"uuid": str(uuid),
"important_odoo_log_lines": important_odoo_lines,
"last_git_log_lines": last_50_git_lines,
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error reading log files: {e}")
# ------------------------ BACKUP HANDLING -------------------------------------
@app.post("/backup/import", dependencies=[Depends(verify_api_key)])
def backup_import(request: ImportRequest):
if not request.filename:
raise HTTPException(status_code=400, detail="Filename is required")
command = [f"{BIN_PATH}/ODOO_19/import", request.filename]
output = run_command(command)
return {"message": output}
@app.post("/backup/move", dependencies=[Depends(verify_api_key)])
def backup_move(request: MoveRequest):
if not request.source or not request.destination:
raise HTTPException(
status_code=400, detail="Source and destination are required"
)
if not os.path.exists(request.source):
raise HTTPException(status_code=404, detail="Source file does not exist")
# Use shell command to move the file
command = ["mv", request.source, request.destination] # Linux/macOS
# For Windows, use: command = ["move", request.source, request.destination]
output = run_command(command)
return {
"message": f"Moved {request.source} to {request.destination}",
"output": output,
}
@app.get("/backup/borgpush", dependencies=[Depends(verify_api_key)])
def backup_borgpush():
return {"message": run_command(["doas", "-u", "4server", f"{BIN_PATH}/borgpush"])}
# ---------------------- Entry Point ----------------------
if __name__ == "__main__":
print(VERSION)
init_db()
time.sleep(25)
uvicorn.run(app, host="10.5.0.1", port=8888)
-25
View File
@@ -36,34 +36,9 @@ def read_file(path):
return None
def read_env_file(path, keys):
"""Read KEY=VALUE lines from path and return the requested keys."""
result = {key: None for key in keys}
try:
with open(path) as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
if line.startswith("export "):
line = line[len("export "):].lstrip()
key, value = line.split("=", 1)
key = key.strip()
if key in result:
result[key] = value.strip().strip("\"'")
except (FileNotFoundError, PermissionError):
pass
return result
def main():
audit = {}
# Contract info from /etc/4server
audit.update(
read_env_file("/etc/4server", ["PROVIDER", "COST", "CONTRACT_END"])
)
# Container identity
audit["container_id"] = os.environ.get("HOSTNAME")
+2 -2
View File
@@ -4,10 +4,10 @@ set -euo pipefail # Fail on error, undefined variables, and pipe errors
# Create backup filename
FILENAME="$(date +"%Y%m%d_%H%M").zip"
BACKUP_DIR="/BACKUP/$UUID"
BACKUP_DIR="/BACKUP/$CONTAINER_ID"
mkdir -p "$BACKUP_DIR"
tar -czvf "$BACKUP_DIR/$FILENAME" -C "/4server/data" "$UUID"
tar -czvf "$BACKUP_DIR/$FILENAME" -C "/4server/data" "$CONTAINER_ID"
# Remove old backups beyond the configured slots
ls -t "$BACKUP_DIR"/[0-9]*.zip 2>/dev/null | tail -n +$((BACKUP_SLOTS + 1)) | while read -r file; do
+18 -12
View File
@@ -4,21 +4,27 @@
set -euo pipefail # Fail on error, undefined variables, and pipe errors
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
# Load helper functions (provides log/warn/die)
_self="$(realpath "${BASH_SOURCE[0]:-$0}")"; SBIN_ROOT="$(dirname "$(dirname "$_self")")" # parent of backup/
source "$SBIN_ROOT/helpers"
# Load helper functions
source /4server/sbin/helpers
# Load SOFTWARE and DB_NAME from container.env
load_container_config "$CONTAINER_ID"
# Get contract info (also exports ODOO_DB_PASSWORD via helpers)
get_contract_info
log "UUID: $UUID"
# DB_NAME is the postgres database name (may differ from CONTAINER_ID for migrated containers)
DB_NAME="${DB_NAME:-$CONTAINER_ID}"
log "CONTAINER_ID: $CONTAINER_ID"
log "DB_NAME: $DB_NAME"
log "Retention: daily 14d, weekly 12w"
log "DB host: $POSTGRES_HOST"
# Build paths
FILENAME="$(date +"%Y%m%d_%H%M").zip"
BACKUP_DIR="/BACKUP/$UUID"
BACKUP_DIR="/BACKUP/$CONTAINER_ID"
BB_DIR="$BACKUP_DIR/bb"
# Ensure backup directories exist (requires elevated permissions like other ops)
@@ -27,12 +33,12 @@ doas mkdir -p "$BB_DIR"
# Perform database dump inside the container
log "Starting dump -> $BACKUP_DIR/$FILENAME"
doas docker exec "$UUID" odoo db \
doas docker exec "$CONTAINER_ID" odoo db \
--db_host "$POSTGRES_HOST" \
-r "$UUID" \
-r "$DB_NAME" \
-w "$ODOO_DB_PASSWORD" \
--data-dir /home/odoo/.local/share/Odoo/ \
dump "$UUID" "/mnt/backup/$FILENAME"
dump "$DB_NAME" "/mnt/backup/$FILENAME"
log "Dump complete: $(doas du -sh "$BACKUP_DIR/$FILENAME" | cut -f1)"
@@ -40,7 +46,7 @@ log "Dump complete: $(doas du -sh "$BACKUP_DIR/$FILENAME" | cut -f1)"
doas chmod 666 "$BACKUP_DIR"/[0-9]*.zip
# Hand ownership back to odoo inside the container
doas docker exec "$UUID" chown odoo:odoo -R /mnt/backup
doas docker exec "$CONTAINER_ID" chown odoo:odoo -R /mnt/backup
# Update the current.zip symlink inside bb/ pointing at the backup just created.
# Target is relative (../$FILENAME) because the symlink lives one level deeper
@@ -97,10 +103,10 @@ done
log "Backup finished successfully ."
# --- Custom modules snapshot ---
# Archive /4server/data/$UUID/git-server/repos/local/custom/ into .current.tar.gz.
# Archive /4server/data/$CONTAINER_ID/git-server/repos/local/custom/ into .current.tar.gz.
# Rebuilt only when at least one file inside is newer than the existing archive,
# so borg backup dedup sees an identical blob on unchanged runs.
CUSTOM_SRC="/4server/data/$UUID/git-server/repos/local/custom"
CUSTOM_SRC="/4server/data/$CONTAINER_ID/git-server/repos/local/custom"
CUSTOM_ARCHIVE="$BB_DIR/current_modules.tar.gz"
if [ ! -d "$CUSTOM_SRC" ]; then
@@ -110,7 +116,7 @@ elif [ ! -f "$CUSTOM_ARCHIVE" ] || \
log "Changes detected in custom modules — rebuilding current.tar.gz"
# Write to a temp file first; mv is atomic, so borg never sees a partial archive
doas tar -czf "${CUSTOM_ARCHIVE}.tmp" \
-C "/4server/data/$UUID/git-server/repos/local" custom
-C "/4server/data/$CONTAINER_ID/git-server/repos/local" custom
doas mv "${CUSTOM_ARCHIVE}.tmp" "$CUSTOM_ARCHIVE"
log ".current.tar.gz updated: $(doas du -sh "$CUSTOM_ARCHIVE" | cut -f1)"
else
+9 -7
View File
@@ -1,18 +1,20 @@
#!/bin/bash
exec >> /4server/data/log/backupAll.log 2>&1
echo "$(date '+%Y-%m-%d %H:%M') Starting backupAll"
_self="$(realpath "${BASH_SOURCE[0]:-$0}")"; SBIN="$(dirname "$_self")"
source "$SBIN/helpers"
log "Starting backupAll"
BIN_PATH="/4server/sbin"
BIN_PATH="$SBIN"
containers=$(docker ps --format '{{.Names}}' | grep -E '^[0-9]{3}-[0-9]{3}')
containers=$(docker ps --format '{{.Names}}' | grep -v '^traefik$' | grep -v '^beedb$')
if [[ -z "$containers" ]]; then
echo "No containers matching pattern NNN-NNN found."
if [ -z "$containers" ]; then
log "No managed container_ids found."
exit 0
fi
echo "Found containers:"
log "Triggering backup for:"
while IFS= read -r container; do
echo " $container"
log " $container"
touch "/4server/data/$container/cc/backup"
done <<< "$containers"
+13 -29
View File
@@ -1,45 +1,29 @@
#!/bin/bash
exec >> /4server/data/log/backupContainer.log 2>&1
echo "$(date '+%Y-%m-%d %H:%M') Backup container $1"
source /4server/sbin/helpers
_self="$(realpath "${BASH_SOURCE[0]:-$0}")"; SBIN="$(dirname "$_self")"
source "$SBIN/helpers"
BIN_PATH="/4server/sbin"
BIN_PATH="$SBIN"
UUID="$1"
CONTAINER_ID="$1"
if [[ -z "$UUID" ]]; then
echo "Usage: $0 <uuid>"
exit 1
fi
[ -z "$CONTAINER_ID" ] && die "Usage: $0 <uuid>"
load_container_config "$CONTAINER_ID"
get_contract_info
# Extract the second part of UUID (split by "-")
SECOND_PART=$(echo "$UUID" | cut -d'-' -f2)
log "Backup container $CONTAINER_ID (SOFTWARE=$SOFTWARE)"
echo "Conatiner Type $UUID"
# Decide which script to run
case "$SECOND_PART" in
001)
# Decide which script to run based on SOFTWARE from container.env
case "$SOFTWARE" in
n8n)
"$BIN_PATH/backup/N8N"
;;
002)
ODOO_17|ODOO_18|ODOO_19)
"$BIN_PATH/backup/ODOO"
;;
003)
"$BIN_PATH/backup/ODOO"
;;
004)
"$BIN_PATH/backup/ODOO"
;;
005)
"$BIN_PATH/backup/ODOO"
;;
*)
echo "Unknown UUID type: $SECOND_PART"
exit 2
*)
die "Unknown SOFTWARE type '${SOFTWARE}' for container_id $CONTAINER_ID — run: initContainer $CONTAINER_ID <n8n|ODOO_17|ODOO_18|ODOO_19>"
;;
esac
+27 -91
View File
@@ -1,108 +1,44 @@
#!/usr/bin/env python3
"""Check the latest backup date.
Usage:
checkBackup <uuid> print latest backup date for one container
checkBackup -a check all containers returned by activeContainers;
status is OK if the latest backup is at most 2 days
old, NOK if older or missing
"""
import subprocess
import sys
from datetime import date, datetime
from pathlib import Path
BACKUP_ROOT = Path("/BACKUP")
MAX_AGE_DAYS = 2
ACTIVE_CONTAINERS = Path(__file__).resolve().parent / "activeContainers"
if len(sys.argv) < 2:
print("Usage: checkBackup <container_id>")
sys.exit(1)
def check_backup(uuid):
"""Return (ok, message) for the given UUID."""
directory = BACKUP_ROOT / uuid
container_id = sys.argv[1]
directory = BACKUP_ROOT / container_id
if not directory.is_dir():
return False, f"ERROR: directory not found for UUID {uuid}"
if not directory.is_dir():
print(f"ERROR: directory not found for container_id {container_id}")
sys.exit(1)
latest_date = None
latest_file = None
latest_date = None
latest_file = None
for zip_file in directory.rglob("*.zip"):
date_part = zip_file.name.split("_")[0]
for zip_file in directory.rglob("*.zip"):
name = zip_file.name
try:
date_part = name.split("_")[0]
if len(date_part) != 8 or not date_part.isdigit():
continue
except Exception:
continue
if latest_date is None or date_part > latest_date:
latest_date = date_part
latest_file = zip_file
if latest_date is None or date_part > latest_date:
latest_date = date_part
latest_file = zip_file
if latest_date is None:
return False, "NO BACKUPS"
if latest_date is None:
print("NO BACKUPS")
sys.exit(1)
if latest_file.stat().st_size < 1000:
return True, "01-01-2000"
if latest_file.stat().st_size < 1000:
print("01-01-2000")
sys.exit(0)
# Format as yyyy-mm-dd
return True, f"{latest_date[:4]}-{latest_date[4:6]}-{latest_date[6:8]}"
def is_recent(ok, message):
"""True if the backup date in message is at most MAX_AGE_DAYS old."""
if not ok:
return False
try:
backup_date = datetime.strptime(message, "%Y-%m-%d").date()
except ValueError:
# e.g. the "01-01-2000" placeholder for too-small backup files
return False
return (date.today() - backup_date).days <= MAX_AGE_DAYS
def active_uuids():
"""UUIDs printed by activeContainers (skips header and separator lines)."""
result = subprocess.run(
[str(ACTIVE_CONTAINERS)], capture_output=True, text=True
)
if result.returncode != 0:
print(
f"ERROR: activeContainers failed: {result.stderr.strip()}",
file=sys.stderr,
)
sys.exit(1)
lines = result.stdout.splitlines()
uuids = []
for line in lines[2:]: # line 0 = header, line 1 = "----" separator
fields = line.split()
if fields:
uuids.append(fields[0])
return uuids
def main():
if len(sys.argv) < 2:
print("Usage: checkBackup <uuid> | -a")
sys.exit(1)
if sys.argv[1] == "-a":
rows = [("uuid", "latest", "status")]
failed = False
for uuid in active_uuids():
ok, message = check_backup(uuid)
recent = is_recent(ok, message)
rows.append((uuid, message, "OK" if recent else "NOK"))
failed = failed or not recent
widths = [max(len(row[i]) for row in rows) for i in range(2)]
for uuid, message, status in rows:
print(f"{uuid:<{widths[0]}} {message:<{widths[1]}} {status}")
sys.exit(1 if failed else 0)
ok, message = check_backup(sys.argv[1])
print(message)
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()
# Format as yyyy-mm-dd
print(f"{latest_date[:4]}-{latest_date[4:6]}-{latest_date[6:8]}")
+16 -9
View File
@@ -1,36 +1,43 @@
#!/bin/bash
_self="$(realpath "${BASH_SOURCE[0]:-$0}")"; SBIN="$(dirname "$_self")"
source "$SBIN/helpers"
cd /4server/data/
while :
do
for dir in ???-???-*; do
# nullglob: if no subdirectories exist the loop body is skipped entirely
shopt -s nullglob
for dir in */; do
dir="${dir%/}"
[ -z "$dir" ] && continue
if [ -d "${dir}/cc" ]; then
if [ -f "${dir}/cc/backup" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') BACKUP for: ${dir%/}"
/4server/sbin/backupContainer ${dir%/} 2
log "BACKUP for: $dir"
"$SBIN/backupContainer" "$dir"
rm "${dir}/cc/backup"
fi
if [ -f "${dir}/cc/restart" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') Restart for: ${dir%/}"
/4server/sbin/startContainer ${dir%/}
log "Restart for: $dir"
"$SBIN/startContainer" "$dir"
rm "${dir}/cc/restart"
fi
if [ -f "${dir}/cc/restore" ]; then
FILENAME=$(head -n 1 "${dir}/cc/restore")
echo "$(date '+%Y-%m-%d %H:%M:%S') Restore for: ${dir%/} - $FILENAME"
/4server/sbin/ODOO_19/restore ${dir%/} $FILENAME
log "Restore for: $dir - $FILENAME"
"$SBIN/odoo/restore" "$dir" "$FILENAME"
rm "${dir}/cc/restore"
fi
fi
if [ -d "${dir}/n8n" ]; then
if [ -f "${dir}/n8n/database.sqlite-journal" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') Restart N8N (journal) for: ${dir%/}"
/4server/sbin/startContainer ${dir%/}
log "Restart N8N (journal) for: $dir"
"$SBIN/startContainer" "$dir"
fi
fi
+16 -16
View File
@@ -1,23 +1,23 @@
#!/bin/bash
# checkFilestore <UUID> — compare container filestore vs host disk via md5
# checkFilestore <CONTAINER_ID> — compare container filestore vs host disk via md5
check_one() {
local UUID="$1"
local CONTAINER_ID="$1"
FILESTORE_HOST="/4server/data/$UUID/odoo/odoo-web-data/filestore/$UUID"
FILESTORE_HOST="/4server/data/$CONTAINER_ID/odoo/odoo-web-data/filestore/$CONTAINER_ID"
[[ -d "$FILESTORE_HOST" ]] || { echo "ERROR: host filestore not found: $FILESTORE_HOST"; exit 2; }
doas docker ps --format "{{.Names}}" | grep -q "^${UUID}$" \
|| { echo "ERROR: container $UUID is not running"; exit 2; }
doas docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_ID}$" \
|| { echo "ERROR: container $CONTAINER_ID is not running"; exit 2; }
# Read data_dir from odoo.conf inside the container, fall back to default
DATA_DIR=$(doas docker exec "$UUID" sh -c \
DATA_DIR=$(doas docker exec "$CONTAINER_ID" sh -c \
'for f in /etc/odoo.conf /etc/odoo/odoo.conf /opt/odoo/etc/odoo.conf; do
grep -E "^data_dir" "$f" 2>/dev/null | cut -d= -f2- | xargs && break
done')
DATA_DIR="${DATA_DIR:-/home/odoo/.local/share/Odoo}"
FILESTORE_CONTAINER="$DATA_DIR/filestore/$UUID"
MOUNT_HOST=$(doas docker inspect "$UUID" --format '{{json .Mounts}}' \
FILESTORE_CONTAINER="$DATA_DIR/filestore/$CONTAINER_ID"
MOUNT_HOST=$(doas docker inspect "$CONTAINER_ID" --format '{{json .Mounts}}' \
| jq -r --arg dest "$DATA_DIR" '.[] | select(.Type=="bind") | select(.Destination==$dest) | .Source')
echo "ODOO USES : $FILESTORE_CONTAINER"
echo "Docker maps to : $DATA_DIR"
@@ -25,7 +25,7 @@ echo ""
echo "Docker mapped to : ${MOUNT_HOST:-<no matching mount found>}"
echo "Hashing container filestore..."
MD5_CONTAINER=$(doas docker exec "$UUID" sh -c \
MD5_CONTAINER=$(doas docker exec "$CONTAINER_ID" sh -c \
"find $FILESTORE_CONTAINER -type f | sort | xargs md5sum 2>/dev/null | awk '{print \$1}'" | md5sum | cut -d' ' -f1)
echo "Hashing host filestore..."
@@ -41,18 +41,18 @@ if [[ -n "${1-}" ]]; then
check_one "$1"
else
for dir in /4server/data/00*/; do
UUID=$(basename "$dir")
result=$(check_one "$UUID" 2>&1)
CONTAINER_ID=$(basename "$dir")
result=$(check_one "$CONTAINER_ID" 2>&1)
if echo "$result" | grep -q "OK: filestores match"; then
echo "$UUID OK"
echo "$CONTAINER_ID OK"
elif echo "$result" | grep -q "not running"; then
echo "$UUID NOT RUNNING"
echo "$CONTAINER_ID NOT RUNNING"
elif echo "$result" | grep -q "host filestore not found"; then
echo "$UUID NO FILESTORE"
echo "$CONTAINER_ID NO FILESTORE"
elif echo "$result" | grep -q "cannot query DB"; then
echo "$UUID DB ERROR"
echo "$CONTAINER_ID DB ERROR"
else
echo "$UUID MISMATCH"
echo "$CONTAINER_ID MISMATCH"
fi
done
fi
-23
View File
@@ -1,23 +0,0 @@
#!/bin/bash
# checkFilestoreAll — check the filestore of every active container.
# Runs checkFilestore for each UUID returned by activeContainers.
# Output: "<UUID> OK" or "<UUID> NOK" per container.
SBIN_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
active_uuids() {
"$SBIN_DIR/activeContainers" | awk 'NF && $1 != "uuid" && $1 !~ /^-/ {print $1}'
}
failed=0
while read -r UUID; do
[[ -n "$UUID" ]] || continue
if "$SBIN_DIR/checkFilestore" "$UUID" >/dev/null 2>&1; then
echo "$UUID OK"
else
echo "$UUID NOK"
failed=1
fi
done < <(active_uuids)
exit "$failed"
+5 -4
View File
@@ -1,13 +1,14 @@
export PATH=/4PROJECTS/bin:$PATH
if [ ! -n "$1" ]; then
echo "Missing Parameters <UUID>"
echo "Missing Parameters <CONTAINER_ID>"
exit 0
fi
UUID=$1
echo "UUID: $UUID"
CONTAINER_ID=$1
echo "CONTAINER_ID: $CONTAINER_ID"
source /4server/sbin/helpers
_self="$(realpath "${BASH_SOURCE[0]:-$0}")"; SBIN="$(dirname "$_self")"
source "$SBIN/helpers"
get_contract_info
+55 -26
View File
@@ -1,5 +1,48 @@
#!/bin/bash
# ---------------------------------------------------------------------------
# Logging — use these everywhere instead of bare echo
# ---------------------------------------------------------------------------
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] $*"; }
warn() { echo "$(date '+%Y-%m-%d %H:%M:%S') [WARN] $*" >&2; }
die() { echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] $*" >&2; exit 1; }
# Resolve the absolute directory of a script.
# Always pass ${BASH_SOURCE[0]} — it holds the real path even when the
# script is invoked via PATH (unlike $0 which may be just a bare name).
# Usage in top-level scripts: SBIN="$(sbin_dir "${BASH_SOURCE[0]}")"
# Sub-scripts one level deep: SBIN_ROOT="$(dirname "$(sbin_dir "${BASH_SOURCE[0]}")")"
sbin_dir() { dirname "$(realpath "$1")"; }
# ---------------------------------------------------------------------------
# load_container_config <container_id>
# Sources /4server/data/<container_id>/config/container.env and exports:
# SOFTWARE — n8n | ODOO_17 | ODOO_18 | ODOO_19
# DB_NAME — postgres database/user (may differ from CONTAINER_ID for migrations)
# ---------------------------------------------------------------------------
load_container_config() {
local container_id="${1:-$CONTAINER_ID}"
local cfg="/4server/data/$container_id/config/container.env"
if [ -f "$cfg" ]; then
while IFS= read -r line; do
case "$line" in
''|'#'*) continue ;;
esac
local _k="${line%%=*}"
local _v="${line#*=}"
case "$_k" in
*[!A-Z_]*) continue ;;
esac
export "$_k=$_v"
done < "$cfg"
else
warn "no container.env found at $cfg"
fi
}
# ---------------------------------------------------------------------------
# Postgres defaults (overridable via environment)
# ---------------------------------------------------------------------------
POSTGRES_HOST="${POSTGRES_HOST:-beedb}"
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
POSTGRES_ADMIN_USER="${POSTGRES_ADMIN_USER:-1gtT0sf8klB9lDbYZD9}"
@@ -7,19 +50,19 @@ POSTGRES_ADMIN_PASSWORD="${POSTGRES_ADMIN_PASSWORD:-ZpSwWNafyy9GhY2gzHw}"
DB_PATH="/4server/data/contracts.db"
# ---------------------------------------------------------------------------
# get_contract_info
# Reads the contracts.db row for $CONTAINER_ID and exports all columns as env vars.
# Also parses SECRET_LIST and exports ODOO_DB_PASSWORD from the psql key.
# ---------------------------------------------------------------------------
get_contract_info() {
log "get_contract_info $CONTAINER_ID"
echo "$(date '+%Y-%m-%d %H:%M:%S') Loading contract info for $UUID from $DB_PATH"
# Single CTE: the table is scanned once and all projections come from it
while IFS="=" read -r key value; do
if [ -n "$key" ]; then
export "$key=$value"
fi
[ -n "$key" ] && export "$key=$value"
done < <(sqlite3 "$DB_PATH" "
WITH c AS (SELECT * FROM containers WHERE UUID='$UUID' LIMIT 1)
SELECT 'UUID=' || COALESCE(UUID, '') FROM c UNION ALL
WITH c AS (SELECT * FROM containers WHERE UUID='$CONTAINER_ID' LIMIT 1)
SELECT 'CONTAINER_ID=' || COALESCE(UUID, '') FROM c UNION ALL
SELECT 'EXPIRES=' || COALESCE(expires, '') FROM c UNION ALL
SELECT 'IMAGE=' || COALESCE(image, '') FROM c UNION ALL
SELECT 'STATUS=' || COALESCE(status, '') FROM c UNION ALL
@@ -33,28 +76,14 @@ get_contract_info() {
SELECT 'DOMAINS_SLOTS=' || COALESCE(domains_slots, '') FROM c;
")
# Parse SECRET_LIST: backslash-separated key=value pairs (e.g. worex1=1\pswl=22)
# - printf avoids echo interpreting escape sequences
# - tr -d '\r\n' strips any embedded newlines the DB value may contain
# - tr '\\' '\n' turns each backslash separator into a real newline
# - %%=* / #*= split on the FIRST = only, so base64 values like tok=ab== are safe
# Parse SECRET_LIST: backslash-separated key=value pairs
# %%=* / #*= split on the FIRST = only, so base64 values like tok=ab== are safe
if [ -n "$SECRET_LIST" ]; then
while IFS= read -r pair || [ -n "$pair" ]; do
_key="${pair%%=*}"
_value="${pair#*=}"
if [ -n "$_key" ]; then
export "$_key=$_value"
fi
[ -n "$_key" ] && export "$_key=$_value"
done < <(printf '%s' "$SECRET_LIST" | tr -d '\r\n' | tr '\\' '\n')
fi
export ODOO_DB_PASSWORD=$psql
if [ -z "$CONTAINERDBID" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') WARNING: no contract found for $UUID in $DB_PATH"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') Loaded contract for $UUID: DBID=$CONTAINERDBID IMAGE=$IMAGE STATUS=$STATUS CREATED=$CREATED EXPIRES=$EXPIRES"
fi
if [ -z "$ODOO_DB_PASSWORD" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') WARNING: no psql secret found for $UUID, ODOO_DB_PASSWORD is empty"
fi
}
-87
View File
@@ -1,87 +0,0 @@
#!/bin/bash
POSTGRES_HOST="${POSTGRES_HOST:-beedb}"
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
POSTGRES_ADMIN_USER="${POSTGRES_ADMIN_USER:-1gtT0sf8klB9lDbYZD9}"
POSTGRES_ADMIN_PASSWORD="${POSTGRES_ADMIN_PASSWORD:-ZpSwWNafyy9GhY2gzHw}"
DB_PATH="/4server/data/contracts.db"
get_contract_info() {
echo "get_contract_info $UUID"
# Single CTE: the table is scanned once and all projections come from it
while IFS="=" read -r key value; do
if [ -n "$key" ]; then
export "$key=$value"
fi
done < <(sqlite3 "$DB_PATH" "
WITH c AS (SELECT * FROM containers WHERE UUID='$UUID' LIMIT 1)
SELECT 'UUID=' || COALESCE(UUID, '') FROM c UNION ALL
SELECT 'EXPIRES=' || COALESCE(expires, '') FROM c UNION ALL
SELECT 'IMAGE=' || COALESCE(image, '') FROM c UNION ALL
SELECT 'STATUS=' || COALESCE(status, '') FROM c UNION ALL
SELECT 'CREATED=' || COALESCE(created, '') FROM c UNION ALL
SELECT 'SECRET_LIST=' || COALESCE(secret_list, '') FROM c UNION ALL
SELECT 'CONTAINERDBID=' || COALESCE(id, '') FROM c UNION ALL
SELECT 'GIT=' || COALESCE(git, '') FROM c UNION ALL
SELECT 'BACKUP_SLOTS=' || COALESCE(backup_slots, '') FROM c UNION ALL
SELECT 'HDD=' || COALESCE(hdd, '') FROM c UNION ALL
SELECT 'WORKERS=' || COALESCE(workers, '') FROM c UNION ALL
SELECT 'DOMAINS_SLOTS=' || COALESCE(domains_slots, '') FROM c;
")
# Parse SECRET_LIST: backslash-separated key=value pairs (e.g. worex1=1\pswl=22)
# - printf avoids echo interpreting escape sequences
# - tr -d '\r\n' strips any embedded newlines the DB value may contain
# - tr '\\' '\n' turns each backslash separator into a real newline
# - %%=* / #*= split on the FIRST = only, so base64 values like tok=ab== are safe
if [ -n "$SECRET_LIST" ]; then
while IFS= read -r pair || [ -n "$pair" ]; do
_key="${pair%%=*}"
_value="${pair#*=}"
if [ -n "$_key" ]; then
export "$_key=$_value"
fi
done < <(printf '%s' "$SECRET_LIST" | tr -d '\r\n' | tr '\\' '\n')
fi
<<<<<<< HEAD
export ODOO_DB_PASSWORD=$psql
=======
done < <(sqlite3 "$DB_PATH" "
SELECT 'UUID=' || UUID FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'EMAIL=' || email FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'EXPIRES=' || expires FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'TAGS=' || replace(replace(tags, char(10), ''), char(13), '') FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'ENV=' || replace(replace(env, char(10), ''), char(13), '') FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'IMAGE=' || image FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'DOMAINS=' || domains FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'STATUS=' || status FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'CREATED=' || created FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'SECRET_LIST=' || COALESCE(secret_list, '') FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'CONTAINERDBID=' || id FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'BUMP=' || bump FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'GIT=' || COALESCE(git, '') FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'BACKUP_SLOTS=' || COALESCE(backup_slots, '') FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'HDD=' || COALESCE(hdd, '') FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'WORKERS=' || COALESCE(workers, '') FROM containers WHERE UUID='$UUID'
UNION ALL SELECT 'DOMAINS_SLOTS=' || COALESCE(domains_slots, '') FROM containers WHERE UUID='$UUID';
")
# Parse ENV JSON into individual env vars
eval $(echo "$ENV" | jq -r 'to_entries | .[] | "export \(.key | ascii_upcase)=\(.value)"')
# Parse SECRET_LIST (backslash-separated key=value pairs, e.g. worex1=1\pswl=22)
if [ -n "$SECRET_LIST" ]; then
while IFS='=' read -r key value; do
if [ -n "$key" ]; then
export "$key=$value"
fi
done < <(echo "$SECRET_LIST" | tr '\\' '\n')
fi
>>>>>>> c583649 (hmm)
}
+49
View File
@@ -0,0 +1,49 @@
#!/bin/bash
# Usage: initContainer <container_id> <software> [db_name]
#
# Creates /4server/data/<container_id>/config/container.env
#
# <container_id> : new codename (e.g. "banana")
# <software> : n8n | ODOO_17 | ODOO_18 | ODOO_19
# [db_name] : optional — postgres DB/user name to use.
# Defaults to <container_id>.
# Pass the OLD container_id here when migrating an existing container
# so the existing postgres database is reused.
#
# Examples:
# New container:
# initContainer banana ODOO_19
#
# Migrating existing container (old container_id kept as DB name):
# initContainer banana ODOO_19 005-003-91f3922c-8cc2-4950-aa57-808775c341ff
CONTAINER_ID="$1"
SOFTWARE="$2"
DB_NAME="${3:-$1}"
if [[ -z "$CONTAINER_ID" || -z "$SOFTWARE" ]]; then
echo "Usage: $0 <container_id> <software> [db_name]"
echo " software: n8n | ODOO_17 | ODOO_18 | ODOO_19"
exit 1
fi
case "$SOFTWARE" in
n8n|ODOO_17|ODOO_18|ODOO_19) ;;
*)
echo "ERROR: Unknown software type: $SOFTWARE"
echo " Valid values: n8n | ODOO_17 | ODOO_18 | ODOO_19"
exit 1
;;
esac
CONFIG_DIR="/4server/data/$CONTAINER_ID/config"
mkdir -p "$CONFIG_DIR"
cat > "$CONFIG_DIR/container.env" <<EOF
SOFTWARE=$SOFTWARE
DB_NAME=$DB_NAME
EOF
echo "Created $CONFIG_DIR/container.env"
echo " SOFTWARE=$SOFTWARE"
echo " DB_NAME=$DB_NAME"
+122 -12
View File
@@ -1,16 +1,126 @@
#!/bin/bash
# Migration script — run once per server after deploying updated code.
# Safe to re-run: each step checks whether it is needed before acting.
sqlite3 /4server/data/contracts.db <<EOF
DB="/4server/data/contracts.db"
# -----------------------------------------------------------------------
# Helper: check whether a column exists
# -----------------------------------------------------------------------
has_column() {
sqlite3 "$DB" "SELECT COUNT(*) FROM pragma_table_info('containers') WHERE name='$1';"
}
# -----------------------------------------------------------------------
# Step 1 — Add columns introduced over time (silently skip if present)
# -----------------------------------------------------------------------
echo "Step 1: adding missing columns..."
sqlite3 "$DB" 2>/dev/null <<'EOF' || true
ALTER TABLE containers ADD COLUMN env_list TEXT;
UPDATE containers
SET env_list = (
SELECT group_concat(key || '=' || value, ', ')
FROM json_each(containers.env)
);
ALTER TABLE containers ADD COLUMN secret_list TEXT;
UPDATE containers
SET secret_list = (
SELECT group_concat(key || '=' || value, ', ')
FROM json_each(containers.secret)
);
EOF
sqlite3 "$DB" 2>/dev/null <<'EOF' || true
ALTER TABLE containers ADD COLUMN secret_list TEXT;
EOF
sqlite3 "$DB" 2>/dev/null <<'EOF' || true
ALTER TABLE containers ADD COLUMN utm_source text;
EOF
sqlite3 "$DB" 2>/dev/null <<'EOF' || true
ALTER TABLE containers ADD COLUMN utm_medium text;
EOF
sqlite3 "$DB" 2>/dev/null <<'EOF' || true
ALTER TABLE containers ADD COLUMN utm_campaign text;
EOF
sqlite3 "$DB" 2>/dev/null <<'EOF' || true
ALTER TABLE containers ADD COLUMN container_id CHAR(50);
EOF
# -----------------------------------------------------------------------
# Step 2 — Backfill container_id from UUID
# -----------------------------------------------------------------------
echo "Step 2: backfilling container_id from UUID..."
sqlite3 "$DB" <<'EOF'
UPDATE containers
SET container_id = UUID
WHERE container_id IS NULL OR container_id = '';
EOF
# -----------------------------------------------------------------------
# Step 3 — Backfill utm_* from old affiliate JSON (if column still exists)
# -----------------------------------------------------------------------
if [ "$(has_column affiliate)" = "1" ]; then
echo "Step 3: backfilling utm fields from affiliate JSON..."
sqlite3 "$DB" <<'EOF'
UPDATE containers
SET utm_source = COALESCE(utm_source, json_extract(affiliate, '$.utm_source')),
utm_medium = COALESCE(utm_medium, json_extract(affiliate, '$.utm_medium')),
utm_campaign = COALESCE(utm_campaign, json_extract(affiliate, '$.utm_campaign'))
WHERE affiliate IS NOT NULL AND affiliate != '';
EOF
else
echo "Step 3: affiliate column not present — skipping utm backfill."
fi
# -----------------------------------------------------------------------
# Step 4 — Remove affiliate column via full table rebuild.
# Also adds container_id UNIQUE constraint and locks in the canonical
# column order to match init_db().
# Only runs if affiliate still exists OR container_id lacks its UNIQUE index.
# -----------------------------------------------------------------------
NEEDS_REBUILD=0
[ "$(has_column affiliate)" = "1" ] && NEEDS_REBUILD=1
# If affiliate is already gone, the rebuild already ran — no need to repeat
# (container_id UNIQUE is guaranteed by the rebuild itself)
if [ "$NEEDS_REBUILD" = "1" ]; then
echo "Step 4: rebuilding table (remove affiliate, add container_id UNIQUE)..."
sqlite3 "$DB" <<'EOF'
BEGIN;
CREATE TABLE containers_new (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
UUID CHAR(50) UNIQUE,
container_id CHAR(50) UNIQUE,
email CHAR(100),
expires DATE,
tags TEXT,
env TEXT,
image CHAR(50),
history TEXT,
comment TEXT,
domains TEXT,
status CHAR(20),
created DATE,
bump DATE,
secret TEXT,
contract TEXT,
git integer,
backup_slots integer,
hdd integer,
workers integer,
utm_source text,
utm_medium text,
utm_campaign text,
domains_slots integer,
secret_list TEXT,
env_list TEXT
);
INSERT INTO containers_new
SELECT ID, UUID, container_id, email, expires, tags, env, image, history,
comment, domains, status, created, bump, secret, contract, git,
backup_slots, hdd, workers, utm_source, utm_medium, utm_campaign,
domains_slots, secret_list, env_list
FROM containers;
DROP TABLE containers;
ALTER TABLE containers_new RENAME TO containers;
COMMIT;
EOF
echo "Step 4: table rebuilt."
else
echo "Step 4: table already up to date — skipping rebuild."
fi
echo "Migration complete."
-54
View File
@@ -1,54 +0,0 @@
#!/bin/bash
# Load functions
source /4server/sbin/ODOO_19/ODOO_19.lib
if [[ -z "$UUID" ]]; then
echo "Error: UUID not set. Aborting."
exit 1
fi
POSTGRES_HOST="${POSTGRES_HOST:-beedb}"
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
POSTGRES_ADMIN_USER="${POSTGRES_ADMIN_USER:-1gtT0sf8klB9lDbYZD9}"
POSTGRES_ADMIN_PASSWORD="${POSTGRES_ADMIN_PASSWORD:-ZpSwWNafyy9GhY2gzHw}"
ODOO_DB_USER="${UUID}"
#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
BASEURL="${BASEURL:-/4server/data/$UUID}"
BACKUPURL="/4backup/$UUID"
doas docker stop "$UUID"
doas docker rm "$UUID"
if [ -n "${UUID:-}" ]; then
echo "Removing directory: $BASEURL"
#doas rm -rf "$BASEURL"
echo "Removing backup directory $BACKUPURL"
#doas rm -rf $BACKUPURL
fi
echo "Dropping PostgreSQL database: $UUID (if exists)..."
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres \
-c "DROP DATABASE IF EXISTS \"$UUID\";"
echo "Dropping PostgreSQL user: $ODOO_DB_USER (if exists)..."
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres <<EOF
DO
\$do\$
BEGIN
IF EXISTS (
SELECT
FROM pg_catalog.pg_user
WHERE usename = '${ODOO_DB_USER}') THEN
-- Drop user safely
EXECUTE 'DROP USER "' || '${ODOO_DB_USER}' || '"';
END IF;
END
\$do\$;
EOF
echo "✅ Database '$UUID' and user '$ODOO_DB_USER' removed (if they existed)."
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
# Unified Odoo nuke script (all versions).
# CONTAINER_ID, DB_NAME, SOFTWARE already exported by nukeContainer.
_self="$(realpath "${BASH_SOURCE[0]:-$0}")"; SBIN_ROOT="$(dirname "$(dirname "$_self")")" # parent of nuke/
source "$SBIN_ROOT/helpers"
source "$SBIN_ROOT/odoo.lib"
[ -z "$CONTAINER_ID" ] && die "CONTAINER_ID not set"
load_container_config "$CONTAINER_ID"
POSTGRES_HOST="${POSTGRES_HOST:-beedb}"
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
POSTGRES_ADMIN_USER="${POSTGRES_ADMIN_USER:-1gtT0sf8klB9lDbYZD9}"
POSTGRES_ADMIN_PASSWORD="${POSTGRES_ADMIN_PASSWORD:-ZpSwWNafyy9GhY2gzHw}"
DB_NAME="${DB_NAME:-$CONTAINER_ID}"
ODOO_DB_USER="$DB_NAME"
BASEURL="/4server/data/$CONTAINER_ID"
BACKUPURL="/BACKUP/$CONTAINER_ID"
log "Nuking container $CONTAINER_ID (DB_NAME=$DB_NAME)"
doas docker stop "$CONTAINER_ID" || warn "docker stop failed (may already be stopped)"
doas docker rm "$CONTAINER_ID" || warn "docker rm failed (may not exist)"
log "Dropping PostgreSQL database: $DB_NAME"
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres \
-c "DROP DATABASE IF EXISTS \"$DB_NAME\";" \
|| die "failed to drop database $DB_NAME"
log "Dropping PostgreSQL user: $ODOO_DB_USER"
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres <<EOF
DO
\$do\$
BEGIN
IF EXISTS (SELECT FROM pg_catalog.pg_user WHERE usename = '${ODOO_DB_USER}') THEN
EXECUTE 'DROP USER "' || '${ODOO_DB_USER}' || '"';
END IF;
END
\$do\$;
EOF
log "Database '$DB_NAME' and user '$ODOO_DB_USER' removed"
# Uncomment when ready to also wipe data directories:
# log "Removing data directory: $BASEURL"
# doas rm -rf "$BASEURL"
# log "Removing backup directory: $BACKUPURL"
# doas rm -rf "$BACKUPURL"
+20 -39
View File
@@ -1,68 +1,49 @@
#!/bin/bash
# Usage: ./start_by_uuid.sh <uuid>
# Example: ./start_by_uuid.sh abc-001-xxxx-xxxx-xxxx
exec > /4server/data/log/nukeContainer.log 2>&1
echo "$(date '+%Y-%m-%d %H:%M') Nuke container $1"
source /4server/sbin/helpers
_self="$(realpath "${BASH_SOURCE[0]:-$0}")"; SBIN="$(dirname "$_self")"
source "$SBIN/helpers"
BIN_PATH="/4server/sbin"
BIN_PATH="$SBIN"
CONTAINER_ID="$1"
UUID="$1"
if [[ -z "$UUID" ]]; then
echo "Usage: $0 <uuid>"
exit 1
fi
[ -z "$CONTAINER_ID" ] && die "Usage: $0 <uuid>"
log "Nuke container $CONTAINER_ID"
get_container_status() {
local uuid="$1"
# Get the container ID or name matching the UUID
CONTAINER_ID=$(docker ps -a --filter "name=$uuid" --format "{{.ID}}")
if [[ -z "$CONTAINER_ID" ]]; then
local container_id="$1"
# Get the Docker container ID or name matching the container_id
DOCKER_ID=$(docker ps -a --filter "name=$container_id" --format "{{.ID}}")
if [[ -z "$DOCKER_ID" ]]; then
echo "not_found"
return
fi
STATUS=$(docker inspect -f '{{.State.Status}}' "$CONTAINER_ID")
STATUS=$(docker inspect -f '{{.State.Status}}' "$DOCKER_ID")
echo "$STATUS"
}
# Check if container exists
STATUS=$(get_container_status "$UUID")
if [[ "$STATUS" == "running" ]]; then
echo "Container $UUID is still running. Aborting deletion."
exit 2
fi
STATUS=$(get_container_status "$CONTAINER_ID")
[ "$STATUS" = "running" ] && die "Container $CONTAINER_ID is still running — stop it first"
load_container_config "$CONTAINER_ID"
get_contract_info
# Extract the second part of UUID (split by "-")
SECOND_PART=$(echo "$UUID" | cut -d'-' -f2)
# Decide which script to run
case "$SECOND_PART" in
001)
case "$SOFTWARE" in
n8n)
"$BIN_PATH/nuke/n8n"
;;
002)
"$BIN_PATH/nuke/ODOO_19"
ODOO_17|ODOO_18|ODOO_19)
"$BIN_PATH/nuke/odoo"
;;
*)
echo "Unknown UUID type: $SECOND_PART"
exit 2
die "Unknown SOFTWARE type '${SOFTWARE}' for container_id $CONTAINER_ID — run: initContainer $CONTAINER_ID <n8n|ODOO_17|ODOO_18|ODOO_19>"
;;
esac
#sqlite3 "/4server/data/contracts.db" <<SQL
#DELETE FROM containers WHERE UUID='$UUID';
#SQL
echo "Container $UUID successfully nuked."
log "Container $CONTAINER_ID successfully nuked."
+107
View File
@@ -0,0 +1,107 @@
#!/bin/bash
# Shared library for all Odoo containers (17 / 18 / 19).
# Sourced by start/odoo, nuke/odoo, and admin tools.
# Requires: helpers already sourced (provides log/warn/die).
# ---------------------------------------------------------------------------
# dump_config — print all resolved variables for debugging
# ---------------------------------------------------------------------------
dump_config() {
log "========== Odoo Container Configuration =========="
log "CONTAINER_ID: $CONTAINER_ID"
log "DB_NAME: $DB_NAME"
log "IMAGE_TAG: $IMAGE_TAG"
log "BRANCH: $BRANCH"
log ""
log "PostgreSQL Host: $POSTGRES_HOST"
log "PostgreSQL Port: $POSTGRES_PORT"
log "ODOO DB User: $ODOO_DB_USER"
log ""
log "BASEURL: $BASEURL"
log "SERVER_IP: $SERVER_IP"
log "PORT: $PORT"
log "=================================================="
}
# ---------------------------------------------------------------------------
# build_traefik_rule
# Reads CONTAINER_ID and BASEURL from env. Resolves each candidate domain via
# nslookup and builds a Traefik Host() rule containing only domains that point
# to this server. Exports DOMAIN_LABEL.
# ---------------------------------------------------------------------------
build_traefik_rule() {
local server_ip
server_ip=$(ip -4 addr show eth0 | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1)
local domains=("$CONTAINER_ID.odoo4projects.com")
if [ -f "$BASEURL/etc/domain" ]; then
while IFS= read -r domain || [ -n "$domain" ]; do
[ -z "$domain" ] && continue
domains+=("$domain")
done < "$BASEURL/etc/domain"
fi
local rule=""
for d in "${domains[@]}"; do
local resolved_ip
resolved_ip=$(nslookup "$d" 2>/dev/null | awk '/^Address: / { print $2 }' | head -n1 || true)
if [ -z "$resolved_ip" ]; then
warn "build_traefik_rule: could not resolve $d, skipping"
continue
fi
if [ "$resolved_ip" = "$server_ip" ]; then
rule+=" || Host(\`$d\`)"
else
warn "build_traefik_rule: $d resolves to $resolved_ip, not $server_ip — skipping"
fi
done
rule="${rule# || }"
if [ -z "$rule" ]; then
die "build_traefik_rule: no valid domains resolved for $CONTAINER_ID — cannot start container"
fi
DOMAIN_LABEL="traefik.http.routers.$CONTAINER_ID.rule=$rule"
log "Traefik rule: $DOMAIN_LABEL"
}
# ---------------------------------------------------------------------------
# check_and_create_db
# Creates the postgres user and database if they don't exist yet.
# On a brand-new container this triggers a restore from default.zip.
# Requires: DB_NAME, ODOO_DB_USER, ODOO_DB_PASSWORD, POSTGRES_* from env.
# ---------------------------------------------------------------------------
check_and_create_db() {
local _dbname="${DB_NAME:-$CONTAINER_ID}"
local _dbuser="${ODOO_DB_USER:-$_dbname}"
log "check_and_create_db: user=$_dbuser db=$_dbname host=$POSTGRES_HOST"
# --- user ---
local _user_result
_user_result=$(PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" \
-d postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='$_dbuser';" 2>&1) \
|| die "check_and_create_db: psql connection failed: $_user_result"
if ! echo "$_user_result" | grep -q 1; then
log "Creating PostgreSQL user $_dbuser"
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" \
-d postgres -c "CREATE USER \"$_dbuser\" WITH PASSWORD '$ODOO_DB_PASSWORD';" \
|| die "check_and_create_db: failed to create user $_dbuser"
fi
# --- database ---
local _db_result
_db_result=$(PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" \
-d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='$_dbname';" 2>&1) \
|| die "check_and_create_db: psql connection failed: $_db_result"
if ! echo "$_db_result" | grep -q 1; then
log "Database $_dbname not found — restoring from default.zip"
"$SBIN_ROOT/odoo/restore" "$CONTAINER_ID" default.zip \
|| die "check_and_create_db: restore failed for $_dbname"
fi
}
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
# Usage: odoo/restore <container_id> <file>
# Restores an Odoo database from a backup zip inside the container.
if [ -z "$2" ]; then
echo "Usage: $0 <container_id> <file>"
exit 1
fi
CONTAINER_ID="$1"
_self="$(realpath "${BASH_SOURCE[0]:-$0}")"; SBIN_ROOT="$(dirname "$(dirname "$_self")")" # parent of odoo/
source "$SBIN_ROOT/helpers"
load_container_config "$CONTAINER_ID"
get_contract_info
DB_NAME="${DB_NAME:-$CONTAINER_ID}"
log "Restoring $2 into CONTAINER_ID=$CONTAINER_ID DB_NAME=$DB_NAME"
log "Postgres host: $POSTGRES_HOST"
BACKUP="/mnt/backup/$2"
TEMPLATE="/mnt/db_images/$2"
if doas docker exec "$CONTAINER_ID" /bin/bash -c "[ -f $BACKUP ]"; then
FILENAME="$BACKUP"
elif doas docker exec "$CONTAINER_ID" /bin/bash -c "[ -f $TEMPLATE ]"; then
FILENAME="$TEMPLATE"
else
die "restore: file $2 not found at $BACKUP or $TEMPLATE inside container $CONTAINER_ID"
fi
log "Using file: $FILENAME"
# Terminate active connections then drop/recreate
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -d postgres -c "
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE datname = '$DB_NAME' AND pid <> pg_backend_pid();" \
|| die "restore: failed to terminate connections on $DB_NAME"
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -d postgres \
-c "DROP DATABASE IF EXISTS \"$DB_NAME\";" \
|| die "restore: failed to drop database $DB_NAME"
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres \
-c "ALTER ROLE \"$DB_NAME\" CREATEDB;" \
|| die "restore: failed to grant CREATEDB on $DB_NAME"
doas docker exec "$CONTAINER_ID" rm -rf /home/odoo/.local/share/Odoo/filestore
doas docker exec "$CONTAINER_ID" rm -rf /root/.local/share/Odoo/filestore
doas docker exec "$CONTAINER_ID" odoo db \
--db_host beedb \
-w "$ODOO_DB_PASSWORD" \
-r "$DB_NAME" \
load "$DB_NAME" "$FILENAME" -f \
|| die "restore: odoo db load failed for $DB_NAME"
PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres \
-c "ALTER ROLE \"$DB_NAME\" NOCREATEDB;" \
|| die "restore: failed to revoke CREATEDB on $DB_NAME"
doas docker exec "$CONTAINER_ID" cp -r /root/.local/share/Odoo/filestore /home/odoo/.local/share/Odoo/filestore
doas docker exec "$CONTAINER_ID" chown -R odoo:odoo /home/odoo/.local
doas docker exec "$CONTAINER_ID" mkdir -p /var/lib/odoo/.local/share/Odoo/
doas docker exec "$CONTAINER_ID" ln -s /home/odoo/.local/share/Odoo/filestore /var/lib/odoo/.local/share/Odoo/filestore
log "Restore complete, restarting container $CONTAINER_ID"
docker restart "$CONTAINER_ID"
-43
View File
@@ -1,43 +0,0 @@
#!/usr/bin/env bash
set -e
DB_PATH="/4server/data/contracts.db"
COMPOSE_FILE="/4server/docker-compose.yml"
START_CONTAINER="/4server/sbin/startContainer"
echo "$(date '+%Y-%m-%d %H:%M:%S') Stopping and removing all Docker containers..."
# Remove all containers, including running containers
containers=$(doas docker ps -a -q)
if [ -n "$containers" ]; then
doas docker rm -f $containers
else
echo "No containers to remove."
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') Starting infrastructure..."
# Start infrastructure
doas docker compose -f "$COMPOSE_FILE" up -d
echo "$(date '+%Y-%m-%d %H:%M:%S') Infrastructure started."
# Get all non-expired container UUIDs
echo "$(date '+%Y-%m-%d %H:%M:%S') Loading active containers from database..."
uuids=$(sqlite3 "$DB_PATH" "
SELECT UUID
FROM containers
WHERE expires > date('now');
")
# Start each active container
for uuid in $uuids; do
echo "$(date '+%Y-%m-%d %H:%M:%S') Starting $uuid"
"$START_CONTAINER" "$uuid"
done
echo "$(date '+%Y-%m-%d %H:%M:%S') Restart complete."
+8 -8
View File
@@ -1,5 +1,5 @@
#!/bin/bash
cd /4server
# Check that a search string was provided
if [ -z "$1" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') Usage: $0 <search_string>"
@@ -9,14 +9,14 @@ fi
SEARCH="$1"
DB_PATH="/4server/data/contracts.db"
# Get all UUIDs where tags include the search string
uuids=$(sqlite3 "$DB_PATH" "SELECT UUID FROM containers WHERE tags LIKE '%$SEARCH%';")
# Get all container_ids where tags include the search string
container_ids=$(sqlite3 "$DB_PATH" "SELECT UUID FROM containers WHERE tags LIKE '%$SEARCH%';")
# Loop through each UUID
for uuid in $uuids; do
# Loop through each container_id
for container_id in $container_ids; do
# Check if a container with this name is running
if doas docker ps --format '{{.Names}}' | grep -qx "$uuid"; then
echo "$(date '+%Y-%m-%d %H:%M:%S') Restarting $uuid"
/4server/sbin/startContainer $uuid
if doas docker ps --format '{{.Names}}' | grep -qx "$container_id"; then
echo "$(date '+%Y-%m-%d %H:%M:%S') Restarting $container_id"
"$(dirname "$(realpath "${BASH_SOURCE[0]:-$0}")")/startContainer" "$container_id"
fi
done
+4 -4
View File
@@ -1,5 +1,5 @@
#!/bin/bash
# rotateLogs — rotate odoo.log for all UUID containers, keep 4 weeks
# rotateLogs — rotate odoo.log for all CONTAINER_ID containers, keep 4 weeks
# Runs as a daemon: waits for midnight, rotates, then waits for next midnight
RETENTION_DAYS=28
@@ -17,7 +17,7 @@ while true; do
for log in /4server/data/00*/logs/odoo.log; do
[[ -f "$log" ]] || continue
DIR=$(dirname "$log")
UUID=$(basename "$(dirname "$DIR")")
CONTAINER_ID=$(basename "$(dirname "$DIR")")
STAMP=$(date '+%Y-%m-%d_%H-%M')
ARCHIVE="$DIR/odoo.log.$STAMP"
@@ -33,11 +33,11 @@ for log in /4server/data/00*/logs/odoo.log; do
chmod "$PERMS" "${ARCHIVE}.gz"
chown "$OWNER" "${ARCHIVE}.gz"
echo "$UUID rotated -> $(basename "${ARCHIVE}.gz")"
echo "$CONTAINER_ID rotated -> $(basename "${ARCHIVE}.gz")"
# Delete rotated logs older than 4 weeks
find "$DIR" -name 'odoo.log.*.gz' -mtime +$RETENTION_DAYS -exec rm -f {} \; \
-exec echo "$UUID deleted: {}" \;
-exec echo "$CONTAINER_ID deleted: {}" \;
done
done
-130
View File
@@ -1,130 +0,0 @@
#!/bin/bash
# Load functions
source /4server/sbin/ODOO_19/ODOO_19.lib
source /4server/sbin/helpers
get_contract_info
# Config variables
UUID="${UUID:-default}"
BRANCH="${BRANCH:-release}"
ODOO_DB_USER="${UUID}"
#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
echo "$(date '+%Y-%m-%d %H:%M:%S') START ODOO 17 UUID=$UUID BRANCH=$BRANCH DBID=$CONTAINERDBID DB_USER=$ODOO_DB_USER HDD=$HDD DOMAIN_SLOTS=$DOMAIN_SLOTS BACKUP_SLOTS=$BACKUP_SLOTS WORKER=$WORKER"
BASEURL="${BASEURL:-/4server/data/$UUID}"
DATA_DIR="$BASEURL/odoo/"
CUSTOM_DIR="$BASEURL/git/$UUID/custom/"
ENTERPRISE_DIR="$BASEURL/git/$UUID/enterprise/"
LOGS_DIR="$BASEURL/logs/"
CONFIG_DIR="$BASEURL/config/"
CC_DIR="$BASEURL/cc/"
BACKUP_DIR="/BACKUP/$UUID"
GIT_DIR="$BASEURL/git-server/"
INSTALL_DIR="$BASEURL/install/"
SSH_DIR="$BASEURL/.ssh/"
ETC_DIR="$BASEURL/etc/"
SERVER_IP=$(ip -4 addr show eth0 | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1)
LABEL_DOMAINS=("$UUID.odoo4projects.com" "$UUID.$HOSTNAME.odoo4projects.com")
if [ -f "$BASEURL/etc/domain" ]; then
while IFS= read -r domain || [[ -n $domain ]]; do
[ -z "$domain" ] && continue
LABEL_DOMAINS+=("$domain")
done < "$BASEURL/etc/domain"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') No custom domain file at $BASEURL/etc/domain, using default domains $UUID.odoo4projects.com and $UUID.$HOSTNAME.odoo4projects.com only"
fi
RULE=""
for d in "${LABEL_DOMAINS[@]}"; do
RESOLVED_IP=$(nslookup "$d" 2>/dev/null | awk '/^Address: / { print $2 }' | head -n1 || true)
if [ -z "$RESOLVED_IP" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') Domain $d does not resolve, skipping"
continue
fi
if [ "$RESOLVED_IP" = "$SERVER_IP" ]; then
RULE+=" || Host(\`$d\`)"
echo "$(date '+%Y-%m-%d %H:%M:%S') Domain $d resolves to $RESOLVED_IP (server IP), added to Traefik rule"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') Domain $d resolves to $RESOLVED_IP but server IP is $SERVER_IP, skipping"
fi
done
RULE="${RULE# || }"
DOMAIN_LABEL="traefik.http.routers.$UUID.rule=$RULE"
echo "$(date '+%Y-%m-%d %H:%M:%S') Traefik label for $UUID: $DOMAIN_LABEL"
docker exec "$UUID" mkdir -p /var/lib/odoo/.local/share/Odoo/
docker exec "$UUID" ln -s /home/odoo/.local/share/Odoo/filestore /var/lib/odoo/.local/share/Odoo/filestore
find "$BASEURL" -type d -exec chmod 777 {} \;
PORT=$((CONTAINERDBID + 2200))
mkdir -p ${ETC_DIR}
touch ${ETC_DIR}gitpath
mkdir -p ${GIT_DIR}keys
touch ${GIT_DIR}keys/id_rsa.pub
echo "git clone \"ssh://git@${UUID}.odoo4projects.com:${PORT}/git-server/repos/odoo.git\"" > "${ETC_DIR}/gitpath"
echo "$(date '+%Y-%m-%d %H:%M:%S') Wrote git clone path for $UUID to ${ETC_DIR}gitpath (SSH port $PORT)"
echo "$(date '+%Y-%m-%d %H:%M:%S') Stopping and removing existing container $UUID (if any)"
docker stop "$UUID" 2>/dev/null
docker rm "$UUID" 2>/dev/null
EXTRA_DOCKER_PARAMETER=""
echo "$(date '+%Y-%m-%d %H:%M:%S') Starting container $UUID from docker.odoo4projects.com/4projects/odoo_17:$BRANCH (SSH port $PORT, network 4server_4projects)"
docker run -d --name "$UUID" \
--network 4server_4projects \
--restart=always \
$EXTRA_DOCKER_PARAMETER \
-v "$DATA_DIR/odoo-web-data:/home/odoo/.local/share/Odoo" \
-v "$CUSTOM_DIR:/mnt/addons/custom" \
-v "$ENTERPRISE_DIR:/mnt/addons/enterprise" \
-v "$LOGS_DIR:/mnt/logs" \
-v "$CC_DIR:/mnt/cc" \
-v "$BACKUP_DIR:/mnt/backup" \
-v "$GIT_DIR:/git-server" \
-v "$INSTALL_DIR:/mnt/install" \
-v "$SSH_DIR:/etc/sshkey" \
-v "$ETC_DIR:/mnt/etc" \
-p "$PORT:22" \
-e HOST="beedb" \
-e USER="$ODOO_DB_USER" \
-e PASSWORD="$ODOO_DB_PASSWORD" \
-e UUID="$UUID" \
-e HDD="$HDD" \
-e DOMAIN_SLOTS="$DOMAIN_SLOTS" \
-e BACKUP_SLOTS="$BACKUP_SLOTS" \
-e WORKER="$WORKER" \
-e GIT="$GIT" \
--label "$DOMAIN_LABEL" \
--label "traefik.http.services.$UUID.loadbalancer.server.port=8069" \
--label "traefic.http.routers.$UUID.entrypoints=web, websecure" \
--label "traefik.http.routers.$UUID.tls.certresolver=production" \
--label "traefik.http.routers.$UUID.tls=true" \
--label "traefik.http.routers.$UUID.service=$UUID" \
docker.odoo4projects.com/4projects/odoo_17:$BRANCH
echo "$(date '+%Y-%m-%d %H:%M:%S') docker run for $UUID finished with exit code $?"
echo "$(date '+%Y-%m-%d %H:%M:%S') Preparing filestore and fixing ownership in container $UUID"
docker exec "$UUID" mkdir -p /var/lib/odoo/.local/share/Odoo
docker exec "$UUID" rm -rf /var/lib/odoo/.local/share/Odoo/filestore
docker exec "$UUID" ln -s /home/odoo/.local/share/Odoo/filestore /var/lib/odoo/.local/share/Odoo/filestore
docker exec $UUID chown -R odoo:odoo /home/odoo/.local
docker exec $UUID chown -R odoo:odoo /var/lib/odoo/.local/share/Odoo
docker exec $UUID chown -R odoo:odoo /mnt/*
docker exec $UUID chown odoo:odoo /git-server/keys/id_rsa.pub
echo "$(date '+%Y-%m-%d %H:%M:%S') Checking/creating database for $UUID (user $ODOO_DB_USER)"
check_and_create_db
echo "$(date '+%Y-%m-%d %H:%M:%S') START ODOO 17 finished for $UUID"
-131
View File
@@ -1,131 +0,0 @@
#!/bin/bash
# Load functions
source /4server/sbin/ODOO_19/ODOO_19.lib
source /4server/sbin/helpers
get_contract_info
# Config variables
UUID="${UUID:-default}"
BRANCH="${BRANCH:-release}"
ODOO_DB_USER="${UUID}"
#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
echo "$(date '+%Y-%m-%d %H:%M:%S') START ODOO 18 UUID=$UUID BRANCH=$BRANCH DBID=$CONTAINERDBID DB_USER=$ODOO_DB_USER HDD=$HDD DOMAIN_SLOTS=$DOMAIN_SLOTS BACKUP_SLOTS=$BACKUP_SLOTS WORKER=$WORKER"
BASEURL="${BASEURL:-/4server/data/$UUID}"
DATA_DIR="$BASEURL/odoo/"
CUSTOM_DIR="$BASEURL/git/$UUID/custom/"
ENTERPRISE_DIR="$BASEURL/git/$UUID/enterprise/"
LOGS_DIR="$BASEURL/logs/"
CONFIG_DIR="$BASEURL/config/"
CC_DIR="$BASEURL/cc/"
BACKUP_DIR="/BACKUP/$UUID"
GIT_DIR="$BASEURL/git-server/"
INSTALL_DIR="$BASEURL/install/"
SSH_DIR="$BASEURL/.ssh/"
ETC_DIR="$BASEURL/etc/"
SERVER_IP=$(ip -4 addr show eth0 | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1)
LABEL_DOMAINS=("$UUID.odoo4projects.com" "$UUID.$HOSTNAME.odoo4projects.com")
if [ -f "$BASEURL/etc/domain" ]; then
while IFS= read -r domain || [[ -n $domain ]]; do
[ -z "$domain" ] && continue
LABEL_DOMAINS+=("$domain")
done < "$BASEURL/etc/domain"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') No custom domain file at $BASEURL/etc/domain, using default domains $UUID.odoo4projects.com and $UUID.$HOSTNAME.odoo4projects.com only"
fi
RULE=""
for d in "${LABEL_DOMAINS[@]}"; do
RESOLVED_IP=$(nslookup "$d" 2>/dev/null | awk '/^Address: / { print $2 }' | head -n1 || true)
if [ -z "$RESOLVED_IP" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') Domain $d does not resolve, skipping"
continue
fi
if [ "$RESOLVED_IP" = "$SERVER_IP" ]; then
RULE+=" || Host(\`$d\`)"
echo "$(date '+%Y-%m-%d %H:%M:%S') Domain $d resolves to $RESOLVED_IP (server IP), added to Traefik rule"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') Domain $d resolves to $RESOLVED_IP but server IP is $SERVER_IP, skipping"
fi
done
RULE="${RULE# || }"
DOMAIN_LABEL="traefik.http.routers.$UUID.rule=$RULE"
echo "$(date '+%Y-%m-%d %H:%M:%S') Traefik label for $UUID: $DOMAIN_LABEL"
docker exec "$UUID" mkdir -p /var/lib/odoo/.local/share/Odoo/
docker exec "$UUID" ln -s /home/odoo/.local/share/Odoo/filestore /var/lib/odoo/.local/share/Odoo/filestore
find "$BASEURL" -type d -exec chmod 777 {} \;
PORT=$((CONTAINERDBID + 2200))
mkdir -p ${ETC_DIR}
touch ${ETC_DIR}gitpath
mkdir -p ${GIT_DIR}keys
touch ${GIT_DIR}keys/id_rsa.pub
echo "git clone \"ssh://git@${UUID}.odoo4projects.com:${PORT}/git-server/repos/odoo.git\"" > "${ETC_DIR}/gitpath"
echo "$(date '+%Y-%m-%d %H:%M:%S') Wrote git clone path for $UUID to ${ETC_DIR}gitpath (SSH port $PORT)"
echo "$(date '+%Y-%m-%d %H:%M:%S') Stopping and removing existing container $UUID (if any)"
docker stop "$UUID" 2>/dev/null
docker rm "$UUID" 2>/dev/null
EXTRA_DOCKER_PARAMETER=""
echo "$(date '+%Y-%m-%d %H:%M:%S') Starting container $UUID from docker.odoo4projects.com/4projects/odoo_18:$BRANCH (SSH port $PORT, network 4server_4projects)"
docker run -d --name "$UUID" \
--network 4server_4projects \
--restart=always \
$EXTRA_DOCKER_PARAMETER \
-v "$DATA_DIR/odoo-web-data:/home/odoo/.local/share/Odoo" \
-v "$CUSTOM_DIR:/mnt/addons/custom" \
-v "$ENTERPRISE_DIR:/mnt/addons/enterprise" \
-v "$LOGS_DIR:/mnt/logs" \
-v "$CC_DIR:/mnt/cc" \
-v "$BACKUP_DIR:/mnt/backup" \
-v "$GIT_DIR:/git-server" \
-v "$INSTALL_DIR:/mnt/install" \
-v "$SSH_DIR:/etc/sshkey" \
-v "$ETC_DIR:/mnt/etc" \
-p "$PORT:22" \
-e HOST="beedb" \
-e USER="$ODOO_DB_USER" \
-e PASSWORD="$ODOO_DB_PASSWORD" \
-e UUID="$UUID" \
-e HDD="$HDD" \
-e DOMAIN_SLOTS="$DOMAIN_SLOTS" \
-e BACKUP_SLOTS="$BACKUP_SLOTS" \
-e WORKER="$WORKER" \
-e GIT="$GIT" \
--label "$DOMAIN_LABEL" \
--label "traefik.http.services.$UUID.loadbalancer.server.port=8069" \
--label "traefic.http.routers.$UUID.entrypoints=web, websecure" \
--label "traefik.http.routers.$UUID.tls.certresolver=production" \
--label "traefik.http.routers.$UUID.tls=true" \
--label "traefik.http.routers.$UUID.service=$UUID" \
docker.odoo4projects.com/4projects/odoo_18:$BRANCH
echo "$(date '+%Y-%m-%d %H:%M:%S') docker run for $UUID finished with exit code $?"
echo "$(date '+%Y-%m-%d %H:%M:%S') Preparing filestore and fixing ownership in container $UUID"
docker exec "$UUID" mkdir -p /var/lib/odoo/.local/share/Odoo
docker exec "$UUID" rm -rf /var/lib/odoo/.local/share/Odoo/filestore
docker exec "$UUID" ln -s /home/odoo/.local/share/Odoo/filestore /var/lib/odoo/.local/share/Odoo/filestore
docker exec $UUID chown -R odoo:odoo /home/odoo/.local
docker exec $UUID chown -R odoo:odoo /var/lib/odoo/.local/share/Odoo
docker exec $UUID chown -R odoo:odoo /mnt/*
docker exec $UUID chown odoo:odoo /git-server/keys/id_rsa.pub
echo "$(date '+%Y-%m-%d %H:%M:%S') Checking/creating database for $UUID (user $ODOO_DB_USER)"
check_and_create_db
echo "$(date '+%Y-%m-%d %H:%M:%S') START ODOO 18 finished for $UUID"
-129
View File
@@ -1,129 +0,0 @@
#!/bin/bash
# Load functions
source /4server/sbin/ODOO_19/ODOO_19.lib
source /4server/sbin/helpers
get_contract_info
# Config variables
UUID="${UUID:-default}"
BRANCH="${BRANCH:-release}"
ODOO_DB_USER="${UUID}"
#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
echo "$(date '+%Y-%m-%d %H:%M:%S') START ODOO 19 UUID=$UUID BRANCH=$BRANCH DBID=$CONTAINERDBID DB_USER=$ODOO_DB_USER HDD=$HDD DOMAIN_SLOTS=$DOMAIN_SLOTS BACKUP_SLOTS=$BACKUP_SLOTS WORKER=$WORKER"
BASEURL="${BASEURL:-/4server/data/$UUID}"
DATA_DIR="$BASEURL/odoo/"
CUSTOM_DIR="$BASEURL/git/$UUID/custom/"
ENTERPRISE_DIR="$BASEURL/git/$UUID/enterprise/"
LOGS_DIR="$BASEURL/logs/"
CONFIG_DIR="$BASEURL/config/"
CC_DIR="$BASEURL/cc/"
BACKUP_DIR="/BACKUP/$UUID"
GIT_DIR="$BASEURL/git-server/"
INSTALL_DIR="$BASEURL/install/"
SSH_DIR="$BASEURL/.ssh/"
ETC_DIR="$BASEURL/etc/"
SERVER_IP=$(ip -4 addr show eth0 | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1)
LABEL_DOMAINS=("$UUID.odoo4projects.com" "$UUID.$HOSTNAME.odoo4projects.com")
if [ -f "$BASEURL/etc/domain" ]; then
while IFS= read -r domain || [[ -n $domain ]]; do
[ -z "$domain" ] && continue
LABEL_DOMAINS+=("$domain")
done < "$BASEURL/etc/domain"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') No custom domain file at $BASEURL/etc/domain, using default domains $UUID.odoo4projects.com and $UUID.$HOSTNAME.odoo4projects.com only"
fi
RULE=""
for d in "${LABEL_DOMAINS[@]}"; do
RESOLVED_IP=$(nslookup "$d" 2>/dev/null | awk '/^Address: / { print $2 }' | head -n1 || true)
if [ -z "$RESOLVED_IP" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') Domain $d does not resolve, skipping"
continue
fi
if [ "$RESOLVED_IP" = "$SERVER_IP" ]; then
RULE+=" || Host(\`$d\`)"
echo "$(date '+%Y-%m-%d %H:%M:%S') Domain $d resolves to $RESOLVED_IP (server IP), added to Traefik rule"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') Domain $d resolves to $RESOLVED_IP but server IP is $SERVER_IP, skipping"
fi
done
RULE="${RULE# || }"
DOMAIN_LABEL="traefik.http.routers.$UUID.rule=$RULE"
echo "$(date '+%Y-%m-%d %H:%M:%S') Traefik label for $UUID: $DOMAIN_LABEL"
find "$BASEURL" -type d -exec chmod 777 {} \;
PORT=$((CONTAINERDBID + 2200))
mkdir -p ${ETC_DIR}
chmod 777 ${ETC_DIR}
touch ${ETC_DIR}gitpath
mkdir -p ${BACKUP_DIR}
chmod 777 ${BACKUP_DIR}
mkdir -p ${GIT_DIR}keys
chmod 777 ${GIT_DIR}
touch ${GIT_DIR}keys/id_rsa.pub
echo "git clone \"ssh://git@${UUID}.$HOSTNAME.odoo4projects.com:${PORT}/git-server/repos/odoo.git\"" > "${ETC_DIR}/gitpath"
echo "$(date '+%Y-%m-%d %H:%M:%S') Wrote git clone path for $UUID to ${ETC_DIR}gitpath (SSH port $PORT)"
echo "$(date '+%Y-%m-%d %H:%M:%S') Stopping and removing existing container $UUID (if any)"
docker stop "$UUID" 2>/dev/null
docker rm "$UUID" 2>/dev/null
EXTRA_DOCKER_PARAMETER=""
echo "$(date '+%Y-%m-%d %H:%M:%S') Starting container $UUID from docker.odoo4projects.com/4projects/odoo_19:$BRANCH (SSH port $PORT, network 4server_4projects)"
docker run -d --name "$UUID" \
--network 4server_4projects \
--restart=always \
$EXTRA_DOCKER_PARAMETER \
-v "$DATA_DIR/odoo-web-data:/home/odoo/.local/share/Odoo" \
-v "$CUSTOM_DIR:/mnt/addons/custom" \
-v "$ENTERPRISE_DIR:/mnt/addons/enterprise" \
-v "$LOGS_DIR:/mnt/logs" \
-v "$CC_DIR:/mnt/cc" \
-v "$BACKUP_DIR:/mnt/backup" \
-v "$GIT_DIR:/git-server" \
-v "$INSTALL_DIR:/mnt/install" \
-v "$SSH_DIR:/etc/sshkey" \
-v "$ETC_DIR:/mnt/etc" \
-p "$PORT:22" \
-e HOST="beedb" \
-e USER="$ODOO_DB_USER" \
-e PASSWORD="$ODOO_DB_PASSWORD" \
-e UUID="$UUID" \
-e HDD="$HDD" \
-e DOMAIN_SLOTS="$DOMAIN_SLOTS" \
-e BACKUP_SLOTS="$BACKUP_SLOTS" \
-e WORKER="$WORKER" \
-e GIT="$GIT" \
--label "$DOMAIN_LABEL" \
--label "traefik.http.services.$UUID.loadbalancer.server.port=8069" \
--label "traefik.http.routers.$UUID.entrypoints=web, websecure" \
--label "traefik.http.routers.$UUID.tls.certresolver=production" \
--label "traefik.http.routers.$UUID.tls=true" \
--label "traefik.http.routers.$UUID.service=$UUID" \
docker.odoo4projects.com/4projects/odoo_19:$BRANCH
echo "$(date '+%Y-%m-%d %H:%M:%S') docker run for $UUID finished with exit code $?"
echo "$(date '+%Y-%m-%d %H:%M:%S') Fixing ownership in container $UUID"
docker exec $UUID chown -R odoo:odoo /home/odoo/.local
docker exec $UUID chown -R odoo:odoo /mnt/*
docker exec $UUID chown -R git:git /git-server
chmod 777 /4server/data/$UUID/cc
echo "$(date '+%Y-%m-%d %H:%M:%S') Checking/creating database for $UUID (user $ODOO_DB_USER)"
check_and_create_db
echo "$(date '+%Y-%m-%d %H:%M:%S') START ODOO 19 finished for $UUID"
-129
View File
@@ -1,129 +0,0 @@
#!/bin/bash
# Load functions
source /4server/sbin/ODOO_19/ODOO_19.lib
source /4server/sbin/helpers
get_contract_info
# Config variables
UUID="${UUID:-default}"
BRANCH="${BRANCH:-release}"
ODOO_DB_USER="${UUID}"
#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
echo "$(date '+%Y-%m-%d %H:%M:%S') START ODOO 20 UUID=$UUID BRANCH=$BRANCH DBID=$CONTAINERDBID DB_USER=$ODOO_DB_USER HDD=$HDD DOMAIN_SLOTS=$DOMAIN_SLOTS BACKUP_SLOTS=$BACKUP_SLOTS WORKER=$WORKER"
BASEURL="${BASEURL:-/4server/data/$UUID}"
DATA_DIR="$BASEURL/odoo/"
CUSTOM_DIR="$BASEURL/git/$UUID/custom/"
ENTERPRISE_DIR="$BASEURL/git/$UUID/enterprise/"
LOGS_DIR="$BASEURL/logs/"
CONFIG_DIR="$BASEURL/config/"
CC_DIR="$BASEURL/cc/"
BACKUP_DIR="/BACKUP/$UUID"
GIT_DIR="$BASEURL/git-server/"
INSTALL_DIR="$BASEURL/install/"
SSH_DIR="$BASEURL/.ssh/"
ETC_DIR="$BASEURL/etc/"
SERVER_IP=$(ip -4 addr show eth0 | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1)
LABEL_DOMAINS=("$UUID.$HOSTNAME.odoo4projects.com")
if [ -f "$BASEURL/etc/domain" ]; then
while IFS= read -r domain || [[ -n $domain ]]; do
[ -z "$domain" ] && continue
LABEL_DOMAINS+=("$domain")
done < "$BASEURL/etc/domain"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') No custom domain file at $BASEURL/etc/domain, using default domain $UUID.$HOSTNAME.odoo4projects.com only"
fi
RULE=""
for d in "${LABEL_DOMAINS[@]}"; do
RESOLVED_IP=$(nslookup "$d" 2>/dev/null | awk '/^Address: / { print $2 }' | head -n1 || true)
if [ -z "$RESOLVED_IP" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') Domain $d does not resolve, skipping"
continue
fi
if [ "$RESOLVED_IP" = "$SERVER_IP" ]; then
RULE+=" || Host(\`$d\`)"
echo "$(date '+%Y-%m-%d %H:%M:%S') Domain $d resolves to $RESOLVED_IP (server IP), added to Traefik rule"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') Domain $d resolves to $RESOLVED_IP but server IP is $SERVER_IP, skipping"
fi
done
RULE="${RULE# || }"
DOMAIN_LABEL="traefik.http.routers.$UUID.rule=$RULE"
echo "$(date '+%Y-%m-%d %H:%M:%S') Traefik label for $UUID: $DOMAIN_LABEL"
find "$BASEURL" -type d -exec chmod 777 {} \;
PORT=$((CONTAINERDBID + 2200))
mkdir -p ${ETC_DIR}
chmod 777 ${ETC_DIR}
touch ${ETC_DIR}gitpath
mkdir -p ${BACKUP_DIR}
chmod 777 ${BACKUP_DIR}
mkdir -p ${GIT_DIR}keys
chmod 777 ${GIT_DIR}
touch ${GIT_DIR}keys/id_rsa.pub
echo "git clone \"ssh://git@${UUID}.${HOSTNAME}.odoo4projects.com:${PORT}/git-server/repos/odoo.git\"" > "${ETC_DIR}/gitpath"
echo "$(date '+%Y-%m-%d %H:%M:%S') Wrote git clone path for $UUID to ${ETC_DIR}gitpath (SSH port $PORT)"
echo "$(date '+%Y-%m-%d %H:%M:%S') Stopping and removing existing container $UUID (if any)"
docker stop "$UUID" 2>/dev/null
docker rm "$UUID" 2>/dev/null
EXTRA_DOCKER_PARAMETER=""
echo "$(date '+%Y-%m-%d %H:%M:%S') Starting container $UUID from docker.odoo4projects.com/4projects/odoo_20:$BRANCH (SSH port $PORT, network 4server_4projects)"
docker run -d --name "$UUID" \
--network 4server_4projects \
--restart=always \
$EXTRA_DOCKER_PARAMETER \
-v "$DATA_DIR/odoo-web-data:/home/odoo/.local/share/Odoo" \
-v "$CUSTOM_DIR:/mnt/addons/custom" \
-v "$ENTERPRISE_DIR:/mnt/addons/enterprise" \
-v "$LOGS_DIR:/mnt/logs" \
-v "$CC_DIR:/mnt/cc" \
-v "$BACKUP_DIR:/mnt/backup" \
-v "$GIT_DIR:/git-server" \
-v "$INSTALL_DIR:/mnt/install" \
-v "$SSH_DIR:/etc/sshkey" \
-v "$ETC_DIR:/mnt/etc" \
-p "$PORT:22" \
-e HOST="beedb" \
-e USER="$ODOO_DB_USER" \
-e PASSWORD="$ODOO_DB_PASSWORD" \
-e UUID="$UUID" \
-e HDD="$HDD" \
-e DOMAIN_SLOTS="$DOMAIN_SLOTS" \
-e BACKUP_SLOTS="$BACKUP_SLOTS" \
-e WORKER="$WORKER" \
-e GIT="$GIT" \
--label "$DOMAIN_LABEL" \
--label "traefik.http.services.$UUID.loadbalancer.server.port=8069" \
--label "traefik.http.routers.$UUID.entrypoints=web, websecure" \
--label "traefik.http.routers.$UUID.tls.certresolver=production" \
--label "traefik.http.routers.$UUID.tls=true" \
--label "traefik.http.routers.$UUID.service=$UUID" \
docker.odoo4projects.com/4projects/odoo_20:$BRANCH
echo "$(date '+%Y-%m-%d %H:%M:%S') docker run for $UUID finished with exit code $?"
echo "$(date '+%Y-%m-%d %H:%M:%S') Fixing ownership in container $UUID"
docker exec $UUID chown -R odoo:odoo /home/odoo/.local
docker exec $UUID chown -R odoo:odoo /mnt/*
docker exec $UUID chown -R git:git /git-server
chmod 777 /4server/data/$UUID/cc
echo "$(date '+%Y-%m-%d %H:%M:%S') Checking/creating database for $UUID (user $ODOO_DB_USER)"
check_and_create_db
echo "$(date '+%Y-%m-%d %H:%M:%S') START ODOO 20 finished for $UUID"
+24 -26
View File
@@ -1,31 +1,29 @@
#!/usr/bin/env bash
#--label "traefik.http.routers.${UUID}.middlewares=cors-headers@file" \
#--label "traefik.http.routers.${CONTAINER_ID}.middlewares=cors-headers@file" \
echo "Start N8N container ${CONTAINER_ID}"
# Get the hostname of the machine
HOSTNAME=$(hostname)
echo "$(date '+%Y-%m-%d %H:%M:%S') START N8N UUID=${UUID} HOST=${HOSTNAME} DOMAIN=${UUID}.odoo4projects.com DOMAIN2=${UUID}.${HOSTNAME}.odoo4projects.com"
mkdir -p /4server/data/${CONTAINER_ID}/n8n
mkdir -p /4server/data/${CONTAINER_ID}/data
mkdir -p /4server/data/${CONTAINER_ID}/backup
mkdir -p /4server/data/${UUID}/n8n
mkdir -p /4server/data/${UUID}/data
mkdir -p /4server/data/${UUID}/backup
chmod 777 /4server/data/${UUID}/n8n
chmod 777 /4server/data/${UUID}/data
chmod 777 /4server/data/${UUID}/backup
chmod 777 /4server/data/${CONTAINER_ID}/n8n
chmod 777 /4server/data/${CONTAINER_ID}/data
chmod 777 /4server/data/${CONTAINER_ID}/backup
# Stop the container if it exists
if docker ps -a --format '{{.Names}}' | grep -q "^${UUID}$"; then
echo "$(date '+%Y-%m-%d %H:%M:%S') Stopping and removing existing container $UUID"
docker stop "$UUID"
docker rm "$UUID"
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_ID}$"; then
echo "$(date '+%Y-%m-%d %H:%M') - stopping existing container $CONTAINER_ID"
docker stop "$CONTAINER_ID"
docker rm "$CONTAINER_ID"
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') Starting container $UUID from docker.odoo4projects.com/4projects/n8n:release (data in /4server/data/${UUID}, network 4server_4projects)"
docker run -d \
--name "$UUID" \
--name "$CONTAINER_ID" \
-p 5678 \
--cap-add=SYS_ADMIN \
--security-opt seccomp=unconfined \
@@ -37,7 +35,7 @@ docker run -d \
-e N8N_SMTP_PASS="Airbus12N@N" \
-e N8N_SMTP_SENDER="n8n <n8n@odoo4projects.com>" \
-e N8N_SMTP_SSL=false \
-e N8N_HOST="${UUID}.odoo4projects.com" \
-e N8N_HOST="${CONTAINER_ID}.odoo4projects.com" \
-e N8N_PORT=5678 \
-e N8N_PROTOCOL=https \
-e N8N_FORMDATA_FILE_SIZE_MAX=3000 \
@@ -45,20 +43,20 @@ docker run -d \
-e NODE_ENV=production \
-e EXECUTIONS_PRUNE=true \
-e EXECUTIONS_PRUNE_MAX_AGE=48 \
-e WEBHOOK_URL="https://${UUID}.odoo4projects.com/" \
-e WEBHOOK_URL="https://${CONTAINER_ID}.odoo4projects.com/" \
-e GENERIC_TIMEZONE="UTC-3" \
-e N8N_BLOCK_ENV_ACCESS_AND_PROCESS_INFORMATION=true \
-e N8N_CUSTOM_EXTENSIONS="/usr/local/share/n8n/custom" \
-v "/4server/data/${UUID}/n8n:/home/node/.n8n" \
-v "/4server/data/${UUID}/data:/data" \
-v "/4server/data/${UUID}/backup:/backup" \
-v "/4server/data/${CONTAINER_ID}/n8n:/home/node/.n8n" \
-v "/4server/data/${CONTAINER_ID}/data:/data" \
-v "/4server/data/${CONTAINER_ID}/backup:/backup" \
--label "traefik.enable=true" \
--label "traefik.http.routers.${UUID}.rule=Host(\`${UUID}.odoo4projects.com\`) || Host(\`${UUID}.${HOSTNAME}.odoo4projects.com\`)" \
--label "traefik.http.routers.${UUID}.entrypoints=web,websecure" \
--label "traefik.http.routers.${UUID}.tls=true" \
--label "traefik.http.routers.${UUID}.tls.certresolver=production" \
--label "traefik.http.services.${UUID}.loadbalancer.server.port=5678" \
--label "traefik.http.routers.${CONTAINER_ID}.rule=Host(\`${CONTAINER_ID}.odoo4projects.com\`)" \
--label "traefik.http.routers.${CONTAINER_ID}.entrypoints=web,websecure" \
--label "traefik.http.routers.${CONTAINER_ID}.tls=true" \
--label "traefik.http.routers.${CONTAINER_ID}.tls.certresolver=production" \
--label "traefik.http.services.${CONTAINER_ID}.loadbalancer.server.port=5678" \
--network 4server_4projects \
docker.odoo4projects.com/4projects/n8n:release
echo "$(date '+%Y-%m-%d %H:%M:%S') docker run for $UUID finished with exit code $?"
echo "Started $1"
+92
View File
@@ -0,0 +1,92 @@
#!/bin/bash
# Unified Odoo start script for all versions (17 / 18 / 19).
# Called by startContainer with IMAGE_TAG exported, e.g.:
# IMAGE_TAG=odoo_19 /4server/sbin/start/odoo
#
# Requires (exported by startContainer before this is called):
# CONTAINER_ID, IMAGE_TAG, SOFTWARE, DB_NAME
_self="$(realpath "${BASH_SOURCE[0]:-$0}")"; SBIN_ROOT="$(dirname "$(dirname "$_self")")" # parent of start/
source "$SBIN_ROOT/helpers"
source "$SBIN_ROOT/odoo.lib"
load_container_config "$CONTAINER_ID"
get_contract_info
BRANCH="${BRANCH:-release}"
DB_NAME="${DB_NAME:-$CONTAINER_ID}"
ODOO_DB_USER="$DB_NAME"
log "Starting Odoo container: CONTAINER_ID=$CONTAINER_ID IMAGE_TAG=$IMAGE_TAG DB_NAME=$DB_NAME"
BASEURL="/4server/data/$CONTAINER_ID"
DATA_DIR="$BASEURL/odoo"
CUSTOM_DIR="$BASEURL/git/$CONTAINER_ID/custom"
ENTERPRISE_DIR="$BASEURL/git/$CONTAINER_ID/enterprise"
LOGS_DIR="$BASEURL/logs"
CONFIG_DIR="$BASEURL/config"
CC_DIR="$BASEURL/cc"
BACKUP_DIR="/BACKUP/$CONTAINER_ID"
GIT_DIR="$BASEURL/git-server"
INSTALL_DIR="$BASEURL/install"
SSH_DIR="$BASEURL/.ssh"
ETC_DIR="$BASEURL/etc"
PORT=$((CONTAINERDBID + 2200))
log "SSH port: $PORT"
# --- prepare directories ---
find "$BASEURL" -type d -exec chmod 777 {} \;
mkdir -p "$ETC_DIR" "$BACKUP_DIR" "$GIT_DIR/keys" "$INSTALL_DIR" "$SSH_DIR"
chmod 777 "$ETC_DIR" "$BACKUP_DIR" "$GIT_DIR"
touch "$GIT_DIR/keys/id_rsa.pub"
echo "git clone \"ssh://git@${CONTAINER_ID}.odoo4projects.com:${PORT}/git-server/repos/odoo.git\"" > "$ETC_DIR/gitpath"
# --- build Traefik domain rule (exports DOMAIN_LABEL) ---
build_traefik_rule
# --- stop and remove any existing container ---
docker stop "$CONTAINER_ID" 2>/dev/null || true
docker rm "$CONTAINER_ID" 2>/dev/null || true
# --- run ---
docker run -d --name "$CONTAINER_ID" \
--network 4server_4projects \
--restart=always \
-v "$DATA_DIR/odoo-web-data:/home/odoo/.local/share/Odoo" \
-v "$CUSTOM_DIR:/mnt/addons/custom" \
-v "$ENTERPRISE_DIR:/mnt/addons/enterprise" \
-v "$LOGS_DIR:/mnt/logs" \
-v "$CC_DIR:/mnt/cc" \
-v "$BACKUP_DIR:/mnt/backup" \
-v "$GIT_DIR:/git-server" \
-v "$INSTALL_DIR:/mnt/install" \
-v "$SSH_DIR:/etc/sshkey" \
-v "$ETC_DIR:/mnt/etc" \
-p "$PORT:22" \
-e HOST="beedb" \
-e USER="$ODOO_DB_USER" \
-e PASSWORD="$ODOO_DB_PASSWORD" \
-e CONTAINER_ID="$CONTAINER_ID" \
-e DB_NAME="$DB_NAME" \
-e HDD="$HDD" \
-e DOMAINS_SLOTS="$DOMAINS_SLOTS" \
-e BACKUP_SLOTS="$BACKUP_SLOTS" \
-e WORKER="$WORKER" \
-e GIT="$GIT" \
--label "$DOMAIN_LABEL" \
--label "traefik.http.services.$CONTAINER_ID.loadbalancer.server.port=8069" \
--label "traefik.http.routers.$CONTAINER_ID.entrypoints=web,websecure" \
--label "traefik.http.routers.$CONTAINER_ID.tls=true" \
--label "traefik.http.routers.$CONTAINER_ID.tls.certresolver=production" \
--label "traefik.http.routers.$CONTAINER_ID.service=$CONTAINER_ID" \
"docker.odoo4projects.com/4projects/${IMAGE_TAG}:${BRANCH}" \
|| die "docker run failed for $CONTAINER_ID"
# --- fix ownership inside container ---
docker exec "$CONTAINER_ID" chown -R odoo:odoo /home/odoo/.local
docker exec "$CONTAINER_ID" chown -R odoo:odoo /mnt
docker exec "$CONTAINER_ID" chown -R git:git /git-server
chmod 777 "$CC_DIR"
# --- ensure DB/user exist, restore from default.zip if brand new ---
check_and_create_db
+20 -41
View File
@@ -1,67 +1,46 @@
#!/bin/bash
# Usage: ./start_by_uuid.sh <uuid>
# Example: ./start_by_uuid.sh abc-001-xxxx-xxxx-xxxx
exec > /4server/data/log/startContainer.log 2>&1
echo "$(date '+%Y-%m-%d %H:%M:%S') Start container $1"
cd/4server
source /4server/sbin/helpers
BIN_PATH="/4server/sbin"
_self="$(realpath "${BASH_SOURCE[0]:-$0}")"; SBIN="$(dirname "$_self")"
source "$SBIN/helpers"
UUID="$1"
BIN_PATH="$SBIN"
CONTAINER_ID="$1"
if [[ -z "$UUID" ]]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') Usage: $0 <uuid>"
exit 1
fi
if [[ ! "$UUID" =~ ^[0-9a-fA-F-]+$ ]]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') ERROR: Invalid UUID format: $UUID"
exit 1
fi
[ -z "$CONTAINER_ID" ] && die "Usage: $0 <uuid>"
log "Start container $CONTAINER_ID"
DOMAIN_FILE="/4server/data/$UUID/etc/domain"
DOMAIN_FILE="/4server/data/$CONTAINER_ID/etc/domain"
DB_FILE="/4server/data/contracts.db"
if [ -f "$DOMAIN_FILE" ]; then
DOMAINS=$(paste -sd "," "$DOMAIN_FILE")
# Escape single quotes for SQLite ('' is the standard SQLite escape for ')
DOMAINS_SAFE="${DOMAINS//\'/\'\'}"
UUID_SAFE="${UUID//\'/\'\'}"
sqlite3 "$DB_FILE" "UPDATE containers SET domains='$DOMAINS_SAFE' WHERE UUID='$UUID_SAFE';"
CONTAINER_ID_SAFE="${CONTAINER_ID//\'/\'\'}"
sqlite3 "$DB_FILE" "UPDATE containers SET domains='$DOMAINS_SAFE' WHERE UUID='$CONTAINER_ID_SAFE';"
fi
load_container_config "$CONTAINER_ID"
get_contract_info
# Extract the second part of UUID (split by "-")
SECOND_PART=$(echo "$UUID" | cut -d'-' -f2)
# Decide which script to run
case "$SECOND_PART" in
001)
# Decide which script to run based on SOFTWARE from container.env
case "$SOFTWARE" in
n8n)
"$BIN_PATH/start/n8n"
;;
002)
"$BIN_PATH/start/ODOO_18"
ODOO_17)
IMAGE_TAG=odoo_17 "$BIN_PATH/start/odoo"
;;
003)
"$BIN_PATH/start/ODOO_19"
ODOO_18)
IMAGE_TAG=odoo_18 "$BIN_PATH/start/odoo"
;;
004)
"$BIN_PATH/start/ODOO_17"
ODOO_19)
IMAGE_TAG=odoo_19 "$BIN_PATH/start/odoo"
;;
005)
"$BIN_PATH/start/ODOO_20"
;;
*)
echo "$(date '+%Y-%m-%d %H:%M:%S') Unknown UUID type: $SECOND_PART"
exit 2
die "Unknown SOFTWARE type '${SOFTWARE}' for container_id $CONTAINER_ID — run: initContainer $CONTAINER_ID <n8n|ODOO_17|ODOO_18|ODOO_19>"
;;
esac
+1 -17
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python3
"""Collect server runtime stats (uptime, top, cgroup, mounts, hdd) and return as JSON."""
"""Collect server runtime stats (uptime, top, cgroup, mounts) and return as JSON."""
import json
import os
import subprocess
@@ -33,20 +32,6 @@ def read_file(path):
return None
def disk_usage(path):
"""Total/used/available bytes for the filesystem at `path` (same figures as df)."""
try:
st = os.statvfs(path)
return {
"path": path,
"total_bytes": st.f_blocks * st.f_frsize,
"used_bytes": (st.f_blocks - st.f_bfree) * st.f_frsize,
"available_bytes": st.f_bavail * st.f_frsize,
}
except Exception as e:
return f"ERROR: {e}"
def main():
stats = {}
@@ -54,7 +39,6 @@ def main():
stats["top"] = cmd(["sh", "-c", "top -b -n 1 | head -n 3"], splitlines=True)
stats["cgroup"] = read_file("/proc/self/cgroup")
stats["mounts"] = cmd(["mount"], splitlines=True)
stats["hdd"] = disk_usage("/")
print(json.dumps(stats, indent=2))
-98
View File
@@ -1,98 +0,0 @@
#!/bin/bash
#
# swapon — create, enable and persist an 8GB swap file.
#
# Runs on each managed server (Alpine/OpenRC). Deploy with ./update_sbin, then:
#
# rex doas /4server/sbin/swapon
#
# The script aborts if any swap is already active or if /swapfile already
# exists, so it is safe to run against the whole fleet.
#
# Optional first argument: swap size in MiB (default 8192 = 8GB).
set -euo pipefail
SWAPFILE="/swapfile"
SWAP_SIZE_MB="${1:-8192}"
FSTAB="/etc/fstab"
log() {
echo "[$(hostname)] $*"
}
die() {
echo "[$(hostname)] ERROR: $*" >&2
exit 1
}
case "$SWAP_SIZE_MB" in
''|*[!0-9]*) die "swap size must be a positive integer in MiB" ;;
esac
[ "$SWAP_SIZE_MB" -gt 0 ] || die "swap size must be greater than zero"
# Remove a partially created swap file if anything fails before it is enabled.
CREATED=0
cleanup() {
[ "$CREATED" -eq 1 ] || return 0
[ -e "$SWAPFILE" ] || return 0
if grep -q "^${SWAPFILE}[[:space:]]" /proc/swaps; then
return 0
fi
log "removing incomplete ${SWAPFILE}"
rm -f "$SWAPFILE"
}
trap cleanup EXIT
# --- must run as root ---------------------------------------------------
[ "$(id -u)" -eq 0 ] || die "must be run as root (use: rex doas /4server/sbin/swapon)"
# --- abort if swap is already active ------------------------------------
# /proc/swaps always has a header line; anything more means swap is in use.
if [ "$(wc -l < /proc/swaps)" -gt 1 ]; then
echo "[$(hostname)] ERROR: swap is already active:" >&2
cat /proc/swaps >&2
exit 1
fi
# --- abort if the swap file already exists ------------------------------
if [ -e "$SWAPFILE" ]; then
die "$SWAPFILE already exists; remove it manually before recreating swap"
fi
# --- persistence requires OpenRC's swap service --------------------------
[ -x /etc/init.d/swap ] || \
die "/etc/init.d/swap not found; cannot persist swap across reboots"
# --- make sure there is enough room on / ---------------------------------
AVAIL_KB=$(df -Pk / | awk 'NR==2 {print $4}')
# Leave a 100MiB margin for filesystem metadata.
REQUIRED_KB=$((SWAP_SIZE_MB * 1024 + 102400))
[ "$AVAIL_KB" -ge "$REQUIRED_KB" ] || \
die "not enough free space on / (need ${SWAP_SIZE_MB}MiB, have $((AVAIL_KB / 1024))MiB)"
# --- create the swap file -----------------------------------------------
log "creating ${SWAP_SIZE_MB}MiB swap file at ${SWAPFILE}"
CREATED=1
dd if=/dev/zero of="$SWAPFILE" bs=1M count="$SWAP_SIZE_MB"
chmod 600 "$SWAPFILE"
/sbin/mkswap "$SWAPFILE"
# --- persist across reboots ---------------------------------------------
# OpenRC's swap service runs `swapon -a`, which reads /etc/fstab.
if ! grep -qE "^[[:space:]]*${SWAPFILE}[[:space:]]" "$FSTAB"; then
# Terminate the last line first in case fstab has no trailing newline.
[ -z "$(tail -c 1 "$FSTAB")" ] || echo >> "$FSTAB"
echo "${SWAPFILE} none swap sw 0 0" >> "$FSTAB"
log "added ${SWAPFILE} to ${FSTAB}"
fi
rc-update add swap boot >/dev/null 2>&1 || \
die "could not add the swap service to the boot runlevel"
# --- enable it now -------------------------------------------------------
# Absolute path avoids recursing into this script if /4server/sbin is on PATH.
/sbin/swapon "$SWAPFILE"
log "swap enabled:"
cat /proc/swaps
-3
View File
@@ -1,4 +1 @@
API_KEY={{API_KEY}}
PROVIDER={{PROVIDER}}
COST={{COST}}
CONTRACT_END={{CONTRACT_END}}
+3 -2
View File
@@ -17,7 +17,7 @@ services:
ipv4_address: 10.5.0.200
traefik:
image: docker.io/library/traefik:3.7
image: docker.io/library/traefik:3.6
container_name: traefik
ports:
- 80:80
@@ -38,4 +38,5 @@ networks:
config:
- subnet: 10.5.0.0/16
gateway: 10.5.0.1
ip_range: 10.5.16.0/20
ip_range: 10.5.0.0/26
-1
View File
@@ -1,2 +1 @@
permit nopass 4server as root
permit nopass root as root
+1 -1
View File
@@ -1,3 +1,3 @@
# Example: allow your host to connect to all DBs as any user with password
host all all 10.5.0.1/16 md5
host all all 10.5.0.1/32 md5
+1 -1
View File
@@ -1,5 +1,5 @@
listen_addresses = '*'
max_connections = 300
max_connections = 150
dynamic_shared_memory_type = posix
max_wal_size = 1GB
min_wal_size = 80MB
+3 -2
View File
@@ -1,2 +1,3 @@
https://dl-cdn.alpinelinux.org/alpine/v3.24/main
https://dl-cdn.alpinelinux.org/alpine/v3.24/community
https://dl-cdn.alpinelinux.org/alpine/v3.22/main
https://dl-cdn.alpinelinux.org/alpine/v3.22/community
+2
View File
@@ -99,3 +99,5 @@ http:
loadBalancer:
servers:
- url: "http://10.5.0.1:8888"
+2 -4
View File
@@ -31,13 +31,11 @@ rex doas pip install --root-user-action ignore --break-system-packages --no-cach
### own bins
echo "Running prsync ./sbin"
prsync -h "/app/host_vars/hosts" -avz ./sbin/ /4server/sbin/
prsync -h "/app/host_vars/hosts" -avz ./sbin/ /usr/sbin/
### POSTGRESS
rex mkdir -p /4server/data/postgres/etc
template templates/pg_hba.conf /4server/data/postgres/data/pg_hba.conf
template templates/postgresql.conf /4server/data/postgres/data/postgresql.conf
template templates/pq_hba.conf /4server/data/postgres/etc/
+4
View File
@@ -0,0 +1,4 @@
rex doas apk update
rex doas apk fix
rex doas apk upgrade
rex doas reboot
+1 -8
View File
@@ -1,7 +1,7 @@
#!/bin/bash
echo "Running prsync ./sbin"
prsync -h "/app/host_vars/hosts" -avz ./sbin/ /4server/sbin/
prsync -h "/app/host_vars/hosts" -avz ./sbin/ /usr/sbin/
template templates/.profile /home/4server/.profile
template templates/postgresql.conf /4server/data/postgres/data/postgresql.conf
@@ -12,10 +12,3 @@ rex doas rc-service checkCalls restart
prsync -h "/app/host_vars/hosts" -avz ./host_vars/borg-backup/* /home/4server/.ssh
rex doas chown 4server:4server /home/4server/.ssh/borg*
rex doas chmod 600 /home/4server/.ssh/borg*
# link all save scripts to /usr/sbin
rex doas ln -sf /4server/sbin/activeContainers /usr/bin/
rex doas ln -sf /4server/sbin/startContainer /usr/bin/
rex doas ln -sf /4server/sbin/restartContainers /usr/bin/
rex doas ln -sf /4server/sbin/checkBackup /usr/bin/
-9
View File
@@ -1,9 +0,0 @@
template templates/repositories /etc/apk/repositories
template templates/docker-compose.yml /4server/docker-compose.yml
rex doas apk update
rex doas apk fix
rex doas apk upgrade
rex doas docker compose -f /4server/docker-compose.yml pull
rex doas docker compose -f /4server/docker-compose.yml up -d
rex doas reboot
Binary file not shown.