Compare commits

..
19 Commits
Author SHA1 Message Date
oliver 920a7de568 db access 2026-11-08 05:08:17 -03:00
oliver 44eaced13d working 2026-10-21 17:27:33 -03:00
oliver eeabd35f8d working 2026-10-17 18:57:34 -03:00
oliver de583713da c 2026-10-15 18:52:10 -03:00
oliver 10f0422155 odoo 2026-10-15 18:33:33 -03:00
oliver 7263c859a2 fixes 2026-10-15 17:45:09 -03:00
oliver cd187b6d3d k 2026-10-15 05:05:31 -03:00
oliver 8772004ff6 working 2026-10-14 18:35:16 -03:00
oliver 4868517abb update 2026-10-14 10:27:30 -03:00
oliver a915a87ed9 works 2026-10-14 10:18:40 -03:00
oliver 722e499089 s 2026-10-14 10:00:09 -03:00
oliver d25a9a94a3 n8n + hostname 2026-10-14 07:26:16 -03:00
oliver b6f6da01e0 host in domain 2026-10-14 07:24:18 -03:00
oliver d6d0f37c5a works 2026-10-13 20:00:20 -03:00
oliver 9b02a488bf works 2026-10-13 17:42:02 -03:00
oliver 9abed9f2e9 filter hosts 2026-10-13 13:24:58 -03:00
oliver ac65ba2169 working 2026-10-13 13:19:55 -03:00
oliver 796047a39f d 2026-10-13 09:47:24 -03:00
oliver 250280a2c7 update 2026-10-13 09:23:36 -03:00
31 changed files with 433 additions and 91 deletions

No files matched your search

+1
View File
@@ -23,6 +23,7 @@ RUN apk add --no-cache \
WORKDIR /root WORKDIR /root
RUN ln -sf /usr/bin/nvim /usr/bin/vim RUN ln -sf /usr/bin/nvim /usr/bin/vim
RUN ln -sf /4server/vault/open /usr/bin/open
COPY known_hosts /root/.ssh/ COPY known_hosts /root/.ssh/
COPY config /root/.ssh/ COPY config /root/.ssh/
+6 -6
View File
@@ -1,8 +1,3 @@
#Host dev
# Hostname dev
# User oliver
# Port 2222
Host dev Host dev
Hostname dev Hostname dev
User 4server User 4server
@@ -30,7 +25,7 @@ Host meppel
IdentityFile /app/host_vars/meppel/meppel IdentityFile /app/host_vars/meppel/meppel
Host sydney Host sydney
Hostname 192.168.9.22 Hostname 192.168.9.14
User 4server User 4server
IdentityFile /app/host_vars/sydney/sydney IdentityFile /app/host_vars/sydney/sydney
@@ -38,3 +33,8 @@ Host saopaulo
Hostname 192.168.9.11 Hostname 192.168.9.11
User 4server User 4server
IdentityFile /app/host_vars/saopaulo/saopaulo 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,3 +15,6 @@
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.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.22 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMO5VhveQe76kEOdVqrdoTwW1fNzmgDzZNBznFEC4ohtXes0VMtJR02lhc+55XTIUX+EDxBtAh8U+kJpFeLetMU=
192.168.9.21 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGlzLZWNjSaDuDd4e66LoBizY5j+QCsifIIxkvX4CrzP/AqAgWDEEgT+pAXBFkNJlBR6TDFJ0bIdwZTbcq8/72E= 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=
+7 -2
View File
@@ -2,7 +2,12 @@
cmd="$*" cmd="$*"
echo "Running on hosts: $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"
pssh -h /app/host_vars/hosts -t 0 "$cmd" echo "Running on hosts: $(tr '\n' ' ' < "$tmp_hosts")"
echo "Command: $cmd"
pssh -h "$tmp_hosts" -t 0 "$cmd"
+7 -2
View File
@@ -7,7 +7,7 @@ fi
host_vars_dir="/app/host_vars" host_vars_dir="/app/host_vars"
keys=(NEBULA_CA API_KEY HOSTNAME NEBULA_CRT NEBULA_KEY SSH_PRIVATE SSH_PUBLIC) keys=(NEBULA_CA API_KEY HOSTNAME NEBULA_CRT NEBULA_KEY SSH_PRIVATE SSH_PUBLIC PROVIDER COST CONTRACT_END )
NEBULA_CA=$(<"$host_vars_dir/ca.crt") NEBULA_CA=$(<"$host_vars_dir/ca.crt")
@@ -21,7 +21,12 @@ case "$remotefile" in
esac esac
# Read hosts from file descriptor 3 to prevent ssh from consuming stdin # Read hosts from file descriptor 3 to prevent ssh from consuming stdin
while read -r host <&3; do while read -r host <&3 || [ -n "$host" ]; do
# Strip comments (# ...) and surrounding whitespace; skip empty lines
host="${host%%#*}"
host="${host//[[:space:]]/}"
[ -z "$host" ] && continue
host_env_file="$host_vars_dir/$host/$host.env" host_env_file="$host_vars_dir/$host/$host.env"
if [ ! -f "$host_env_file" ]; then if [ ! -f "$host_env_file" ]; then
+5 -6
View File
@@ -6,12 +6,11 @@ rex doas apk update
rex doas apk add bash doas rsync openssh linux-lts rex doas apk add bash doas rsync openssh linux-lts
### activate lts kerner ### activate lts kerner
template templates/extlinux.conf /boot/extlinux.conf #template templates/extlinux.conf /boot/extlinux.conf
rex doas chown root:root /boot/extlinux.conf #rex doas chown root:root /boot/extlinux.conf
rex doas chmod 644 /boot/extlinux.conf #rex doas chmod 644 /boot/extlinux.conf
# ass swap file ????
rex doas mkdir -p /4server rex doas mkdir -p /4server
rex doas chmod 777 /4server rex doas chmod 777 /4server
@@ -20,13 +19,13 @@ rex doas chmod 777 /4server
# ----- install nabula # ----- install nabula
echo "prsync nebula bin" echo "prsync nebula bin"
prsync -h "/app/host_vars/hosts" -avz ./sbin/nebula /4server/nebula 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 mkdir -p /etc/nebula
rex doas chmod 700 /etc/nebula rex doas chmod 700 /etc/nebula
template templates/nebula/config.yml /etc/nebula/config.yml 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/host.crt /etc/nebula/host.crt
template templates/nebula/ca.crt /etc/nebula/ca.crt template templates/nebula/ca.crt /etc/nebula/ca.crt
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
rex doas docker system prune -a -f
+14
View File
@@ -65,6 +65,20 @@ 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 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" 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" 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 \ PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \
-h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres \ -h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres \
+17
View File
@@ -0,0 +1,17 @@
#!/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;
"
+79 -25
View File
@@ -17,6 +17,14 @@ from fastapi.responses import PlainTextResponse, RedirectResponse
from fastapi.security.api_key import APIKeyHeader from fastapi.security.api_key import APIKeyHeader
from pydantic import BaseModel 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 ---------------------- # ---------------------- Constants ----------------------
DB_PATH = "/4server/data/contracts.db" DB_PATH = "/4server/data/contracts.db"
BIN_PATH = "/4server/sbin" BIN_PATH = "/4server/sbin"
@@ -89,13 +97,29 @@ def init_db():
def execute_db(query: str, params: tuple = (), fetch: bool = False): 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 = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(query, params) try:
conn.commit() cursor.execute(query, params)
data = cursor.fetchall() if fetch else None conn.commit()
conn.close() 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: if data and fetch:
return [dict(row) for row in data] return [dict(row) for row in data]
return None return None
@@ -160,22 +184,6 @@ class ImageRequest(BaseModel):
image: str 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 ---------------------- # ---------------------- Routes ----------------------
@app.get("/", include_in_schema=False) @app.get("/", include_in_schema=False)
def redirect_to_odoo(): def redirect_to_odoo():
@@ -184,11 +192,24 @@ def redirect_to_odoo():
@app.post("/container/update", dependencies=[Depends(verify_api_key)]) @app.post("/container/update", dependencies=[Depends(verify_api_key)])
def update_container(request: ContainerModel): 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 # Fetch existing record
existing = execute_db( existing = execute_db(
"SELECT * FROM containers WHERE UUID = ?", (request.UUID,), fetch=True "SELECT * FROM containers WHERE UUID = ?", (request.UUID,), fetch=True
) )
log.info(
"update_container lookup: UUID=%r existing=%s",
request.UUID,
existing,
)
if not 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 # If record does not exist, insert a new one with all given fields
execute_db( execute_db(
""" """
@@ -221,15 +242,21 @@ def update_container(request: ContainerModel):
request.env_list, 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 {"UUID": request.UUID, "status": "created"}
# Existing record found, do partial update # Existing record found, do partial update
existing = existing[0] # Assuming fetch returns list of dicts existing = existing[0] # Assuming fetch returns list of dicts
log.info("update_container: existing row before update=%s", existing)
updates = {} updates = {}
params = [] params = []
# Only add fields that are not None in the request # 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": if field == "UUID":
continue continue
value = getattr(request, field) value = getattr(request, field)
@@ -237,17 +264,26 @@ def update_container(request: ContainerModel):
updates[field] = value updates[field] = value
params.append(value) params.append(value)
log.info("update_container: computed updates=%s params=%s", updates, params)
if updates: if updates:
# Build SQL dynamically # Build SQL dynamically
set_clause = ", ".join(f"{k}=?" for k in updates.keys()) set_clause = ", ".join(f"{k}=?" for k in updates.keys())
params.append(request.UUID) # UUID for WHERE clause 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 { return {
"UUID": request.UUID, "UUID": request.UUID,
"status": "updated", "status": "updated",
"fields_updated": list(updates.keys()), "fields_updated": list(updates.keys()),
} }
log.info("update_container: no non-None fields supplied -> no_change")
return {"UUID": request.UUID, "status": "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)]) @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])} return {"message": run_command([f"{BIN_PATH}/restartContainers", request.image])}
@@ -551,12 +587,30 @@ def backup_move(request: MoveRequest):
class BackupCheckRequest(BaseModel): class BackupCheckRequest(BaseModel):
uuid: str uuid: Optional[str] = None
@app.post("/backup/check", dependencies=[Depends(verify_api_key)]) @app.post("/backup/check", dependencies=[Depends(verify_api_key)])
def backup_check(request: BackupCheckRequest): def backup_check(request: BackupCheckRequest):
return {"message": run_command([f"{BIN_PATH}/checkBackup", request.uuid])} 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"])}
@app.get("/backup/all", dependencies=[Depends(verify_api_key)]) @app.get("/backup/all", dependencies=[Depends(verify_api_key)])
+25
View File
@@ -36,9 +36,34 @@ def read_file(path):
return None 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(): def main():
audit = {} audit = {}
# Contract info from /etc/4server
audit.update(
read_env_file("/etc/4server", ["PROVIDER", "COST", "CONTRACT_END"])
)
# Container identity # Container identity
audit["container_id"] = os.environ.get("HOSTNAME") audit["container_id"] = os.environ.get("HOSTNAME")
+91 -27
View File
@@ -1,44 +1,108 @@
#!/usr/bin/env python3 #!/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 import sys
from datetime import date, datetime
from pathlib import Path from pathlib import Path
BACKUP_ROOT = Path("/BACKUP") BACKUP_ROOT = Path("/BACKUP")
MAX_AGE_DAYS = 2
ACTIVE_CONTAINERS = Path(__file__).resolve().parent / "activeContainers"
if len(sys.argv) < 2:
print("Usage: checkBackup <uuid>")
sys.exit(1)
uuid = sys.argv[1] def check_backup(uuid):
directory = BACKUP_ROOT / uuid """Return (ok, message) for the given UUID."""
directory = BACKUP_ROOT / uuid
if not directory.is_dir(): if not directory.is_dir():
print(f"ERROR: directory not found for UUID {uuid}") return False, f"ERROR: directory not found for UUID {uuid}"
sys.exit(1)
latest_date = None latest_date = None
latest_file = None latest_file = None
for zip_file in directory.rglob("*.zip"): for zip_file in directory.rglob("*.zip"):
name = zip_file.name date_part = zip_file.name.split("_")[0]
try:
date_part = name.split("_")[0]
if len(date_part) != 8 or not date_part.isdigit(): if len(date_part) != 8 or not date_part.isdigit():
continue continue
except Exception:
continue
if latest_date is None or date_part > latest_date: if latest_date is None or date_part > latest_date:
latest_date = date_part latest_date = date_part
latest_file = zip_file latest_file = zip_file
if latest_date is None: if latest_date is None:
print("NO BACKUPS") return False, "NO BACKUPS"
sys.exit(1)
if latest_file.stat().st_size < 1000: if latest_file.stat().st_size < 1000:
print("01-01-2000") return True, "01-01-2000"
sys.exit(0)
# Format as yyyy-mm-dd # Format as yyyy-mm-dd
print(f"{latest_date[:4]}-{latest_date[4:6]}-{latest_date[6:8]}") 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()
+23
View File
@@ -0,0 +1,23 @@
#!/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"
+1 -1
View File
@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
cd /4server
# Check that a search string was provided # Check that a search string was provided
if [ -z "$1" ]; then if [ -z "$1" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') Usage: $0 <search_string>" echo "$(date '+%Y-%m-%d %H:%M:%S') Usage: $0 <search_string>"
+2 -2
View File
@@ -28,14 +28,14 @@ ETC_DIR="$BASEURL/etc/"
SERVER_IP=$(ip -4 addr show eth0 | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1) SERVER_IP=$(ip -4 addr show eth0 | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1)
LABEL_DOMAINS=("$UUID.odoo4projects.com") LABEL_DOMAINS=("$UUID.odoo4projects.com" "$UUID.$HOSTNAME.odoo4projects.com")
if [ -f "$BASEURL/etc/domain" ]; then if [ -f "$BASEURL/etc/domain" ]; then
while IFS= read -r domain || [[ -n $domain ]]; do while IFS= read -r domain || [[ -n $domain ]]; do
[ -z "$domain" ] && continue [ -z "$domain" ] && continue
LABEL_DOMAINS+=("$domain") LABEL_DOMAINS+=("$domain")
done < "$BASEURL/etc/domain" done < "$BASEURL/etc/domain"
else else
echo "$(date '+%Y-%m-%d %H:%M:%S') No custom domain file at $BASEURL/etc/domain, using default domain $UUID.odoo4projects.com only" 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 fi
RULE="" RULE=""
+2 -2
View File
@@ -29,14 +29,14 @@ ETC_DIR="$BASEURL/etc/"
SERVER_IP=$(ip -4 addr show eth0 | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1) SERVER_IP=$(ip -4 addr show eth0 | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1)
LABEL_DOMAINS=("$UUID.odoo4projects.com") LABEL_DOMAINS=("$UUID.odoo4projects.com" "$UUID.$HOSTNAME.odoo4projects.com")
if [ -f "$BASEURL/etc/domain" ]; then if [ -f "$BASEURL/etc/domain" ]; then
while IFS= read -r domain || [[ -n $domain ]]; do while IFS= read -r domain || [[ -n $domain ]]; do
[ -z "$domain" ] && continue [ -z "$domain" ] && continue
LABEL_DOMAINS+=("$domain") LABEL_DOMAINS+=("$domain")
done < "$BASEURL/etc/domain" done < "$BASEURL/etc/domain"
else else
echo "$(date '+%Y-%m-%d %H:%M:%S') No custom domain file at $BASEURL/etc/domain, using default domain $UUID.odoo4projects.com only" 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 fi
RULE="" RULE=""
+3 -3
View File
@@ -29,14 +29,14 @@ ETC_DIR="$BASEURL/etc/"
SERVER_IP=$(ip -4 addr show eth0 | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1) SERVER_IP=$(ip -4 addr show eth0 | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1)
LABEL_DOMAINS=("$UUID.odoo4projects.com") LABEL_DOMAINS=("$UUID.odoo4projects.com" "$UUID.$HOSTNAME.odoo4projects.com")
if [ -f "$BASEURL/etc/domain" ]; then if [ -f "$BASEURL/etc/domain" ]; then
while IFS= read -r domain || [[ -n $domain ]]; do while IFS= read -r domain || [[ -n $domain ]]; do
[ -z "$domain" ] && continue [ -z "$domain" ] && continue
LABEL_DOMAINS+=("$domain") LABEL_DOMAINS+=("$domain")
done < "$BASEURL/etc/domain" done < "$BASEURL/etc/domain"
else else
echo "$(date '+%Y-%m-%d %H:%M:%S') No custom domain file at $BASEURL/etc/domain, using default domain $UUID.odoo4projects.com only" 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 fi
RULE="" RULE=""
@@ -73,7 +73,7 @@ chmod 777 ${GIT_DIR}
touch ${GIT_DIR}keys/id_rsa.pub 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 "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') Wrote git clone path for $UUID to ${ETC_DIR}gitpath (SSH port $PORT)"
+2 -2
View File
@@ -4,7 +4,7 @@
# Get the hostname of the machine # Get the hostname of the machine
HOSTNAME=$(hostname) HOSTNAME=$(hostname)
echo "$(date '+%Y-%m-%d %H:%M:%S') START N8N UUID=${UUID} HOST=${HOSTNAME} DOMAIN=${UUID}.odoo4projects.com" 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/${UUID}/n8n mkdir -p /4server/data/${UUID}/n8n
mkdir -p /4server/data/${UUID}/data mkdir -p /4server/data/${UUID}/data
@@ -53,7 +53,7 @@ docker run -d \
-v "/4server/data/${UUID}/data:/data" \ -v "/4server/data/${UUID}/data:/data" \
-v "/4server/data/${UUID}/backup:/backup" \ -v "/4server/data/${UUID}/backup:/backup" \
--label "traefik.enable=true" \ --label "traefik.enable=true" \
--label "traefik.http.routers.${UUID}.rule=Host(\`${UUID}.odoo4projects.com\`)" \ --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}.entrypoints=web,websecure" \
--label "traefik.http.routers.${UUID}.tls=true" \ --label "traefik.http.routers.${UUID}.tls=true" \
--label "traefik.http.routers.${UUID}.tls.certresolver=production" \ --label "traefik.http.routers.${UUID}.tls.certresolver=production" \
+1 -1
View File
@@ -3,7 +3,7 @@
# Example: ./start_by_uuid.sh abc-001-xxxx-xxxx-xxxx # Example: ./start_by_uuid.sh abc-001-xxxx-xxxx-xxxx
exec > /4server/data/log/startContainer.log 2>&1 exec > /4server/data/log/startContainer.log 2>&1
echo "$(date '+%Y-%m-%d %H:%M:%S') Start container $1" echo "$(date '+%Y-%m-%d %H:%M:%S') Start container $1"
cd/4server
source /4server/sbin/helpers source /4server/sbin/helpers
BIN_PATH="/4server/sbin" BIN_PATH="/4server/sbin"
+17 -1
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Collect server runtime stats (uptime, top, cgroup, mounts) and return as JSON.""" """Collect server runtime stats (uptime, top, cgroup, mounts, hdd) and return as JSON."""
import json import json
import os
import subprocess import subprocess
@@ -32,6 +33,20 @@ def read_file(path):
return None 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(): def main():
stats = {} stats = {}
@@ -39,6 +54,7 @@ def main():
stats["top"] = cmd(["sh", "-c", "top -b -n 1 | head -n 3"], splitlines=True) stats["top"] = cmd(["sh", "-c", "top -b -n 1 | head -n 3"], splitlines=True)
stats["cgroup"] = read_file("/proc/self/cgroup") stats["cgroup"] = read_file("/proc/self/cgroup")
stats["mounts"] = cmd(["mount"], splitlines=True) stats["mounts"] = cmd(["mount"], splitlines=True)
stats["hdd"] = disk_usage("/")
print(json.dumps(stats, indent=2)) print(json.dumps(stats, indent=2))
+98
View File
@@ -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
+3
View File
@@ -1 +1,4 @@
API_KEY={{API_KEY}} API_KEY={{API_KEY}}
PROVIDER={{PROVIDER}}
COST={{COST}}
CONTRACT_END={{CONTRACT_END}}
+1 -1
View File
@@ -17,7 +17,7 @@ services:
ipv4_address: 10.5.0.200 ipv4_address: 10.5.0.200
traefik: traefik:
image: docker.io/library/traefik:3.6 image: docker.io/library/traefik:3.7
container_name: traefik container_name: traefik
ports: ports:
- 80:80 - 80:80
+1
View File
@@ -1 +1,2 @@
permit nopass 4server as root 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 # Example: allow your host to connect to all DBs as any user with password
host all all 10.5.0.1/32 md5 host all all 10.5.0.1/16 md5
+2 -3
View File
@@ -1,3 +1,2 @@
https://dl-cdn.alpinelinux.org/alpine/v3.22/main https://dl-cdn.alpinelinux.org/alpine/v3.24/main
https://dl-cdn.alpinelinux.org/alpine/v3.22/community https://dl-cdn.alpinelinux.org/alpine/v3.24/community
+2 -2
View File
@@ -35,8 +35,8 @@ prsync -h "/app/host_vars/hosts" -avz ./sbin/ /4server/sbin/
### POSTGRESS ### POSTGRESS
rex mkdir -p /4server/data/postgres/etc rex mkdir -p /4server/data/postgres/etc
template templates/pg_hba.conf /4server/data/postgres/etc/pg_hba.conf template templates/pg_hba.conf /4server/data/postgres/data/pg_hba.conf
template templates/postgresql.conf /4server/data/postgres/etc/postgresql.conf template templates/postgresql.conf /4server/data/postgres/data/postgresql.conf
-4
View File
@@ -1,4 +0,0 @@
rex doas apk update
rex doas apk fix
rex doas apk upgrade
rex doas reboot
+7
View File
@@ -12,3 +12,10 @@ rex doas rc-service checkCalls restart
prsync -h "/app/host_vars/hosts" -avz ./host_vars/borg-backup/* /home/4server/.ssh 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 chown 4server:4server /home/4server/.ssh/borg*
rex doas chmod 600 /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
@@ -0,0 +1,9 @@
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.