Compare commits

..

28 Commits

Author SHA1 Message Date
oliver ce01dd64f7 USB 2026-05-27 16:28:42 -03:00
Your Name eb4d83e9e9 hmm 2026-05-27 16:28:02 -03:00
Your Name 5c6dbe416a n 2026-04-21 17:09:04 -03:00
oliver 72243d8b18 pruning 2026-04-21 17:07:07 -03:00
oliver 11a1c2635f comment 2026-04-21 16:32:37 -03:00
oliver 835165289b Update api 2026-04-21 16:28:49 -03:00
oliver 08b9c38d0f get 2026-04-21 16:18:27 -03:00
oliver eb8c770506 Update api 2026-04-21 16:07:22 -03:00
oliver 3c33bd86e5 works 2026-04-21 16:04:12 -03:00
oliver 04f665a20d Update borgpush 2026-04-21 15:55:54 -03:00
oliver 1d2ddd85e1 borgpush 2026-04-21 15:55:03 -03:00
oliver 0251f06861 backup 2026-04-21 13:59:34 -03:00
oliver 4328750ac8 h 2026-04-21 13:53:37 -03:00
oliver 22e49faf46 m 2026-04-21 13:49:52 -03:00
oliver c94bac2db3 n 2026-04-21 13:49:29 -03:00
oliver 1f08c66c78 c 2026-04-21 13:08:18 -03:00
oliver ba5ad005a6 Backup 2026-04-21 12:45:18 -03:00
oliver 1b64bdc08d b 2026-04-21 12:17:42 -03:00
oliver 7e3f4be906 Create backup 2026-04-21 12:17:33 -03:00
oliver d354d989e7 Backup 17, 18 changed 2026-04-21 12:03:46 -03:00
oliver 902c43ec89 Update ODOO_19 2026-04-21 12:02:39 -03:00
oliver 717045baa6 added modules 2026-04-21 11:51:35 -03:00
oliver e672b1d2c6 ln, update 2026-04-21 11:44:30 -03:00
oliver ad59e8f0f1 NEW 2026-04-21 11:39:34 -03:00
Your Name 943d67045e secret list 2026-04-11 19:30:00 -03:00
Your Name b525fdaeaf changes 2026-04-11 19:26:23 -03:00
Your Name 2549419ef1 fix 2026-04-11 18:30:19 -03:00
Your Name 73090ddaa1 changes 2026-04-11 18:12:42 -03:00
39 changed files with 1036 additions and 293 deletions
+2
View File
@@ -1,2 +1,4 @@
alpine.qcow2
tmp/
exchange/
backup/
+7
View File
@@ -0,0 +1,7 @@
# Build output
output/
data/
# Nebula certificates and keys (user-provided secrets)
nebula/*.crt
nebula/*.key
+54
View File
@@ -0,0 +1,54 @@
FROM alpine:3.22
# Base tools
RUN apk add --no-cache \
bash \
coreutils \
util-linux \
tar \
gzip \
xz \
cpio \
curl \
wget \
ca-certificates \
rsync \
file \
grep \
gawk \
sed \
vim
# Filesystem + disk tools (needed by chroot-setup.sh and for Alpine install)
RUN apk add --no-cache \
dosfstools \
e2fsprogs \
xfsprogs \
btrfs-progs \
parted \
sfdisk \
blkid \
lsblk \
wipefs \
util-linux-misc \
findmnt
# ISO + boot tools
RUN apk add --no-cache \
xorriso \
syslinux \
mtools \
squashfs-tools \
libarchive-tools \
busybox-static
# GRUB (BIOS + EFI) — available if needed for alternative builds
RUN apk add --no-cache \
grub \
grub-bios \
grub-efi \
efibootmgr
WORKDIR /work
CMD ["/bin/bash"]
+119
View File
@@ -0,0 +1,119 @@
# Alpine Linux USB Rescue ISO
A bootable hybrid ISO that:
- Gets an IP via **DHCP on `eth0`**
- Automatically opens a **Nebula overlay tunnel**
- Runs **OpenSSH** (login: `root` / `alpine`)
- Carries all tools needed to **install Alpine onto the host machine** (`setup-alpine`, `setup-disk`, `parted`, …)
The ISO is a hybrid image — it can be written to a USB stick with `dd` *or* burned to a CD/DVD.
---
## Prerequisites
- Docker + Docker Compose
- A Nebula certificate set for this node (see below)
---
## 1. Place Nebula certificates
Put the following files in `nebula/`:
```
nebula/
nebula ← binary (already included)
config.yml ← already included, edit as needed
ca.crt ← your Nebula CA certificate ← YOU PROVIDE
host.crt ← this node's certificate ← YOU PROVIDE
host.key ← this node's private key ← YOU PROVIDE
```
`ca.crt`, `host.crt`, and `host.key` are gitignored.
---
## 2. Build the ISO
```bash
docker compose run --rm alpine bash /scripts/create.sh
```
The ISO is written to `output/rescue-alpine.iso`.
---
## 3. Write to USB
```bash
dd if=output/rescue-alpine.iso of=/dev/sdX bs=4M status=progress && sync
```
Replace `/dev/sdX` with your USB device (check with `lsblk`). **All data on the device will be erased.**
Or burn to CD/DVD:
```bash
cdrecord dev=/dev/sr0 output/rescue-alpine.iso
```
---
## 4. Boot and connect
1. Insert USB and boot the target machine from it.
2. The system comes up fully in RAM — no writes to the host disk.
3. On boot, `eth0` gets an IP via DHCP, then the Nebula tunnel comes up automatically.
4. SSH in via the Nebula overlay IP (see `nebula/config.yml`):
```bash
ssh root@<nebula-ip>
# password: alpine
```
---
## 5. Install Alpine onto the host machine
Once SSH'd in (or at the local console):
```bash
# Interactive guided installer — partitions disk, installs Alpine
setup-alpine
# Or use setup-disk for more control
setup-disk -m sys /dev/sda
```
All required tools (`parted`, `e2fsprogs`, `dosfstools`, `btrfs-progs`, `alpine-conf`, …) are pre-installed.
---
## Default credentials
| Field | Value |
|----------|----------|
| Username | `root` |
| Password | `alpine` |
Change this in `scripts/chroot-setup.sh` before building if needed.
---
## Architecture
```
ISO (hybrid — bootable on USB or CD)
└── isolinux (BIOS boot)
├── vmlinuz-lts
└── initramfs.gz ← entire Alpine system packed as cpio+gzip
├── eth0 DHCP (openrc networking)
├── nebula → /usr/local/bin/nebula
├── /etc/nebula/ (config + certs)
├── OpenSSH (sshd)
└── Alpine install tools
```
The system runs **entirely in RAM** — the host disk is never touched until you explicitly run the installer.
Executable
+2
View File
@@ -0,0 +1,2 @@
docker compose run --rm alpine bash /scripts/create.sh
+15
View File
@@ -0,0 +1,15 @@
services:
alpine:
build: .
devices:
- /dev/loop-control:/dev/loop-control
privileged: true
volumes:
- ./data:/work
- ./scripts:/scripts:ro
- ./nebula:/nebula:ro
- ./output:/output
- /dev:/dev
working_dir: /work
tty: true
stdin_open: true
+66
View File
@@ -0,0 +1,66 @@
pki:
ca: /etc/nebula/ca.crt
cert: /etc/nebula/host.crt
key: /etc/nebula/host.key
static_host_map:
"192.168.9.1": ["167.71.79.60:4242"]
lighthouse:
am_lighthouse: false
interval: 60
hosts:
- "192.168.9.1"
listen:
host: 0.0.0.0
port: 4242
punchy:
punch: true
relay:
am_relay: false
use_relays: true
tun:
disabled: false
dev: nebula2
drop_local_broadcast: false
drop_multicast: false
tx_queue: 500
mtu: 1300
routes:
#- mtu: 8800
# route: 10.0.0.0/16
unsafe_routes:
logging:
level: info
format: text
firewall:
outbound_action: drop
inbound_action: drop
conntrack:
tcp_timeout: 12m
udp_timeout: 3m
default_timeout: 10m
outbound:
- port: any
proto: any
host: any
inbound:
- port: any #ping
proto: icmp
groups:
- admin
- port: 22 #GIT
proto: tcp
groups:
- admin
BIN
View File
Binary file not shown.
+135
View File
@@ -0,0 +1,135 @@
#!/bin/sh
set -eu
# ---------------------------------------------------------------------------
# chroot-setup.sh — Runs inside the Alpine chroot to configure the system
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# 1. APK repositories
# ---------------------------------------------------------------------------
echo "==> Configuring APK repositories"
cat > /etc/apk/repositories <<'EOF'
https://dl-cdn.alpinelinux.org/alpine/latest-stable/main
https://dl-cdn.alpinelinux.org/alpine/latest-stable/community
EOF
apk update
# ---------------------------------------------------------------------------
# 2. Install packages
# ---------------------------------------------------------------------------
echo "==> Installing packages"
apk add --no-cache \
linux-lts \
mkinitfs \
openrc \
busybox \
musl \
libc-utils \
util-linux \
e2fsprogs \
dosfstools \
btrfs-progs \
parted \
sfdisk \
openssh \
dhcpcd \
iproute2 \
iputils \
net-tools \
curl \
wget \
ca-certificates \
vim \
mc \
bind-tools \
nmap \
rsync \
alpine-conf
# ---------------------------------------------------------------------------
# 3. Generate initramfs
# ---------------------------------------------------------------------------
echo "==> Generating initramfs with mkinitfs"
mkinitfs $(ls /lib/modules/ | tail -1)
# ---------------------------------------------------------------------------
# 4. Network configuration
# ---------------------------------------------------------------------------
echo "==> Configuring network interfaces"
mkdir -p /etc/network
cat > /etc/network/interfaces <<'EOF'
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet dhcp
EOF
# ---------------------------------------------------------------------------
# 5. SSH configuration
# ---------------------------------------------------------------------------
echo "==> Configuring sshd"
sed -i \
-e 's/^#*\s*PermitRootLogin.*/PermitRootLogin yes/' \
-e 's/^#*\s*PasswordAuthentication.*/PasswordAuthentication yes/' \
/etc/ssh/sshd_config
# Ensure the directives exist if sed found nothing to replace
grep -q '^PermitRootLogin' /etc/ssh/sshd_config || echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config
grep -q '^PasswordAuthentication' /etc/ssh/sshd_config || echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
echo "==> Generating SSH host keys"
ssh-keygen -A
# ---------------------------------------------------------------------------
# 6. Root password
# ---------------------------------------------------------------------------
echo "==> Setting root password"
echo "root:alpine" | chpasswd
# ---------------------------------------------------------------------------
# 7. Nebula OpenRC service
# ---------------------------------------------------------------------------
echo "==> Creating /etc/init.d/nebula"
cat > /etc/init.d/nebula <<'EOF'
#!/sbin/openrc-run
description="Nebula overlay network"
command="/usr/local/bin/nebula"
command_args="-config /etc/nebula/config.yml"
command_background=true
pidfile="/run/nebula.pid"
depend() {
need net
after networking
}
start_pre() {
checkpath -f -m 0600 -o root:root /run/nebula.pid
}
EOF
chmod +x /etc/init.d/nebula
# ---------------------------------------------------------------------------
# 8. OpenRC service enablement
# ---------------------------------------------------------------------------
echo "==> Enabling services"
rc-update add sshd default
rc-update add networking default
rc-update add nebula default
# Also ensure basic boot services are present
rc-update add devfs sysinit || true
rc-update add dmesg sysinit || true
rc-update add mdev sysinit || true
rc-update add hwdrivers sysinit || true
# ---------------------------------------------------------------------------
# 9. Hostname
# ---------------------------------------------------------------------------
echo "==> Setting hostname to 'rescue'"
echo "rescue" > /etc/hostname
echo "==> chroot-setup.sh complete"
+211
View File
@@ -0,0 +1,211 @@
#!/bin/bash
set -euo pipefail
# ---------------------------------------------------------------------------
# create.sh — Build a bootable Alpine Linux rescue ISO
#
# Run inside the Docker build container:
# docker compose run --rm alpine bash /scripts/create.sh
#
# The resulting ISO is a hybrid image: it can be burned to a CD/DVD or
# written directly to a USB stick with dd.
# ---------------------------------------------------------------------------
ALPINE_MIRROR="https://dl-cdn.alpinelinux.org/alpine"
ALPINE_BRANCH="latest-stable"
ALPINE_ARCH="x86_64"
MINIROOTFS_BASE="${ALPINE_MIRROR}/${ALPINE_BRANCH}/releases/${ALPINE_ARCH}"
ISO="/output/rescue-alpine.iso"
CHROOT="/mnt/alpine-chroot"
ISOROOT="/tmp/isoroot"
mkdir -p /output "$CHROOT" "$ISOROOT"
# ---------------------------------------------------------------------------
# 1. Fetch latest Alpine minirootfs
# ---------------------------------------------------------------------------
echo "==> Fetching latest Alpine minirootfs index"
TARBALL=$(curl -fsSL "${MINIROOTFS_BASE}/" \
| grep -oP 'alpine-minirootfs-[0-9.]+-x86_64\.tar\.gz' \
| sort -V | tail -1)
[[ -z "$TARBALL" ]] && { echo "ERROR: could not determine minirootfs tarball" >&2; exit 1; }
TARBALL_LOCAL="/tmp/${TARBALL}"
if [[ ! -f "$TARBALL_LOCAL" ]]; then
echo "==> Downloading ${MINIROOTFS_BASE}/${TARBALL}"
curl -fsSL -o "$TARBALL_LOCAL" "${MINIROOTFS_BASE}/${TARBALL}"
else
echo "==> Using cached ${TARBALL_LOCAL}"
fi
# ---------------------------------------------------------------------------
# 2. Extract minirootfs into chroot
# ---------------------------------------------------------------------------
echo "==> Extracting minirootfs into ${CHROOT}"
tar -xzf "$TARBALL_LOCAL" -C "$CHROOT"
# ---------------------------------------------------------------------------
# 3. Copy Nebula binary and config into chroot
# ---------------------------------------------------------------------------
echo "==> Installing Nebula binary"
mkdir -p "${CHROOT}/usr/local/bin"
cp /nebula/nebula "${CHROOT}/usr/local/bin/nebula"
chmod +x "${CHROOT}/usr/local/bin/nebula"
echo "==> Installing Nebula config and certificates"
mkdir -p "${CHROOT}/etc/nebula"
cp /nebula/config.yml "${CHROOT}/etc/nebula/config.yml"
for f in /nebula/*.crt /nebula/*.key; do
[[ -f "$f" ]] && cp "$f" "${CHROOT}/etc/nebula/"
done
# ---------------------------------------------------------------------------
# 4. Bind-mount virtual filesystems for chroot
# ---------------------------------------------------------------------------
echo "==> Bind-mounting virtual filesystems"
mount --bind /dev "${CHROOT}/dev"
mount --bind /proc "${CHROOT}/proc"
mount --bind /sys "${CHROOT}/sys"
mkdir -p "${CHROOT}/run"
mount --bind /run "${CHROOT}/run"
cp /etc/resolv.conf "${CHROOT}/etc/resolv.conf"
# Make scripts available inside chroot
mkdir -p "${CHROOT}/scripts"
mount --bind /scripts "${CHROOT}/scripts"
# ---------------------------------------------------------------------------
# 5. Run in-chroot configuration
# ---------------------------------------------------------------------------
echo "==> Running chroot-setup.sh inside chroot"
chroot "$CHROOT" /bin/sh /scripts/chroot-setup.sh
# ---------------------------------------------------------------------------
# 6. Unmount virtual filesystems
# ---------------------------------------------------------------------------
echo "==> Unmounting virtual filesystems"
umount "${CHROOT}/scripts" || true
umount "${CHROOT}/run" || true
umount "${CHROOT}/sys" || true
umount "${CHROOT}/proc" || true
umount "${CHROOT}/dev" || true
# ---------------------------------------------------------------------------
# 7. Pack the full system as a squashfs (xz compressed)
# ---------------------------------------------------------------------------
echo "==> Creating squashfs from chroot (xz — this may take a while)"
mksquashfs "$CHROOT" "${ISOROOT}/rootfs.squashfs" \
-comp xz -Xdict-size 100% -noappend \
-e boot/initramfs-lts 2>/dev/null || \
mksquashfs "$CHROOT" "${ISOROOT}/rootfs.squashfs" \
-comp xz -noappend
echo " squashfs size: $(du -sh "${ISOROOT}/rootfs.squashfs" | cut -f1)"
# ---------------------------------------------------------------------------
# 8. Copy kernel into ISO root
# ---------------------------------------------------------------------------
echo "==> Copying kernel"
cp "${CHROOT}/boot/vmlinuz-lts" "${ISOROOT}/vmlinuz"
# ---------------------------------------------------------------------------
# 9. Build tiny initramfs: busybox + init script that mounts the squashfs
# ---------------------------------------------------------------------------
echo "==> Building tiny initramfs"
INITRD="/tmp/initrd"
rm -rf "$INITRD"
mkdir -p "${INITRD}"/{bin,dev,proc,sys,mnt/boot,mnt/squash,mnt/upper,mnt/work,mnt/newroot}
# Static busybox — no external library deps
BBOX=$(find /bin /usr/bin /usr/sbin -name 'busybox.static' -o -name 'busybox' 2>/dev/null \
| xargs -r file 2>/dev/null | grep -i 'statically linked' | cut -d: -f1 | head -1)
[[ -z "$BBOX" ]] && { echo "ERROR: cannot find a statically linked busybox" >&2; exit 1; }
echo " using static busybox: $BBOX"
cp "$BBOX" "${INITRD}/bin/busybox"
chmod +x "${INITRD}/bin/busybox"
for cmd in sh mount umount mkdir mknod switch_root mdev sleep; do
ln -sf busybox "${INITRD}/bin/${cmd}"
done
cat > "${INITRD}/init" <<'INIT_EOF'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev 2>/dev/null || mdev -s
# Scan block devices for the ISO containing rootfs.squashfs
found=0
for try in 1 2 3 4 5; do
for dev in /dev/sr0 /dev/sda /dev/sda1 /dev/sdb /dev/sdb1 /dev/sdc /dev/sdc1; do
[ -b "$dev" ] || continue
mount -t iso9660 -o ro "$dev" /mnt/boot 2>/dev/null || \
mount -o ro "$dev" /mnt/boot 2>/dev/null || continue
if [ -f /mnt/boot/rootfs.squashfs ]; then
found=1
break 2
fi
umount /mnt/boot 2>/dev/null
done
sleep 1
done
if [ "$found" != "1" ]; then
echo "ERROR: rootfs.squashfs not found — dropping to shell"
exec sh
fi
mount -t squashfs -o ro /mnt/boot/rootfs.squashfs /mnt/squash
mount -t tmpfs tmpfs /mnt/upper
mkdir -p /mnt/upper/rw /mnt/upper/work
mount -t overlay overlay \
-o lowerdir=/mnt/squash,upperdir=/mnt/upper/rw,workdir=/mnt/upper/work \
/mnt/newroot
exec switch_root /mnt/newroot /sbin/init
INIT_EOF
chmod +x "${INITRD}/init"
( cd "$INITRD" && find . | cpio --quiet -oH newc | gzip -9 ) > "${ISOROOT}/initramfs.gz"
echo " initramfs size: $(du -sh "${ISOROOT}/initramfs.gz" | cut -f1)"
echo " initramfs contents:"
zcat "${ISOROOT}/initramfs.gz" | cpio -t 2>/dev/null
# ---------------------------------------------------------------------------
# 10. Build ISO with grub-mkrescue (BIOS + UEFI hybrid)
# grub-mkrescue uses its own file loader — no BIOS INT 13h size limits.
# ---------------------------------------------------------------------------
echo "==> Writing GRUB config"
mkdir -p "${ISOROOT}/boot/grub"
cat > "${ISOROOT}/boot/grub/grub.cfg" <<'EOF'
set timeout=5
set default=0
menuentry "Alpine Linux USB Rescue" {
linux /vmlinuz quiet init=/sbin/init
initrd /initramfs.gz
}
EOF
echo "==> Building hybrid ISO with grub-mkrescue: ${ISO}"
grub-mkrescue -o "$ISO" "$ISOROOT" -- -V ALPINE_RESCUE
# ---------------------------------------------------------------------------
# Done
# ---------------------------------------------------------------------------
ISO_SIZE=$(du -sh "$ISO" | cut -f1)
echo ""
echo "================================================================"
echo " Build complete!"
echo " ISO : ${ISO} (${ISO_SIZE})"
echo ""
echo " Burn to USB:"
echo " dd if=${ISO} of=/dev/sdX bs=4M status=progress && sync"
echo ""
echo " Burn to CD/DVD:"
echo " cdrecord dev=/dev/sr0 ${ISO}"
echo ""
echo " Default login : root / alpine"
echo " Nebula tunnel : auto-starts on boot (needs certs in nebula/)"
echo "================================================================"
+3 -1
View File
@@ -15,7 +15,9 @@ RUN apk add --no-cache \
e2fsprogs \
screen \
rsync \
device-mapper
sshfs \
device-mapper \
borgbackup
WORKDIR /root
+5
View File
@@ -29,6 +29,11 @@ Host meppel
User 4server
IdentityFile /app/host_vars/meppel/meppel
Host sydney
Hostname 192.168.9.22
User 4server
IdentityFile /app/host_vars/sydney/sydney
Host saopaulo
Hostname 192.168.9.11
User 4server
+1
View File
@@ -13,4 +13,5 @@
192.168.9.11 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDc6J024++J25z2ZMXQ3Vim+mLVIVs+cZngv8DmtEDfrT/Ptl7+F2IqXRNcZuq1YTjEZO8eQg27iIemPoPT3xa8=
192.168.9.21 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMFuf87ngDaF8D0kzVzzB953ji6ptg/V9t+WPac+DAjO
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=
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
set -e
export BORG_REPO="ssh://e7e9h45y@e7e9h45y.repo.borgbase.com/./repo"
export BORG_RSH="ssh -i ~/.ssh/borg-backup"
echo "Applying retention policy..."
borg prune \
--keep-daily=7 \
--keep-weekly=8 \
--keep-monthly=12 \
--stats
echo "Done: pruning complete"
Regular → Executable
View File
+1 -1
View File
@@ -13,7 +13,7 @@ source /4server/sbin/helpers
get_contract_info
export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
echo "PASSWORD $ODOO_DB_PASSWORD"
+1 -2
View File
@@ -13,11 +13,10 @@ source /4server/sbin/helpers
get_contract_info
export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
#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"
+182 -75
View File
@@ -1,30 +1,27 @@
#!/usr/bin/env python3
from fastapi import FastAPI, HTTPException, Depends, Response
from fastapi.security.api_key import APIKeyHeader
from fastapi.responses import RedirectResponse, PlainTextResponse
from pydantic import BaseModel
import psutil
import json
import os
import re
import sqlite3
import subprocess
import os
import uvicorn
from typing import Dict, Any, Optional
from datetime import datetime
import json
import re
from collections import deque
import time
from collections import deque
from datetime import datetime
from pathlib import Path
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.8"
VERSION = "API: 0.0.9"
# ---------------------- FastAPI App ----------------------
app = FastAPI()
@@ -57,7 +54,7 @@ def init_db():
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
cursor.execute('''
cursor.execute("""
CREATE TABLE IF NOT EXISTS containers (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
UUID CHAR(50) UNIQUE,
@@ -74,9 +71,19 @@ def init_db():
created DATE,
bump DATE,
secret TEXT,
contract 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()
@@ -101,13 +108,12 @@ class ContractItem(BaseModel):
product_id: int
features: Dict[str, Any]
class ContainerModel(BaseModel):
UUID: str
email: Optional[str] = None
expires: Optional[str] = None
tags: Optional[str] = None
env: Optional[Dict[str, Any]] = None
affiliate: Optional[str] = None
image: Optional[str] = None
history: Optional[str] = None
comment: Optional[str] = None
@@ -115,37 +121,57 @@ class ContainerModel(BaseModel):
status: Optional[str] = None
created: Optional[str] = None
bump: Optional[str] = None
secret: Optional[Dict[str, Any]] = None
contract: 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
secret_list: Optional[str] = None
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")
from fastapi import FastAPI, Depends
import json
from typing import Any, Dict, Optional
from fastapi import Depends, FastAPI
from fastapi.responses import RedirectResponse
from pydantic import BaseModel
from typing import Optional, Dict, Any
import json
app = FastAPI()
@@ -158,25 +184,43 @@ def redirect_to_odoo():
@app.post("/container/update", dependencies=[Depends(verify_api_key)])
def update_container(request: ContainerModel):
# Convert dict fields to JSON strings
env_str = json.dumps(request.env) if isinstance(request.env, dict) else None
secret_str = json.dumps(request.secret) if isinstance(request.secret, dict) else None
contract_str = json.dumps(request.contract) if isinstance(request.contract, dict) else None
# Fetch existing record
existing = execute_db("SELECT * FROM containers WHERE UUID = ?", (request.UUID,), fetch=True)
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, env, affiliate, image, history,
comment, domains, status, created, bump, secret, contract)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
request.UUID, request.email, request.expires, request.tags, env_str,
request.affiliate, request.image, request.history, request.comment,
request.domains, request.status, request.created, request.bump, secret_str, contract_str
))
execute_db(
"""
INSERT INTO containers (UUID, 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
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,
request.secret_list,
request.env_list,
),
)
return {"UUID": request.UUID, "status": "created"}
# Existing record found, do partial update
@@ -190,9 +234,6 @@ def update_container(request: ContainerModel):
continue
value = getattr(request, field)
if value is not None:
if field in ["env", "secret", "contract"]:
value = json.dumps(value)
updates[field] = value
params.append(value)
@@ -201,7 +242,11 @@ def update_container(request: ContainerModel):
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": "updated",
"fields_updated": list(updates.keys()),
}
return {"UUID": request.UUID, "status": "no_change"}
@@ -211,6 +256,11 @@ 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:
@@ -222,7 +272,9 @@ def stop_container(request: UUIDRequest):
@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)
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])}
@@ -232,8 +284,26 @@ def nuke_container(request: UUIDRequest):
def info_container(request: Optional[UUIDRequest] = None):
# Fields to select
fields = [
"ID", "UUID", "email", "expires", "tags", "env", "affiliate",
"image", "history", "comment", "domains", "status", "created", "contract"
"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)
@@ -242,13 +312,10 @@ def info_container(request: Optional[UUIDRequest] = None):
rows = execute_db(
f"SELECT {field_str} FROM containers WHERE UUID=?",
(str(request.UUID),),
fetch=True
fetch=True,
)
else:
rows = execute_db(
f"SELECT {field_str} FROM containers",
fetch=True
)
rows = execute_db(f"SELECT {field_str} FROM containers", fetch=True)
# Map rows to dicts safely
containers = []
@@ -265,6 +332,7 @@ def info_container(request: Optional[UUIDRequest] = None):
return n8n_items
@app.post("/container/bump", dependencies=[Depends(verify_api_key)])
def bump_container(request: UUIDRequest):
today = datetime.utcnow().strftime("%Y-%m-%d")
@@ -275,10 +343,16 @@ def bump_container(request: UUIDRequest):
@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}}"
])
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}
@@ -286,7 +360,10 @@ def container_quota(request: UUIDRequest):
# ---------------------- 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")
return Response(
content=run_command([f"{BIN_PATH}/getContainers"]),
media_type="application/json",
)
@app.get("/system/images", dependencies=[Depends(verify_api_key)])
@@ -294,6 +371,7 @@ 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")
@@ -307,12 +385,15 @@ def get_cpu_log():
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"]
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()
@@ -328,10 +409,14 @@ def get_system_info():
"latest_bump": bump_dates,
"version": VERSION,
"resources": {
"memory": {"total": mem.total, "available": mem.available, "used": mem.used},
"memory": {
"total": mem.total,
"available": mem.available,
"used": mem.used,
},
"disk": {"total": disk.total, "used": disk.used, "free": disk.free},
"cpu_count": cpu_count
}
"cpu_count": cpu_count,
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@@ -342,6 +427,13 @@ 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:
@@ -382,7 +474,10 @@ def audit_odoo(uuid: str):
@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.")
raise HTTPException(
status_code=400,
detail="Invalid UUID format. Only numbers, letters a-f, and '-' are allowed.",
)
BASE_LOG_DIR = "/4server/data"
@@ -396,12 +491,12 @@ async def get_odoo_log_summary(uuid: str):
# --- 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"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"),
]
@@ -424,7 +519,9 @@ async def get_odoo_log_summary(uuid: str):
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)]
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)
@@ -437,6 +534,7 @@ async def get_odoo_log_summary(uuid: str):
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):
@@ -447,10 +545,13 @@ def backup_import(request: ImportRequest):
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")
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")
@@ -460,7 +561,15 @@ def backup_move(request: MoveRequest):
# For Windows, use: command = ["move", request.source, request.destination]
output = run_command(command)
return {"message": f"Moved {request.source} to {request.destination}", "output": output}
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 ----------------------
@@ -469,5 +578,3 @@ if __name__ == "__main__":
init_db()
time.sleep(25)
uvicorn.run(app, host="10.5.0.1", port=8888)
-48
View File
@@ -1,48 +0,0 @@
#!/bin/bash
# Backup Odoo database script
# Author: Your Name
# Description: Dumps Odoo DB, manages backups, and sets permissions
set -euo pipefail # Fail on error, undefined variables, and pipe errors
# Load helper functions
source /4server/sbin/helpers
# Get contract info
get_contract_info
# Export Odoo database password
export ODOO_DB_PASSWORD
ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
# Display basic info
echo "UUID: $UUID"
echo "Backup slots: $BACKUP_SLOTS"
# Create backup filename
FILENAME="$(date +"%Y%m%d_%H%M").zip"
BACKUP_DIR="/BACKUP/$UUID"
# Ensure backup directory exists
mkdir -p "$BACKUP_DIR"
# Perform database dump using docker
doas docker exec "$UUID" odoo db \
--db_host beedb \
-r "$UUID" \
-w "$ODOO_DB_PASSWORD" \
--data-dir /home/odoo/.local/share/Odoo/ \
dump "$UUID" "/mnt/backup/$FILENAME"
# Set permissions for backup files
doas chmod 600 "$BACKUP_DIR"/*
doas docker exec "$UUID" chown odoo:odoo -R /mnt/backup
# 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
echo "Deleting old backup: $file"
doas rm -f "$file"
done
-48
View File
@@ -1,48 +0,0 @@
#!/bin/bash
# Backup Odoo database script
# Author: Your Name
# Description: Dumps Odoo DB, manages backups, and sets permissions
set -euo pipefail # Fail on error, undefined variables, and pipe errors
# Load helper functions
source /4server/sbin/helpers
# Get contract info
get_contract_info
# Export Odoo database password
export ODOO_DB_PASSWORD
ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
# Display basic info
echo "UUID: $UUID"
echo "Backup slots: $BACKUP_SLOTS"
# Create backup filename
FILENAME="$(date +"%Y%m%d_%H%M").zip"
BACKUP_DIR="/BACKUP/$UUID"
# Ensure backup directory exists
mkdir -p "$BACKUP_DIR"
# Perform database dump using docker
doas docker exec "$UUID" odoo db \
--db_host beedb \
-r "$UUID" \
-w "$ODOO_DB_PASSWORD" \
--data-dir /home/odoo/.local/share/Odoo/ \
dump "$UUID" "/mnt/backup/$FILENAME"
# Set permissions for backup files
doas chmod 600 "$BACKUP_DIR"/*
doas docker exec "$UUID" chown odoo:odoo -R /mnt/backup
# 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
echo "Deleting old backup: $file"
doas rm -f "$file"
done
-48
View File
@@ -1,48 +0,0 @@
#!/bin/bash
# Backup Odoo database script
# Author: Your Name
# Description: Dumps Odoo DB, manages backups, and sets permissions
set -euo pipefail # Fail on error, undefined variables, and pipe errors
# Load helper functions
source /4server/sbin/helpers
# Get contract info
get_contract_info
# Export Odoo database password
export ODOO_DB_PASSWORD
ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
# Display basic info
echo "UUID: $UUID"
echo "Backup slots: $BACKUP_SLOTS"
# Create backup filename
FILENAME="$(date +"%Y%m%d_%H%M").zip"
BACKUP_DIR="/BACKUP/$UUID"
# Ensure backup directory exists
mkdir -p "$BACKUP_DIR"
# Perform database dump using docker
doas docker exec "$UUID" odoo db \
--db_host beedb \
-r "$UUID" \
-w "$ODOO_DB_PASSWORD" \
--data-dir /home/odoo/.local/share/Odoo/ \
dump "$UUID" "/mnt/backup/$FILENAME"
# Set permissions for backup files
doas chmod 600 "$BACKUP_DIR"/*
doas docker exec "$UUID" chown odoo:odoo -R /mnt/backup
# 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
echo "Deleting old backup: $file"
doas rm -f "$file"
done
+3 -3
View File
@@ -26,13 +26,13 @@ case "$SECOND_PART" in
"$BIN_PATH/backup/N8N"
;;
002)
"$BIN_PATH/backup/ODOO_18"
"$BIN_PATH/backup/ODOO"
;;
003)
"$BIN_PATH/backup/ODOO_19"
"$BIN_PATH/backup/ODOO"
;;
004)
"$BIN_PATH/backup/ODOO_17"
"$BIN_PATH/backup/ODOO"
;;
*)
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# ssh -i ~/.ssh/borg-backup e7e9h45y@e7e9h45y.repo.borgbase.com
set -e
export BORG_REPO="ssh://e7e9h45y@e7e9h45y.repo.borgbase.com/./repo"
export BORG_RSH="ssh -i ~/.ssh/borg-backup"
export BORG_PASSPHRASE="Airbus12"
HOSTNAME=$(hostname)
DATE=$(date +%Y-%m-%d_%H-%M-%S)
ARCHIVE_NAME="${HOSTNAME}-${DATE}"
# Resolve symlinks externally and feed real paths to borg via --paths-from-stdin.
# - First find: locates all current* symlinks under /BACKUP and resolves them with realpath.
# - Second find: locates all regular current* files under /BACKUP (non-symlinks).
# - sort -u: deduplicates in case a resolved symlink target overlaps with a regular file.
{
find /BACKUP -name 'current*' -type l -print0 | xargs -0 -I {} realpath {}
find /BACKUP -name 'current*' ! -type l
} | sort -u | borg create \
--verbose \
--stats \
--compression lz4 \
--paths-from-stdin \
::$ARCHIVE_NAME
echo "Backup completed: $ARCHIVE_NAME"
+36 -23
View File
@@ -5,34 +5,47 @@ 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() {
DB_PATH="/4server/data/contracts.db"
echo "get_contract_info $UUID"
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;
")
while IFS="=" read -r key value; do
if [ -n "$key" ]; then
export "$key=$value"
# 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
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=' || secret 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';
")
eval $(echo "$ENV" | jq -r 'to_entries | .[] | "export \(.key | ascii_upcase)=\(.value)"')
export ODOO_DB_PASSWORD=$psql
}
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
sqlite3 /4server/data/contracts.db <<EOF
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
+1 -3
View File
@@ -12,7 +12,7 @@ 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')
#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
BASEURL="${BASEURL:-/4server/data/$UUID}"
BACKUPURL="/4backup/$UUID"
@@ -52,5 +52,3 @@ END
EOF
echo "✅ Database '$UUID' and user '$ODOO_DB_USER' removed (if they existed)."
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
# Usage: ./odoo_passwd.sh <container_name>
CONTAINER=$1
if [ -z "$CONTAINER" ]; then
echo "Usage: $0 <docker_container>"
exit 1
fi
# Step 1: Get users list
echo "Fetching users..."
USERS=$(doas docker exec -i "$CONTAINER" bash -c \
'PGPASSWORD="$PASSWORD" psql -h "$HOST" -U "$USER" -At -F"," -c "SELECT id, login FROM res_users ORDER BY id;"')
# Step 2: Display users
echo "Select a user:"
IFS=$'\n'
select USER_LINE in $USERS; do
if [ -n "$USER_LINE" ]; then
break
fi
done
USER_ID=$(echo "$USER_LINE" | cut -d',' -f1)
USER_LOGIN=$(echo "$USER_LINE" | cut -d',' -f2)
echo "Selected user: $USER_LOGIN (ID: $USER_ID)"
# Step 3: Ask for new password
read -s -p "Enter new password: " NEW_PASS
echo
read -s -p "Confirm password: " CONFIRM_PASS
echo
if [ "$NEW_PASS" != "$CONFIRM_PASS" ]; then
echo "Passwords do not match!"
exit 1
fi
# Step 4: Generate Odoo-compatible hash using Python inside container
HASH=$(doas docker exec -i "$CONTAINER" python3 - <<EOF
from passlib.context import CryptContext
ctx = CryptContext(schemes=['pbkdf2_sha512'])
print(ctx.hash("$NEW_PASS"))
EOF
)
echo "Updating password..."
# Step 5: Update DB
doas docker exec -i "$CONTAINER" bash -c \
"PGPASSWORD=\"$PASSWORD\" psql -h \"$HOST\" -U \"$USER\" -c \"UPDATE res_users SET password='$HASH' WHERE id=$USER_ID;\""
echo "Password updated successfully for $USER_LOGIN"
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
doas docker exec -it $1 bash -c 'PGPASSWORD="$PASSWORD" psql -h "$HOST" -U "$USER"'
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# Check that a search string was provided
if [ -z "$1" ]; then
echo "Usage: $0 <search_string>"
exit 1
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%';")
# Loop through each UUID
for uuid in $uuids; do
# Check if a container with this name is running
if doas docker ps --format '{{.Names}}' | grep -qx "$uuid"; then
echo "Restarting $uuid"
/4server/sbin/startContainer $uuid
fi
done
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
doas docker exec -it $1 bash -c 'PGPASSWORD="$PASSWORD" psql -h "$HOST" -U "$USER"'
+3 -3
View File
@@ -10,10 +10,10 @@ UUID="${UUID:-default}"
BRANCH="${BRANCH:-release}"
ODOO_DB_USER="${UUID}"
export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
echo "ENV: $HDD $DOMAIN_COUNT $BACKUP_SLOTS $CONTAINERDBID"
echo "ENV: $HDD $DOMAIN_SLOTS $BACKUP_SLOTS $CONTAINERDBID"
echo "DBID: $CONTAINERDBID"
BASEURL="${BASEURL:-/4server/data/$UUID}"
@@ -101,7 +101,7 @@ docker run -d --name "$UUID" \
-e PASSWORD="$ODOO_DB_PASSWORD" \
-e UUID="$UUID" \
-e HDD="$HDD" \
-e DOMAIN_COUNT="$DOMAIN_COUNT" \
-e DOMAIN_SLOTS="$DOMAIN_SLOTS" \
-e BACKUP_SLOTS="$BACKUP_SLOTS" \
-e WORKER="$WORKER" \
-e GIT="$GIT" \
+3 -3
View File
@@ -10,10 +10,10 @@ UUID="${UUID:-default}"
BRANCH="${BRANCH:-release}"
ODOO_DB_USER="${UUID}"
export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
echo "ENV: $HDD $DOMAIN_COUNT $BACKUP_SLOTS $CONTAINERDBID"
echo "ENV: $HDD $DOMAIN_SLOTS $BACKUP_SLOTS $CONTAINERDBID"
echo "DBID: $CONTAINERDBID"
BASEURL="${BASEURL:-/4server/data/$UUID}"
@@ -101,7 +101,7 @@ docker run -d --name "$UUID" \
-e PASSWORD="$ODOO_DB_PASSWORD" \
-e UUID="$UUID" \
-e HDD="$HDD" \
-e DOMAIN_COUNT="$DOMAIN_COUNT" \
-e DOMAIN_SLOTS="$DOMAIN_SLOTS" \
-e BACKUP_SLOTS="$BACKUP_SLOTS" \
-e WORKER="$WORKER" \
-e GIT="$GIT" \
+4 -4
View File
@@ -10,10 +10,10 @@ UUID="${UUID:-default}"
BRANCH="${BRANCH:-release}"
ODOO_DB_USER="${UUID}"
export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql')
echo "***** $ODOO_DB_PASSWORD"
echo "ENV: $HDD $DOMAIN_COUNT $BACKUP_SLOTS $CONTAINERDBID"
echo "ENV: $HDD $DOMAIN_SLOTS $BACKUP_SLOTS $CONTAINERDBID"
echo "DBID: $CONTAINERDBID"
BASEURL="${BASEURL:-/4server/data/$UUID}"
@@ -104,7 +104,7 @@ docker run -d --name "$UUID" \
-e PASSWORD="$ODOO_DB_PASSWORD" \
-e UUID="$UUID" \
-e HDD="$HDD" \
-e DOMAIN_COUNT="$DOMAIN_COUNT" \
-e DOMAIN_SLOTS="$DOMAIN_SLOTS" \
-e BACKUP_SLOTS="$BACKUP_SLOTS" \
-e WORKER="$WORKER" \
-e GIT="$GIT" \
+9 -6
View File
@@ -15,16 +15,20 @@ if [[ -z "$UUID" ]]; then
exit 1
fi
if [[ ! "$UUID" =~ ^[0-9a-fA-F-]+$ ]]; then
echo "ERROR: Invalid UUID format: $UUID"
exit 1
fi
DOMAIN_FILE="/4server/data/$UUID/etc/domain"
DB_FILE="/4server/data/contracts.db"
if [ -f "$DOMAIN_FILE" ]; then
DOMAINS=$(paste -sd "," "$DOMAIN_FILE")
sqlite3 "$DB_FILE" <<SQL
UPDATE containers
SET domains='$DOMAINS'
WHERE UUID='$UUID';
SQL
# 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';"
fi
@@ -59,4 +63,3 @@ case "$SECOND_PART" in
exit 2
;;
esac
+1 -2
View File
@@ -1,7 +1,6 @@
# ~/.bashrc
clear
echo "Server {{HOSTNAME}}"
echo "Server {{HOSTNAME}} IP: $(ip route get 1.1.1.1 | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}')"
export PS1="\[\e[32m\]\h:\w\$\[\e[0m\] "
df -h .
+1 -2
View File
@@ -25,7 +25,7 @@ template templates/.profile /home/4server/.profile
### PACKAGES
template templates/repositories /etc/apk/repositories
rex "doas apk update && doas apk upgrade"
rex doas apk add iperf linux-lts openssh ufw python3 build-base python3-dev linux-headers py3-pip gcc g++ musl-dev libffi-dev make jq rsync mc vim docker docker-compose htop linux-lts sqlite bash postgresql16-client
rex doas apk add sshfs borgbackup iperf linux-lts openssh ufw python3 build-base python3-dev linux-headers py3-pip gcc g++ musl-dev libffi-dev make jq rsync mc vim docker docker-compose htop linux-lts sqlite bash postgresql16-client
rex doas pip install --root-user-action ignore --break-system-packages --no-cache-dir "uvicorn[standard]" fastapi pydantic psutil gdown
@@ -94,4 +94,3 @@ prsync -h "/app/host_vars/hosts" -avz ./etc/traefik/certs/* /4server/data/traefi
template templates/docker-compose.yml /4server/docker-compose.yml
rex doas docker-compose -f /4server/docker-compose.yml up -d --force-recreate
+4
View File
@@ -2,8 +2,12 @@
echo "Running prsync ./sbin"
prsync -h "/app/host_vars/hosts" -avz ./sbin/ /4server/sbin/
template templates/.profile /home/4server/.profile
rex doas rc-service api restart
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*
Binary file not shown.
Executable
+2
View File
@@ -0,0 +1,2 @@
sudo borg mount ssh://e7e9h45y@e7e9h45y.repo.borgbase.com/./repo /mnt