Compare commits
21
Commits
e03bb926e8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
920a7de568 | ||
|
|
44eaced13d | ||
|
|
eeabd35f8d | ||
|
|
de583713da | ||
|
|
10f0422155 | ||
|
|
7263c859a2 | ||
|
|
cd187b6d3d | ||
|
|
8772004ff6 | ||
|
|
4868517abb | ||
|
|
a915a87ed9 | ||
|
|
722e499089 | ||
|
|
d25a9a94a3 | ||
|
|
b6f6da01e0 | ||
|
|
d6d0f37c5a | ||
|
|
9b02a488bf | ||
|
|
9abed9f2e9 | ||
|
|
ac65ba2169 | ||
|
|
796047a39f | ||
|
|
250280a2c7 | ||
|
|
ed4fa767b0 | ||
|
|
df86400eb5 |
No files matched your search
@@ -23,6 +23,7 @@ RUN apk add --no-cache \
|
||||
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/
|
||||
|
||||
+6
-6
@@ -1,8 +1,3 @@
|
||||
#Host dev
|
||||
# Hostname dev
|
||||
# User oliver
|
||||
# Port 2222
|
||||
|
||||
Host dev
|
||||
Hostname dev
|
||||
User 4server
|
||||
@@ -30,7 +25,7 @@ Host meppel
|
||||
IdentityFile /app/host_vars/meppel/meppel
|
||||
|
||||
Host sydney
|
||||
Hostname 192.168.9.22
|
||||
Hostname 192.168.9.14
|
||||
User 4server
|
||||
IdentityFile /app/host_vars/sydney/sydney
|
||||
|
||||
@@ -38,3 +33,8 @@ 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
|
||||
@@ -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.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=
|
||||
+7
-2
@@ -2,7 +2,12 @@
|
||||
|
||||
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"
|
||||
+12
-5
@@ -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)
|
||||
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")
|
||||
|
||||
@@ -15,8 +15,18 @@ 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; 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"
|
||||
|
||||
if [ ! -f "$host_env_file" ]; then
|
||||
@@ -34,7 +44,6 @@ while read -r host <&3; do
|
||||
|
||||
SSH_PRIVATE=$(<"$host_vars_dir/$host/$host")
|
||||
SSH_PUBLIC=$(<"$host_vars_dir/$host/$host.pub")
|
||||
|
||||
content=$(<"$localfile")
|
||||
|
||||
for key in "${keys[@]}"; do
|
||||
@@ -42,7 +51,6 @@ while read -r host <&3; 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"
|
||||
@@ -52,4 +60,3 @@ while read -r host <&3; do
|
||||
ssh "$host" "doas mv '$remotetmp' '$remotefile'"
|
||||
|
||||
done 3< /app/host_vars/hosts
|
||||
|
||||
+3
-4
@@ -6,12 +6,11 @@ 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
|
||||
|
||||
@@ -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
|
||||
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 \
|
||||
|
||||
Executable
+17
@@ -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;
|
||||
"
|
||||
+74
-20
@@ -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()
|
||||
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])}
|
||||
|
||||
|
||||
@@ -551,13 +587,31 @@ def backup_move(request: MoveRequest):
|
||||
|
||||
|
||||
class BackupCheckRequest(BaseModel):
|
||||
uuid: str
|
||||
uuid: Optional[str] = None
|
||||
|
||||
|
||||
@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"])}
|
||||
|
||||
|
||||
@app.get("/backup/all", dependencies=[Depends(verify_api_key)])
|
||||
def backup_all():
|
||||
|
||||
@@ -36,9 +36,34 @@ 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")
|
||||
|
||||
|
||||
+87
-23
@@ -1,44 +1,108 @@
|
||||
#!/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 <uuid>")
|
||||
sys.exit(1)
|
||||
|
||||
uuid = sys.argv[1]
|
||||
directory = BACKUP_ROOT / uuid
|
||||
def check_backup(uuid):
|
||||
"""Return (ok, message) for the given UUID."""
|
||||
directory = BACKUP_ROOT / uuid
|
||||
|
||||
if not directory.is_dir():
|
||||
print(f"ERROR: directory not found for UUID {uuid}")
|
||||
sys.exit(1)
|
||||
if not directory.is_dir():
|
||||
return False, f"ERROR: directory not found for UUID {uuid}"
|
||||
|
||||
latest_date = None
|
||||
latest_file = None
|
||||
latest_date = None
|
||||
latest_file = None
|
||||
|
||||
for zip_file in directory.rglob("*.zip"):
|
||||
name = zip_file.name
|
||||
try:
|
||||
date_part = name.split("_")[0]
|
||||
for zip_file in directory.rglob("*.zip"):
|
||||
date_part = zip_file.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:
|
||||
print("NO BACKUPS")
|
||||
if latest_date is None:
|
||||
return False, "NO BACKUPS"
|
||||
|
||||
if latest_file.stat().st_size < 1000:
|
||||
return True, "01-01-2000"
|
||||
|
||||
# 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)
|
||||
|
||||
if latest_file.stat().st_size < 1000:
|
||||
print("01-01-2000")
|
||||
sys.exit(0)
|
||||
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
|
||||
|
||||
# Format as yyyy-mm-dd
|
||||
print(f"{latest_date[:4]}-{latest_date[4:6]}-{latest_date[6:8]}")
|
||||
|
||||
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()
|
||||
Executable
+23
@@ -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,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>"
|
||||
|
||||
@@ -28,14 +28,14 @@ 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")
|
||||
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 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
|
||||
|
||||
RULE=""
|
||||
|
||||
@@ -29,14 +29,14 @@ 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")
|
||||
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 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
|
||||
|
||||
RULE=""
|
||||
|
||||
@@ -29,14 +29,14 @@ 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")
|
||||
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 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
|
||||
|
||||
RULE=""
|
||||
@@ -73,7 +73,7 @@ chmod 777 ${GIT_DIR}
|
||||
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)"
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
# 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"
|
||||
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}/data
|
||||
@@ -53,7 +53,7 @@ docker run -d \
|
||||
-v "/4server/data/${UUID}/data:/data" \
|
||||
-v "/4server/data/${UUID}/backup:/backup" \
|
||||
--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}.tls=true" \
|
||||
--label "traefik.http.routers.${UUID}.tls.certresolver=production" \
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# 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"
|
||||
|
||||
+17
-1
@@ -1,7 +1,8 @@
|
||||
#!/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 os
|
||||
import subprocess
|
||||
|
||||
|
||||
@@ -32,6 +33,20 @@ 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 = {}
|
||||
|
||||
@@ -39,6 +54,7 @@ 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))
|
||||
|
||||
|
||||
Executable
+98
@@ -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
|
||||
@@ -1 +1,4 @@
|
||||
API_KEY={{API_KEY}}
|
||||
PROVIDER={{PROVIDER}}
|
||||
COST={{COST}}
|
||||
CONTRACT_END={{CONTRACT_END}}
|
||||
@@ -17,7 +17,7 @@ services:
|
||||
ipv4_address: 10.5.0.200
|
||||
|
||||
traefik:
|
||||
image: docker.io/library/traefik:3.6
|
||||
image: docker.io/library/traefik:3.7
|
||||
container_name: traefik
|
||||
ports:
|
||||
- 80:80
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
permit nopass 4server as root
|
||||
permit nopass root as root
|
||||
@@ -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/32 md5
|
||||
host all all 10.5.0.1/16 md5
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
https://dl-cdn.alpinelinux.org/alpine/v3.22/main
|
||||
https://dl-cdn.alpinelinux.org/alpine/v3.22/community
|
||||
|
||||
https://dl-cdn.alpinelinux.org/alpine/v3.24/main
|
||||
https://dl-cdn.alpinelinux.org/alpine/v3.24/community
|
||||
+2
-2
@@ -35,8 +35,8 @@ prsync -h "/app/host_vars/hosts" -avz ./sbin/ /4server/sbin/
|
||||
|
||||
### POSTGRESS
|
||||
rex mkdir -p /4server/data/postgres/etc
|
||||
template templates/pg_hba.conf /4server/data/postgres/etc/
|
||||
template templates/postgresql.conf /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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
rex doas apk update
|
||||
rex doas apk fix
|
||||
rex doas apk upgrade
|
||||
rex doas reboot
|
||||
@@ -12,3 +12,10 @@ 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/
|
||||
Executable
+9
@@ -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.
Reference in new issue
Block a user