diff --git a/app/onboarding b/app/onboarding index ca352dc..1d0e918 100755 --- a/app/onboarding +++ b/app/onboarding @@ -11,7 +11,6 @@ rex doas apk add bash doas rsync openssh linux-lts #rex doas chmod 644 /boot/extlinux.conf -# ass swap file ???? rex doas mkdir -p /4server rex doas chmod 777 /4server @@ -20,13 +19,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 diff --git a/app/sbin/api b/app/sbin/api index 0aff93f..1cfcc58 100755 --- a/app/sbin/api +++ b/app/sbin/api @@ -17,6 +17,14 @@ 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" @@ -89,13 +97,29 @@ 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() - cursor.execute(query, params) - conn.commit() - data = cursor.fetchall() if fetch else None - conn.close() + 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, + ) if data and fetch: return [dict(row) for row in data] return None @@ -160,22 +184,6 @@ 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(): @@ -184,11 +192,24 @@ 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, + ) 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( """ @@ -221,15 +242,21 @@ 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"} # 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 - for field in ContainerModel.__fields__: + model_fields = getattr(ContainerModel, "model_fields", None) or ContainerModel.__fields__ + for field in model_fields: if field == "UUID": continue value = getattr(request, field) @@ -237,17 +264,26 @@ def update_container(request: ContainerModel): updates[field] = value params.append(value) + log.info("update_container: computed updates=%s params=%s", updates, params) + 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)) + 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) return { "UUID": request.UUID, "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"} @@ -257,7 +293,7 @@ def start_container(request: UUIDRequest): @app.post("/container/restartAllContainers", dependencies=[Depends(verify_api_key)]) -def start_container(request: ImageRequest): +def restart_all_containers_by_image(request: ImageRequest): return {"message": run_command([f"{BIN_PATH}/restartContainers", request.image])} diff --git a/app/sbin/swapon b/app/sbin/swapon new file mode 100755 index 0000000..2839ed3 --- /dev/null +++ b/app/sbin/swapon @@ -0,0 +1,98 @@ +#!/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