commit 3deb54f5aa0f8257a5cd2e437e66ba8bdd17a714 Author: oliver Date: Tue Apr 21 17:29:54 2026 -0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..318f71a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +alpine.qcow2 +tmp/ +exchange/ +backup/ diff --git a/alpine/.bashrc b/alpine/.bashrc new file mode 100644 index 0000000..f8aa567 --- /dev/null +++ b/alpine/.bashrc @@ -0,0 +1,17 @@ +# ~/.bashrc + +echo "command: mount_volume " +echo "alias: set_prod" + +export hosts_file="/app/hosts.dev" +export host_vars_dir="/app/host_vars/" + +export PS1="\[\e[32m\]\h:\w\$\[\e[0m\] " +df -h . + +set_prod() { + export hosts_file="/app/hosts.all" + export host_vars_dir="/app/host_vars/vault/" + echo "LIVE MODE ENABLED !!! " +} +clear diff --git a/alpine/Dockerfile b/alpine/Dockerfile new file mode 100644 index 0000000..59bed44 --- /dev/null +++ b/alpine/Dockerfile @@ -0,0 +1,45 @@ +FROM alpine:latest + +RUN apk add --no-cache \ + bash \ + git \ + neovim \ + python3 \ + py3-pip \ + py3-virtualenv \ + openssh \ + cryptsetup \ + util-linux \ + pssh \ + mc \ + e2fsprogs \ + screen \ + rsync \ + sshfs \ + device-mapper \ + borgbackup + + +WORKDIR /root + +RUN ln -sf /usr/bin/nvim /usr/bin/vim + +COPY known_hosts /root/.ssh/ +COPY config /root/.ssh/ + +RUN ssh-keygen -t rsa -b 4096 -f /root/.ssh/id_rsa -N "" && \ + chmod 600 /root/.ssh/id_rsa && \ + chmod 600 /root/.ssh/config + +COPY rex /usr/bin/ +COPY template /usr/bin/ +COPY create_volume /usr/bin/ +COPY mount_volume /usr/bin/ + +COPY .bashrc /root/.bashrc + +WORKDIR /app + +CMD ["bash"] + + diff --git a/alpine/config b/alpine/config new file mode 100644 index 0000000..b24a9c1 --- /dev/null +++ b/alpine/config @@ -0,0 +1,40 @@ +#Host dev +# Hostname dev +# User oliver +# Port 2222 + +Host dev + Hostname dev + User 4server + Port 2222 + IdentityFile /app/host_vars/dev/dev + +Host manchester + Hostname 192.168.9.20 + User 4server + IdentityFile /app/host_vars/manchester/manchester + +Host boston + Hostname 192.168.9.16 + User 4server + IdentityFile /app/host_vars/boston/boston + +Host mumbai + Hostname 192.168.9.17 + User 4server + IdentityFile /app/host_vars/mumbai/mumbai + +Host meppel + Hostname 192.168.9.21 + 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 + IdentityFile /app/host_vars/saopaulo/saopaulo diff --git a/alpine/create_volume b/alpine/create_volume new file mode 100755 index 0000000..853b931 --- /dev/null +++ b/alpine/create_volume @@ -0,0 +1,23 @@ +#!/bin/bash + +set -e + +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +FILE="/data/$1" +MAPPER_NAME="encrypted_volume" +MOUNT_POINT="/mnt/${MAPPER_NAME}" +dd if=/dev/zero of="$FILE" bs=1M count=10 + + +echo "Setting up LUKS on $FILE..." +cryptsetup luksFormat "$FILE" +cryptsetup close "$MAPPER_NAME" || true + +cryptsetup open "$FILE" "$MAPPER_NAME" + +mkfs.ext4 /dev/mapper/"$MAPPER_NAME" + diff --git a/alpine/dpush_kann_das_weg b/alpine/dpush_kann_das_weg new file mode 100644 index 0000000..3927bbd --- /dev/null +++ b/alpine/dpush_kann_das_weg @@ -0,0 +1,26 @@ +#!/bin/bash + +set -euo pipefail + +if [ $# -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +LOCAL_FILE="$1" +REMOTE_PATH="$2" + +if [ ! -f "$LOCAL_FILE" ]; then + echo "Error: Local file '$LOCAL_FILE' not found." + exit 1 +fi + +if [ ! -f "$hosts_file" ]; then + echo "Error: Hosts file '$hosts_file' not found." + exit 1 +fi + +# Push the file content using tee with doas +echo "Pushing $LOCAL_FILE to $REMOTE_PATH on all hosts in $hosts_file" +pssh -h "$hosts_file" -l "$USER" -i "doas tee \"$REMOTE_PATH\" > /dev/null" < "$LOCAL_FILE" + diff --git a/alpine/known_hosts b/alpine/known_hosts new file mode 100644 index 0000000..09e0e30 --- /dev/null +++ b/alpine/known_hosts @@ -0,0 +1,17 @@ +192.168.9.17 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINmL6mEsSSpEWVICaXSFOd/Z1i6S5zn+B2q/ZKlqLocb +192.168.9.17 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDhrtXNi+1udDp34CDDmMWNqUTfsn7F6XVv4xtGSmaUbREt0A56RIqo6EMegLSN+9J2z+x85ZxxNRoQc2kAamB9/6v4poDyMJKhqDJ4qimyboOSPI1FqAojmeSCGXmSU6bk5lYDKbkQsINeaGiQ86inv8Rgar4EV834Sp1HEGUZBksn7+xPL4MkmMWfcykrELgU8dnsJYiRJKWxU2d4UTWorBWtcICg1SJndB2QBncYT3dOEl+283HDBnKU6w2o7OhP+Tz+vTyVxtqZ7QF9Z/e7et9zFXnjsTopnkCVBFt8/esKPmgesiY+j0COx9aaMD6Ks2ssa8UqYHybmHwO8o5vfvcCR3ofZhhkGRnAXijd1D6bK3uXJR13PYnlPjiNQA0Lvf2YrOMDBDmRk5j8zQRHXe7oygmYz49z+lrR83VBIFVJkTt/GELQJ6kjQdSHdRinL54sVPl407cdmzBdwenY6vBCcN2UIDu4ohubqr0QH//PaXbtj4pEEExzurdq99U= +192.168.9.17 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJPErwjui8jQC3Tkql+G6PsDd16pe7wm2vEhs/qYaEGSM+L7lW7XiXVMPhpCd4UrRVFFibjyfZsxGCv8LmctqIs= +192.168.9.20 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG9kTGcpJCCoUDyPmhWx9hWTLhXr1SqpYFSW50l1zFsa +192.168.9.20 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCoTci1WePcd1dFfZbS6C6utZ0ni9aGuvTc1YpORg1fnEF8IZEB9FzEb4vGIEMqyYZq9HsVXJdcBTbDMtAqYlx3IqIMRsS8zFnnlqM+u0OKgt4WLRDfvKY3eGNilm3gplNBwdnh0dHafRy+CSHMvC/fcnkjFFfj8nQ8rdOoE/qPd1gH6l0+mcGWpvH78dK3eU03M964aAxzrBqUhU3y6lYSinI0EZHf6mniOSVBJ27ulImisb+z14row2HgPvjnIbp89QOP9b7b0lzVQ5VdmikAMVtsbKARyZi7Tn1ZrTZqGBx5SnuGKxG0Wl3gLBAL5pTHV7XrmCUJOkUdnoTNCxjsV6Qo6SBiKHYqwJPGry4MbT2bOdFIkw5YVJKEU+Q6txMy1axcZ2ACtcWRiUaMHoVsfbFiQv/Llgc7fsmD8NCMNArD80N4YTD95Di+Ch8FZ0Cf5sfV/6NnvER2hivr+9+Q84UVwA1RyhGrPPS9emMixIod5UgtzO9vmJcpJrmiCLk= +192.168.9.20 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLBKg3hH/hdu4o51Ubg831HsUEedZp0VNAtpmY5QN9RTOJJaYxUJBo3ZbyrLMEWCvVTAT4nfwUAJhaf78l8Lk60= +192.168.9.16 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKSSlK1dlG8Qszv7sy0DAuNu3bsH7c1eTfFOL3OcsAyE +192.168.9.16 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCXNdN2lYWg9k3r5B9QISWCAWvAzzDj/aO5EeYPimDcz383Qliq0xW+FjJvgw2fY/Aoljwd6pGY3pJPN1aM3i4Qxt22S6emsFkGm89/GOdEu9ZkB+ln0fL8uPJTdjQklkhGb8YIF4aD8OMkMNXn7Ale+TiJJuWWmh7UAQITM5EqX1Wq+uAM/2ixpKo3dQU8L6pzeGi5yQUzHB3eyckSnFjCrSS/hW6lsKyhoaSYn+cAiuhbWEwP4lv9um6Cl2rLZYcysJTTs1DP7gJL3eyytnQ83R8MEX5/qY2zSVFCaNPguF7hlC7MgdR90naqlzm40XzYXHloDIg92+kGZGdR6jcKy7QasHuQLeXXJEk63jl64IJwRJPNnrVGszEDwIz0nBzVX/Yot2R/uAjhJRdR5d9tlZbMILkng6I/BVdJhqnJzuBottZpfu0qaBakIecDbLiJeU+AbAQffzWesyAmlHaLLLCHgwLdNaVjCltx6/RebF8HRRO82sdRE8Js6dSAXZ0= +192.168.9.16 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLpXVmQVbrhdfbrTHINQ2lZCxLxgrWbIDoNoxRR7EuqI5qNr2LQhqmTVZpNUIvj7PhdGPN4hry9jMfC39Dwa7Eo= + +192.168.9.11 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFbAGYglyMYT/IfX9G4n9GhbPf+8T7TyVin/DfP3eKgb +192.168.9.11 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCthnkPJKC1Ij/Fc4fTRubcV8xDSKJ4UrlUYgZ+lO2AZYKhefVZOaDkizqWbjf9c4dqAAEX1lqgDwr3UwEw6hukLLhherCU6tn3Q/nmOQsbKrEGG+kJXnzCi1jYsH4UNQB8Xl18eaLhGZosqCfEVK3LHw2/nMsxLfbrw61qf9Z7/haQOJU2M2W9X3U/yEk4sOCoxguJIoNJ0JZB+X3xO2TXKLCKuaWpckbmHAVuL0c4u0ZfYqF3lCxWLj/iI5SKD/YqajohaVBaoJkmtIkLuuSSFe5gJb22htMgKXoQ0TcxxAvTnHwa92sVSMx7Lp1zQ5RGzN+Dw99FL4HyR4KDPlmVpGPZbNp0LnnggQ0SBmT6F3KarNZLzSW6w2foBkl9XjTEBUjb5cHXK8jq7nEUK26TgirngSAFg7y44NWta6whRX0+/wCanlvgQ1BNYGf4vcw5P+Q8MoGOdepF7b8gQiUn/KhtmnQem/Q1//xDt9MOJkmjBemG9KIso/gqtJkHeps= +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= diff --git a/alpine/mount_volume b/alpine/mount_volume new file mode 100755 index 0000000..df18789 --- /dev/null +++ b/alpine/mount_volume @@ -0,0 +1,25 @@ +#!/bin/sh +set -e + +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +FILE="/data/$1" +MAPPER_NAME="encrypted_volume" +MOUNT_POINT="/mnt/${MAPPER_NAME}" + + +cryptsetup close "$MAPPER_NAME" || true +cryptsetup open "$FILE" "$MAPPER_NAME" + +mkdir -p "$MOUNT_POINT" + +if ! mountpoint -q "$MOUNT_POINT"; then + mount /dev/mapper/"$MAPPER_NAME" "$MOUNT_POINT" + echo "Mounted /dev/mapper/$MAPPER_NAME at $MOUNT_POINT" +else + echo "$MOUNT_POINT is already mounted." +fi + diff --git a/alpine/rex b/alpine/rex new file mode 100755 index 0000000..92d575d --- /dev/null +++ b/alpine/rex @@ -0,0 +1,8 @@ +#!/bin/bash + +cmd="$*" + +echo "Running on hosts: $cmd" + +pssh -h /app/host_vars/hosts -t 0 "$cmd" + diff --git a/alpine/template b/alpine/template new file mode 100755 index 0000000..6c0a841 --- /dev/null +++ b/alpine/template @@ -0,0 +1,55 @@ +#!/bin/bash + +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +host_vars_dir="/app/host_vars" + +keys=(NEBULA_CA API_KEY HOSTNAME NEBULA_CRT NEBULA_KEY SSH_PRIVATE SSH_PUBLIC) + +NEBULA_CA=$(<"$host_vars_dir/ca.crt") + +localfile="$1" +remotefile="$2" +remotetmp_base="/var/tmp/4server" + +# Read hosts from file descriptor 3 to prevent ssh from consuming stdin +while read -r host <&3; do + host_env_file="$host_vars_dir/$host/$host.env" + + if [ ! -f "$host_env_file" ]; then + echo "Warning: env file for host '$host' not found at $host_env_file. Skipping." + continue + fi + + # Load host environment variables (supports multi-line) + set -a + source "$host_env_file" + set +a + + NEBULA_KEY=$(<"$host_vars_dir/$host/$host.key") + NEBULA_CRT=$(<"$host_vars_dir/$host/$host.crt") + + SSH_PRIVATE=$(<"$host_vars_dir/$host/$host") + SSH_PUBLIC=$(<"$host_vars_dir/$host/$host.pub") + + content=$(<"$localfile") + + for key in "${keys[@]}"; do + value="${!key}" # indirect reference + # 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" + echo "$content" | ssh "$host" "cat > '$remotetmp'" + + # Move temporary file to final location with doas + ssh "$host" "doas mv '$remotetmp' '$remotefile'" + +done 3< /app/host_vars/hosts + diff --git a/app/.ssh/boston b/app/.ssh/boston new file mode 100644 index 0000000..973dd18 --- /dev/null +++ b/app/.ssh/boston @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACAOKT5um7fzviD27odH4GTW5oos0xAeP3DW7OhTqbEyAgAAAJhBIAtnQSAL +ZwAAAAtzc2gtZWQyNTUxOQAAACAOKT5um7fzviD27odH4GTW5oos0xAeP3DW7OhTqbEyAg +AAAEAk4EqWEVc34Dht1e/vD0ajtgSCo6xq30YPtvnrNuL3PQ4pPm6bt/O+IPbuh0fgZNbm +iizTEB4/cNbs6FOpsTICAAAADm9saXZlckBtYXRlLTE2AQIDBAUGBw== +-----END OPENSSH PRIVATE KEY----- + diff --git a/app/.ssh/boston.pub b/app/.ssh/boston.pub new file mode 100644 index 0000000..336d611 --- /dev/null +++ b/app/.ssh/boston.pub @@ -0,0 +1,2 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA4pPm6bt/O+IPbuh0fgZNbmiizTEB4/cNbs6FOpsTIC oliver@mate-16 + diff --git a/app/.ssh/london b/app/.ssh/london new file mode 100644 index 0000000..4617a96 --- /dev/null +++ b/app/.ssh/london @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACD64E32udubuP0PEhxgoxR35jgHFUzlSRONXfGgsxNPswAAAJhgli31YJYt +9QAAAAtzc2gtZWQyNTUxOQAAACD64E32udubuP0PEhxgoxR35jgHFUzlSRONXfGgsxNPsw +AAAEC+wOOEwsAkPIhD02f+Yw5MYaCZZ5hhU9ZG62CcdH4+XvrgTfa525u4/Q8SHGCjFHfm +OAcVTOVJE41d8aCzE0+zAAAADm9saXZlckBtYXRlLTE2AQIDBAUGBw== +-----END OPENSSH PRIVATE KEY----- + diff --git a/app/.ssh/london.pub b/app/.ssh/london.pub new file mode 100644 index 0000000..8b13f28 --- /dev/null +++ b/app/.ssh/london.pub @@ -0,0 +1,2 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPrgTfa525u4/Q8SHGCjFHfmOAcVTOVJE41d8aCzE0+z oliver@mate-16 + diff --git a/app/.ssh/mumbai b/app/.ssh/mumbai new file mode 100644 index 0000000..ad84fa4 --- /dev/null +++ b/app/.ssh/mumbai @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDEN4FtZTrBFBkDDy7hTptxeoyPvpHDbSKuJxEGWusaMgAAAJjCrJXhwqyV +4QAAAAtzc2gtZWQyNTUxOQAAACDEN4FtZTrBFBkDDy7hTptxeoyPvpHDbSKuJxEGWusaMg +AAAEAUwrtarcT6FifBEUGdm8F32i2lZ3rEeYyKQo5n0wRyxcQ3gW1lOsEUGQMPLuFOm3F6 +jI++kcNtIq4nEQZa6xoyAAAADm9saXZlckBtYXRlLTE2AQIDBAUGBw== +-----END OPENSSH PRIVATE KEY----- diff --git a/app/.ssh/mumbai.pub b/app/.ssh/mumbai.pub new file mode 100644 index 0000000..abe07f3 --- /dev/null +++ b/app/.ssh/mumbai.pub @@ -0,0 +1,2 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMQ3gW1lOsEUGQMPLuFOm3F6jI++kcNtIq4nEQZa6xoy ansible@mumbai + diff --git a/app/.ssh/saopaulo b/app/.ssh/saopaulo new file mode 100644 index 0000000..200b07e --- /dev/null +++ b/app/.ssh/saopaulo @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACDAXv2aFH8xzLWB5zvtJI1SkgDnB/OMhmkFQQa/vzhk8gAAAJjM4MTdzODE +3QAAAAtzc2gtZWQyNTUxOQAAACDAXv2aFH8xzLWB5zvtJI1SkgDnB/OMhmkFQQa/vzhk8g +AAAEDVHiNzHN0zUBHU5ui2U00xxXs4UIVGVRW1PCEhxpD/iMBe/ZoUfzHMtYHnO+0kjVKS +AOcH84yGaQVBBr+/OGTyAAAADm9saXZlckBtYXRlLTE2AQIDBAUGBw== +-----END OPENSSH PRIVATE KEY----- + diff --git a/app/.ssh/saopaulo.pub b/app/.ssh/saopaulo.pub new file mode 100644 index 0000000..2e26447 --- /dev/null +++ b/app/.ssh/saopaulo.pub @@ -0,0 +1,2 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMBe/ZoUfzHMtYHnO+0kjVKSAOcH84yGaQVBBr+/OGTy oliver@mate-16 + diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..c76c183 --- /dev/null +++ b/app/README.md @@ -0,0 +1,9 @@ +aaa-bbb-UUID +aaa = server +001 = manchester +002 = boston + +bbb = image +001 = n8n +002 = ODOO_18 +003 = ODOO_19 diff --git a/app/backup_prune b/app/backup_prune new file mode 100755 index 0000000..79cc008 --- /dev/null +++ b/app/backup_prune @@ -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" diff --git a/app/download_sbin b/app/download_sbin new file mode 100755 index 0000000..8f7f760 --- /dev/null +++ b/app/download_sbin @@ -0,0 +1,6 @@ +#!/bin/bash + +rsync -avz --progress -e ssh mumbai:/4server/sbin/* /app/sbin/ + + + diff --git a/app/etc/.bashrc b/app/etc/.bashrc new file mode 100644 index 0000000..b9fc86b --- /dev/null +++ b/app/etc/.bashrc @@ -0,0 +1,15 @@ +# ~/.bashrc + +echo "command: mount_volume " +echo "alias: set_prod" + +export hosts_file="/app/hosts.dev" + +export PS1="\[\e[32m\]\h:\w\$\[\e[0m\] " +df -h . + +set_prod() { + export HOSTS_FILE="/app/hosts.all" + echo "HOSTS_FILE set to: $HOSTS_FILE" +} +cd /4server diff --git a/app/etc/init.d/4server-api.init.j2 b/app/etc/init.d/4server-api.init.j2 new file mode 100644 index 0000000..a1ab4a3 --- /dev/null +++ b/app/etc/init.d/4server-api.init.j2 @@ -0,0 +1,21 @@ +#!/sbin/openrc-run + +name="4server-api" +description="4server API Service" + +command="/4server/sbin/api" +command_args="" +pidfile="/run/${RC_SVCNAME}.pid" +command_background="yes" + + +output_log="/4server/data/api.log" +error_log="/4server/data/api.log" + +depend() { + need net + use logger dns + after firewall +} + + diff --git a/app/etc/traefik/certs/001-001-123e4567-e89b-12d3-a456-426614174000.od8n.com.crt b/app/etc/traefik/certs/001-001-123e4567-e89b-12d3-a456-426614174000.od8n.com.crt new file mode 100644 index 0000000..a93e419 --- /dev/null +++ b/app/etc/traefik/certs/001-001-123e4567-e89b-12d3-a456-426614174000.od8n.com.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBzCCAe8CFA4xhYG29I1JGz2K+LN79to0c5HrMA0GCSqGSIb3DQEBCwUAMEAx +PjA8BgNVBAMMNTAwMS0wMDEtMTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0 +MTc0MDAwLm9kOG4uY29tMB4XDTI1MDgyNTA0Mjk1OVoXDTI2MDgyNTA0Mjk1OVow +QDE+MDwGA1UEAww1MDAxLTAwMS0xMjNlNDU2Ny1lODliLTEyZDMtYTQ1Ni00MjY2 +MTQxNzQwMDAub2Q4bi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQDPRqge1j5i2Mtygzh4Ep3S4WAVRUbygFgliBslkrO4d4mxusOLhzBn5JJY+NWA +pNsvnojMcGlgeIugsi3MeMn2/ay88Y5THPrHXqf4jTJB8DvlYbi41HfBX6rstF4z +2IZ4gIp6aem6wVuIcI6DKlPlEQss09aFkTrp4jKvPPCq3tgbcI4PkHvHm4fpzIjW +5I8JrQBqBHrNmYQfUT+ZEABKj0XQMH+CceNOIw18ChKoHIJbIpqAKO0zMYiQ6fCZ +y4OWJCHk7ekXNdNPjt2K1lh1doNK6gPxjsIuh5Pxd+BANoumqMCFbLNs8bdwO7p7 +Po5uFbU5RB4L5KSoxYPgmFZ5AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAD/1Wxi +xzOkSNHG3Fu50q0m4qxEEoNYvxFkB2EUSA5DKGeCQq46tKpjNR+zZzG2fbn711v9 +qSNuZEhxajQkQ0oR7CbMDs8Ql/WlregdZTv053liBHFkpwYVRSaaE2LnxvnlbmMq +eNIlPldmN/b7Rs07e5GcIkZ8mMQrbT2TuQV4Q7be8qjex8zF7OzLF1ok//C2SMwF +0qd5Z9e4rycI2XleE73Y/Vdl1vO+/RbZDfhhp12gYi/A+jywMYgckVEjUKQ2/9FT +UIaDtDEh9oYirBEyqmJPsZi2nqt1UJKDctqGykmZZwymAtfYPnPNVWYgp7nU210c +hc8zQDaZyEZUEYw= +-----END CERTIFICATE----- diff --git a/app/etc/traefik/certs/001-001-123e4567-e89b-12d3-a456-426614174000.od8n.com.csr b/app/etc/traefik/certs/001-001-123e4567-e89b-12d3-a456-426614174000.od8n.com.csr new file mode 100644 index 0000000..1769cc5 --- /dev/null +++ b/app/etc/traefik/certs/001-001-123e4567-e89b-12d3-a456-426614174000.od8n.com.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIChTCCAW0CAQAwQDE+MDwGA1UEAww1MDAxLTAwMS0xMjNlNDU2Ny1lODliLTEy +ZDMtYTQ1Ni00MjY2MTQxNzQwMDAub2Q4bi5jb20wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQDPRqge1j5i2Mtygzh4Ep3S4WAVRUbygFgliBslkrO4d4mx +usOLhzBn5JJY+NWApNsvnojMcGlgeIugsi3MeMn2/ay88Y5THPrHXqf4jTJB8Dvl +Ybi41HfBX6rstF4z2IZ4gIp6aem6wVuIcI6DKlPlEQss09aFkTrp4jKvPPCq3tgb +cI4PkHvHm4fpzIjW5I8JrQBqBHrNmYQfUT+ZEABKj0XQMH+CceNOIw18ChKoHIJb +IpqAKO0zMYiQ6fCZy4OWJCHk7ekXNdNPjt2K1lh1doNK6gPxjsIuh5Pxd+BANoum +qMCFbLNs8bdwO7p7Po5uFbU5RB4L5KSoxYPgmFZ5AgMBAAGgADANBgkqhkiG9w0B +AQsFAAOCAQEAktJBSt2lSbrsmUlhG+6AZ4lo52qOwmxxTQ7steEfkMp6zOvO3FXk +meOiU59fFyOkH0pUpJo4RolZPfSyzdi0R9fV5wR/a1eqaiNyzReTPyyXKP2SMdzu +Xav3ldaMOGp6gPa3qmyQ6nQJjVJWj/FulCslIAv55Qk2xMlRQYV+IIK4Gggl74d9 +Kwbq3MDvMeLJS5Qzr/hHqWmPWiUKbs1DbTajSe63B36/yMEi8VYjdWw7K86kS2X/ +0yy1M9+HOHPG1Ch5zHaa64iioo1iaMxTqBgOvJTsuCtNX5oflj56STBozNvzMq1o +0/E9/uxW1TDL8iDYp/k2krT4k1M0rKq6Tg== +-----END CERTIFICATE REQUEST----- diff --git a/app/etc/traefik/certs/001-001-123e4567-e89b-12d3-a456-426614174000.od8n.com.key b/app/etc/traefik/certs/001-001-123e4567-e89b-12d3-a456-426614174000.od8n.com.key new file mode 100644 index 0000000..37f1bfd --- /dev/null +++ b/app/etc/traefik/certs/001-001-123e4567-e89b-12d3-a456-426614174000.od8n.com.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPRqge1j5i2Mty +gzh4Ep3S4WAVRUbygFgliBslkrO4d4mxusOLhzBn5JJY+NWApNsvnojMcGlgeIug +si3MeMn2/ay88Y5THPrHXqf4jTJB8DvlYbi41HfBX6rstF4z2IZ4gIp6aem6wVuI +cI6DKlPlEQss09aFkTrp4jKvPPCq3tgbcI4PkHvHm4fpzIjW5I8JrQBqBHrNmYQf +UT+ZEABKj0XQMH+CceNOIw18ChKoHIJbIpqAKO0zMYiQ6fCZy4OWJCHk7ekXNdNP +jt2K1lh1doNK6gPxjsIuh5Pxd+BANoumqMCFbLNs8bdwO7p7Po5uFbU5RB4L5KSo +xYPgmFZ5AgMBAAECggEAXDMx1YW3eoC0/tsf40lkqslV1CBczsIIc6l3ad830nZZ +6ZUKJqacAZrK/oixb+flF+mNMGNQfHkiovifJrUUIan1jJZmmNHrO4P/c7BbCrmx +6vbtFEpuerXzchdJUAagyjljX9B9B3W7IZXvzqilaN+L+QTCB+fyLNdRdGHHLDnn +HflKCPVQS/PWTBLufxtmmZ59uqLkWzOio8d710qrxPankejXb2kvuDjWuWu9Rdja +lxR8IdZaG32hPhrkFL7fxlQjLuRsPIP0GMaQeEOOvyTi2e5BBCE1CiCxT78e8cIP +eae42jNHelhkj/FA6vLcQwPAis1TZ8e3+GkUt357gQKBgQDtxaBm8PT6a+R9GNzs +c5gCWwm21YUqzbCl4Q/8Mnyn2wqQn+/RmfMwr1uNLAiGkiB1JNjbO0pzoeJyk03F +vDxd/QyicSfYizndj+XIEmLRyUwixXZzpqoJLYHoiwrl6f/vVQfDLgUJYbMFHzQW +xuuLGhqFjbLgKlVwGEd0O2QiawKBgQDfKoi0upLabWHgYmsFm7HxqUECi3/iJDrx +JW/qDOBQtgfUTi01JFg0/xctQRdERh+8bBwOCWnkMdaPtJTXjJo1AgO+XzF0BB1B +FooXcqe7eokPUeBb3P/CdXNNrfBcisDbTX2za0AG8ETja4jBhliR45O+S58G5hku +8W59g+BLqwKBgChQoenSYTc0pAEx/gN5dgSwOu1tNq8TQShfCL7SMKClWx06gQcg ++0L9+J/vH2Lx098I6FwDqZQBlsumfkFQsUueZE4GsaLduGoAxA0wUOERKH+cy4DA +eYQk2yn6qVZiXqrN2AsX+nKkxh3QNJzIDZgATQ7n/7RSeToQY80pZMkRAoGAe4s3 +fR3gmI1/ZtH1P3iPDSLO+5KwrEe0XbWE/EQ+lk//i5fvzQCe2E/zy7jCIajUfuI2 +scqiVZMFni6xS5bp87h2zBg0724rp9HLhumRU+elItcH5rM037lXqMRHUWP7Gi0P +DpmsK2suJ9xrK/+s3q7nJq0Ej7QocuVzboboT9sCgYEA2aaRafzP6v/quJwc33RZ +0PFxPXDKV9MuXjuve+7d37iSo3A/1h8/hdwg48r6OnQUCtLOvkZ1t21UKlbGY6F9 +qw8VkMxmCPgIZHqLfOT7TNsHoF8eZAN6HMGkI/+SzpYpaA3NcB0tajJk8+8PHJI6 +Rg/Hkv4zpzmiPDN7F6l89rQ= +-----END PRIVATE KEY----- diff --git a/app/etc/traefik/certs/dev.crt b/app/etc/traefik/certs/dev.crt new file mode 100644 index 0000000..1750a20 --- /dev/null +++ b/app/etc/traefik/certs/dev.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFNzCCAx+gAwIBAgIUCIJuU/7oPy1PrksppEPukIs4MfgwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHKi5sb2NhbDAeFw0yNTA4MDgyMTMyMzlaFw0yNjA4MDgy +MTMyMzlaMBIxEDAOBgNVBAMMByoubG9jYWwwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQC+uBU5mo8h8LK00Hrw9AtaPI2yXBaVh5S8KrAJ0eoUSxc0gg7q +dwsD9+boyaDbiePcllTLvmIKqX8K2TbiucEaqNGzJauop0+UZjjCQrzuq+cD6xPh ++1bzcWN+oLubUtv4wi3mRNCtP56YyM4c72OweXB9Mhi9Z8e2caTjCLdcCS10i5Oy +NWYZFhnjBxXJoElTt4HZFLDj60Iqi9thVGO4virv7VBwOvAKaCgOOuagPtISgHO7 +1t1hV9TNTHRcE37xpOZT6moPsEBitkszwPx24SgATGrG5J8UbDJ5EdY+kA4wD0mU +hi9pUWaRlKWQjqRRszvsSnbQUPHORHSUFFpycworeNUBCmTs5jm0/+RqI4TLTUX6 +ZbJ6azgGpgbJtMbMlywW1Yuy9ACrSP/jncKekiR+0uQ5s+y2crT+aeuzHsyMtUUn +TI1ExsOE/QWGH7MV298D+jvSSWg4WTf3dzAiFsDxP4JtDZ1NmDwm6Pjmano1Y57g +uU++4RvYN6YKxDnkcWXIZFpUvW+dr7oLZaOcqwCx4KVCFo4e2qqigYgWgz8r05iE +ngj7UZO70n3dZrkL4Iu2tFATHLBy1SYZIu3ewZodOeK54q63bYtVFj7ECAE4Eb7J +6DgjOtN3GH9E2aKMjzFRvWzItRufLWIycPN/tAOh6dOPuX9oZQf71sxe3wIDAQAB +o4GEMIGBMB0GA1UdDgQWBBSGznETTeVc5FVFEGbdVUzR5jfQADAfBgNVHSMEGDAW +gBSGznETTeVc5FVFEGbdVUzR5jfQADAPBgNVHRMBAf8EBTADAQH/MC4GA1UdEQQn +MCWCCWFwcC5sb2NhbIIJYXBpLmxvY2Fsgg10cmFlZmlrLmxvY2FsMA0GCSqGSIb3 +DQEBCwUAA4ICAQCl+LRB+6Rz0EJFbZnhLWvumY2KegS+QkB6YUDycJIuq/2Q9RWB +Z0yV94asZcvHE21/BHhnMk4Qa2PsQn8gQIGCAhj+/2DVt5mGwWVgoes1gtAg6okH +YYKhTljjfpMFqyp/lyzanzF4VdnhzDKpaRLxKwuCf0xe9V03S4/fri/tVjxpjUyc +eaTgfDlzJgQu2rZZz8dG7fltCEhl9gBGbQ3WWaSDYOW49UXqS3LR0eBZ4s/RAG7Z +LiBIKzOFQjLplaODsCOpOguzRfL6O2WXDADbuh7XAQmmhkfsuruPvP/5E1G1hb6K +khsKyiYo4WLpdGJACezN/jmQVcqULz8iLI/jRaoT5g3dwvBkzyolIF+A6a33D3Ph +vQd5ta6BT/EWTBp4T5MSyvd03rkqV0oCHeF+wTQ3iR4b5jrxlVtqCFlsK32NrB9e +ZAboJitgxLgs6ZKXhoxCGjtZdpgYyxqgEOtJazzNitNxB8Xyb3hCc2t7VPpRUfUa +gyddQFd1yZmhPZqhugXI+LL7xO7HHyrz+CwqeWkObJNDRIe6Me4Rxo9H0ZQfjLa1 +fAgxubtAsGr0AwQSg3X/PamEhVdjvCBCtadgHQZQLaP7ilPBcER/xBQ1jbI1LYzF +BTCypCFykXbDxxbOwzhwRLoUHzWS2XAYT7vOHE60AokMKwArz9s3Hu+wUg== +-----END CERTIFICATE----- diff --git a/app/etc/traefik/certs/dev.key b/app/etc/traefik/certs/dev.key new file mode 100644 index 0000000..44e4adb --- /dev/null +++ b/app/etc/traefik/certs/dev.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC+uBU5mo8h8LK0 +0Hrw9AtaPI2yXBaVh5S8KrAJ0eoUSxc0gg7qdwsD9+boyaDbiePcllTLvmIKqX8K +2TbiucEaqNGzJauop0+UZjjCQrzuq+cD6xPh+1bzcWN+oLubUtv4wi3mRNCtP56Y +yM4c72OweXB9Mhi9Z8e2caTjCLdcCS10i5OyNWYZFhnjBxXJoElTt4HZFLDj60Iq +i9thVGO4virv7VBwOvAKaCgOOuagPtISgHO71t1hV9TNTHRcE37xpOZT6moPsEBi +tkszwPx24SgATGrG5J8UbDJ5EdY+kA4wD0mUhi9pUWaRlKWQjqRRszvsSnbQUPHO +RHSUFFpycworeNUBCmTs5jm0/+RqI4TLTUX6ZbJ6azgGpgbJtMbMlywW1Yuy9ACr +SP/jncKekiR+0uQ5s+y2crT+aeuzHsyMtUUnTI1ExsOE/QWGH7MV298D+jvSSWg4 +WTf3dzAiFsDxP4JtDZ1NmDwm6Pjmano1Y57guU++4RvYN6YKxDnkcWXIZFpUvW+d +r7oLZaOcqwCx4KVCFo4e2qqigYgWgz8r05iEngj7UZO70n3dZrkL4Iu2tFATHLBy +1SYZIu3ewZodOeK54q63bYtVFj7ECAE4Eb7J6DgjOtN3GH9E2aKMjzFRvWzItRuf +LWIycPN/tAOh6dOPuX9oZQf71sxe3wIDAQABAoICAFzKl5kVN/qdb3VF0esV8cgP +miljYKGT+6upYUkF1svU1Q95D+TH0pY1sSUlpJvr9O9IPS18DZt+aA9RK8EX+3oL +FSwCcgh2juN28LqjWeUNwjJH176lWOLNEklzzpN9twTLBSX56UXBpFpVqOKvHmOo +UjC3hQ3yRlrf5AeKIBwpYvJHTq7wCCLAfAvXUKRu1f5jVEvYI1BhECo/LZenRXWH +IMDnR7GzG0MU9hgmVDs3FWJnGOgVXFSWNTVFs39xBNxxDJdbgAruCAV/CAvAI5V7 +asjqZTEr3rJDCjOZmBGMaTq81WHr/3lQX4UJO5yfqhcOC2OlvzUPjPZ8m/PIC1C4 +rOg6EqEA5X+VOspxbJGQVlsA1R1CkI499s5CERWQ8Z9Gb5kr4/SzKBnp8DAbngNR +rZxuT4pch7rHZgEDiW8h18aRN3LDvjUPF2pvowEKPRmdQJ6xTi22GuyL3pl3M8Wg +3snIl0sdfsnarWTV545bm1nIZ+4agfIzRjIc+Z4ACx3k9NBObkHxdq3Grscgl+cr +OtuQYt7T0EDfPOGqXgZ2/imdtovIYOz7BHzlchZGIFmgtSFfgZCGcWQWiXdlGmOC +EJQ9gwqLmCG4i1V8UIb2NQXPF77rHpOz/psptCxj7gvTwp8yFURL6hdqoUpzl7l+ +H3QGUAl1N6vvCJry0HLBAoIBAQDmue87L+x6BBg+0g2SAO0Ivl5w2Q4KKGZREgld +7CGVyDCcZGs1EHsATZgNPUH0L9rjegTtSzarGdLPSTnDdc6mataS++3YdmWRYKxo +8so3L074FLW3acisP8YdsH99jrwkrYAm130whIkk8cEZAlSYd3uVj7RypN/WDDWm +UOmGWrQYBduGF3/JvnFbLIbassfzc7Yx2jgFDyFpQur6ZNDK3YUbjGALe07D/TO6 +k4AN7NUg8J8e/nF5J2HOtGUTlHScouz3AhJFbBtGJMh2zPburR7iAU4oSCAwbiiG +3AsVqndt1iTUqkEeHUBogx9OiXfLccXxdZkXNz42Tv7ezYePAoIBAQDTnEJJV/sm +NDhMkPjKFLprdpP7n6nNlECXrNogHGTTOiXMUcgVl/CuX8cfOc7ExfJ91Bh2XqN2 +H9tgOzlTWEcRORFAJv2ZRSBTfVQAboL1ncZTMXlDR3SqPVC7GC53gk4IAbm2Rs59 +jqHqZJdGMHAK747zQAPO8c5qCUgDAO8hP7mrdBU9+Tj9lA0vNyGq4uUqoepUvado +Suk3CggsVjcVffzBIm8u0QHNHVUg3hVHycJTQwSIyWbej/eCx1ZD5/9olO6aREHS +lKr0Bm4+1AdlkXgJE/eoABQa1fiUgEjPbi9q4ORjpFbMbcvnb9Z6sf2VvTZn/wj6 +K4JtX50o+YuxAoIBAQCa/RvvLmo0LLv8ty3Wfji8PuVB+QytViYlH3CbXxvQegHt +jKdXphJ6SaVyf0vmtJ7dYAIfRP8cQOSTyiS7YE/JCsvJQOKtHhtsZPxsI2wjVew+ +Sesnoi/jRZPYLc/2kANiwAnuDaNTDDT0VFHacu5Q3TJvbXFR8d9K8ji32HKGhjek +S4sDsJVu+Dc9f9O25ZHbwEcLhgNLorZW91TRjxeSruvTbaC3FcX7cgNlud+zevxQ +fFLnhxTCxem16Qhc9sS+09NKumF7sPtBS0Q+ScE246RzPV07QfgdkGI49Weczj4b +0lY7ZYMIr62shyhooX+PcoX9hXmpVrq70KT1FiuRAoIBAQDAN8Gys9usIWU+j7We +guOvuB/GQotQ9akS3e2pm4EuqjQpe+Q/USxMiS3sPGuJLLIQAHhUFbVwGJICwOla +vuaXS3pTBtf3wOYTUNXcKoaFK9M6QMeBCMh914Kc4ONcpZ3SAhc67uik/soviz4q +gNdV57O3XF/ZPKcehN3H9LJDRoqWprSg/eD53uF3ESJhAwfeCQQ+A6SsxNdBqrgv +5gTVXgMZQPkz0qFLO6jXWUSFWE1PqqHUyvXJl4biYcYHmxbTXe27beNIsMj1L143 +bgxmA5TA0kV1ctTQZ6sM4dbBrboe4Lg1ltNNkTLWQS3XeBT8Tsq7/tudu6YXSfIN +hViRAoIBAHGypG9v+vToWta0AT4CC3eOvNjzKGtr26oycFsXqQE8Q6ZKohcG0UNj +QnfawjyVhSdq2hS0O0uZuhyeea9nBtL8y8u120rvS10C71er7hG2ywscdJ4Hr5WX +D27RC+U7AwMbcqEy3Vs9vo2c5cBivLGWf/R3SgCecwxX8APysuSXod7DKhNviS4P +f8t8Tui//+PkNV6brOLvu0kITypoFhp9qAexgAuLTXOPNEILugcsfusBwPEjSdAR +LBh1fxSrGPCcRqo+8N4qorki1IE0l/bJBj3p2vREgItmq+OC0KT47Ye0BVJJtrrU +YV/U3ImFkT12e6nwfgrMRfQCZrRsp9g= +-----END PRIVATE KEY----- diff --git a/app/etc/traefik/certs/generate b/app/etc/traefik/certs/generate new file mode 100755 index 0000000..d5d64b5 --- /dev/null +++ b/app/etc/traefik/certs/generate @@ -0,0 +1,6 @@ +#!/bin/bash + +openssl genrsa -out $1.key 2048 +openssl req -new -key $1.key -out $1.csr -subj "/CN=$1" +openssl x509 -req -days 365 -in $1.csr -signkey $1.key -out $1.crt + diff --git a/app/hardening b/app/hardening new file mode 100755 index 0000000..efff82f --- /dev/null +++ b/app/hardening @@ -0,0 +1,12 @@ +#!/bin/bash +# DISABLE ROOT and PASSWORD LOGIN +rex "doas sh -c 'grep -q \"permit nopass 4server as root\" /etc/doas.d/4server.conf 2>/dev/null || echo \"permit nopass 4server as root\" | tee -a /etc/doas.d/4server.conf > /dev/null'" + +rex "doas sh -c 'sed -i \"s/^#\?PasswordAuthentication.*/PasswordAuthentication no/\" /etc/ssh/sshd_config'" +rex "doas sh -c 'sed -i \"s/^#\?PasswordAuthentication.*/PasswordAuthentication no/\" /etc/ssh/sshd_config.d/50-cloud-init.conf'" +rex "doas sed -ri 's/^\s*#?\s*PermitRootLogin\s+.*/PermitRootLogin no/' /etc/ssh/sshd_config" +rex "doas sh -c 'echo \"PermitRootLogin no\" >> /etc/ssh/sshd_config.d/50-cloud-init.conf'" + +rex doas apk del linux-virt + +rex doas rc-service sshd restart diff --git a/app/host_vars/ca.crt b/app/host_vars/ca.crt new file mode 100644 index 0000000..bf88e8e --- /dev/null +++ b/app/host_vars/ca.crt @@ -0,0 +1,5 @@ +-----BEGIN NEBULA CERTIFICATE----- +Cj8KDU9ET080cHJvamVjdHMoqNOhvgYwqKTJogg6IDv7w4DxfOvLDJ6WgjE3V8MZ +k1I6t5GjmBmnyd0Wf0UqQAESQAzBFnjUsemshOlFCJisKbXdqBR83/Fl5aS0xSQj +ZcDIpmgPnslBHTo8oPJLWeuU0Qd9IHNfdQvam2j6YnzVQAE= +-----END NEBULA CERTIFICATE----- diff --git a/app/host_vars/create b/app/host_vars/create new file mode 100755 index 0000000..595ec2d --- /dev/null +++ b/app/host_vars/create @@ -0,0 +1,26 @@ +#!/bin/bash + +# Check for argument +if [ "$#" -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +key_name="$1" +target_dir="./$key_name" + +# Create directory if it doesn't exist +mkdir -p "$target_dir" + +# Full paths for private and public keys +private_key="$target_dir/$key_name" +public_key="$target_dir/$key_name.pub" + +# Generate Ed25519 key without passphrase +ssh-keygen -t ed25519 -f "$private_key" -N "" -q + +# Confirm creation +echo "SSH key pair created:" +echo "Private key: $private_key" +echo "Public key : $public_key" + diff --git a/app/host_vars/dev/dev b/app/host_vars/dev/dev new file mode 100644 index 0000000..db8ecee --- /dev/null +++ b/app/host_vars/dev/dev @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBH5vr90RFgOJrP2Xjr5I5QBxlonCC7pce56JDJFboPXQAAAJh9gvJGfYLy +RgAAAAtzc2gtZWQyNTUxOQAAACBH5vr90RFgOJrP2Xjr5I5QBxlonCC7pce56JDJFboPXQ +AAAEBSerxP83/u4p/IobVSxko5ZXO+/PPczGW0kopTfLLAykfm+v3REWA4ms/ZeOvkjlAH +GWicILulx7nokMkVug9dAAAAEXJvb3RAMmNjNzhkYzBhZDIwAQIDBA== +-----END OPENSSH PRIVATE KEY----- diff --git a/app/host_vars/dev/dev.crt b/app/host_vars/dev/dev.crt new file mode 100644 index 0000000..340c7db --- /dev/null +++ b/app/host_vars/dev/dev.crt @@ -0,0 +1,6 @@ +-----BEGIN NEBULA CERTIFICATE----- +CmYKA2RldhIKk5KghQyA/v//DyIDYmVlKI+YrcUGMKekyaIIOiAXY9FKiA1V6ayD +Vx9Ce9UK3YcCF93DNP68WPixdl9LZUognXOojuxdSXZ4IG4v3A8HJ/77YSYnV/il +ywmZ6V2khEESQHUVytAPARrJ0KxKPolUot6cl+UNMo5HOMqg2kxiRZBIUTp5XIME +WfrYcdjlS9af7I34439r6gs4bA2LDGaaMQs= +-----END NEBULA CERTIFICATE----- diff --git a/app/host_vars/dev/dev.env b/app/host_vars/dev/dev.env new file mode 100644 index 0000000..1c58284 --- /dev/null +++ b/app/host_vars/dev/dev.env @@ -0,0 +1,2 @@ +API_KEY=your-secret-api-key +HOSTNAME="dev" diff --git a/app/host_vars/dev/dev.key b/app/host_vars/dev/dev.key new file mode 100644 index 0000000..60f5ec9 --- /dev/null +++ b/app/host_vars/dev/dev.key @@ -0,0 +1,3 @@ +-----BEGIN NEBULA X25519 PRIVATE KEY----- +96/m6SrUsGWzT6atNvnopzygGhIAaXbBCXT8KAvwKp8= +-----END NEBULA X25519 PRIVATE KEY----- diff --git a/app/host_vars/dev/dev.pub b/app/host_vars/dev/dev.pub new file mode 100644 index 0000000..e371ca8 --- /dev/null +++ b/app/host_vars/dev/dev.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEfm+v3REWA4ms/ZeOvkjlAHGWicILulx7nokMkVug9d root@2cc78dc0ad20 diff --git a/app/host_vars/hosts b/app/host_vars/hosts new file mode 100644 index 0000000..c58c6d8 --- /dev/null +++ b/app/host_vars/hosts @@ -0,0 +1 @@ +sydney diff --git a/app/migrate b/app/migrate new file mode 100755 index 0000000..0d980a5 --- /dev/null +++ b/app/migrate @@ -0,0 +1,5 @@ +#!/bin/bash +rex "doas sqlite3 /4server/data/contracts.db </dev/null | grep -Eo 'Address: ([0-9]{1,3}\.){3}[0-9]{1,3}' | awk '{print $2}' | tail -n1) + if [[ "$ns_ip" == "$server_ip" ]]; then + filtered_domains+=" $domain" + fi + done + filtered_domains=$(echo "$filtered_domains" | xargs) + + DOMAIN_LABEL="" + for domain in $filtered_domains; do + if [ -z "$DOMAIN_LABEL" ]; then + DOMAIN_LABEL="traefik.http.routers.$UUID.rule=Host(\`$domain\`)" + else + DOMAIN_LABEL+=" || Host(\`$domain\`)" + fi + done + echo "$DOMAIN_LABEL" +} diff --git a/app/sbin/ODOO_19/audit b/app/sbin/ODOO_19/audit new file mode 100755 index 0000000..1c5b49f --- /dev/null +++ b/app/sbin/ODOO_19/audit @@ -0,0 +1,3 @@ +#!/bin/bash + +/4server/sbin/ODOO_19/listModules $1 diff --git a/app/sbin/ODOO_19/dbVersion b/app/sbin/ODOO_19/dbVersion new file mode 100755 index 0000000..ca12723 --- /dev/null +++ b/app/sbin/ODOO_19/dbVersion @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +import csv +import sys +import zipfile + +if len(sys.argv) < 2: + print("Usage: python3 check_odoo_version.py ") + sys.exit(1) + +zip_path = sys.argv[1] +base_version = None + +with zipfile.ZipFile(zip_path, 'r') as z: + # Assume there is only one .sql file in the zip + sql_files = [f for f in z.namelist() if f.endswith('.sql')] + if not sql_files: + print("No .sql file found in the zip.") + sys.exit(1) + + sql_file_name = sql_files[0] + + with z.open(sql_file_name, 'r') as f: + # Decode bytes to string + lines = (line.decode('utf-8') for line in f) + + # Skip lines until COPY command + for line in lines: + if line.startswith("COPY public.ir_module_module"): + break + + # Read the COPY data until the terminator '\.' + reader = csv.reader(lines, delimiter='\t', quotechar='"', escapechar='\\') + for row in reader: + if row == ['\\.']: # End of COPY + break + if len(row) < 12: + continue + module_name = row[7].strip() # 8th column = name + if module_name == "base": + base_version = row[11].strip() # 12th column = latest_version + break + +if base_version: + print(base_version.split(".")[0]) +else: + print("Base module not found in dump.") + diff --git a/app/sbin/ODOO_19/import b/app/sbin/ODOO_19/import new file mode 100755 index 0000000..8b5d6af --- /dev/null +++ b/app/sbin/ODOO_19/import @@ -0,0 +1,32 @@ +#!/bin/bash + +# Create the tmp directory if it doesn't exist +mkdir -p /4server/tmp/ + +# Save original stdout +exec 3>&1 + +# Redirect all other output to log +exec > /4server/data/log/importDb.log 2>&1 +echo "$(date '+%Y-%m-%d %H:%M') Import file $1" + +# Generate random 8-digit filename +RANDOM_FILE="/4server/tmp/import_$(date '+%Y%m%d_%H%M').zip" + +# Download file from Google Drive using gdown +gdown "$1" -O "$RANDOM_FILE" + +# Execute dbVersion on the downloaded file and capture output +VERSION=$(/4server/sbin/ODOO_19/dbVersion "$RANDOM_FILE") + +# Output JSON to original stdout +cat <&3 +{ + "version":"$VERSION", + "file":"$RANDOM_FILE" +} +EOF + +# Close saved stdout +exec 3>&- + diff --git a/app/sbin/ODOO_19/listModules b/app/sbin/ODOO_19/listModules new file mode 100755 index 0000000..07a4c09 --- /dev/null +++ b/app/sbin/ODOO_19/listModules @@ -0,0 +1,124 @@ +#!/bin/bash +export PATH=/4PROJECTS/bin:$PATH +if [ ! -n "$1" ]; then + echo "Missing Parameters " + exit 0 +fi + +UUID=$1 + +source /4server/sbin/helpers + +get_contract_info + +# Query installed modules from database and save CSV in variable +DB_MODULES_CSV=$(PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" PAGER= psql \ + -h "$POSTGRES_HOST" \ + -U "$POSTGRES_ADMIN_USER" \ + -p "$POSTGRES_PORT" \ + -d "$UUID" \ + --csv \ + -c " +SELECT + name, + latest_version, + author, + summary +FROM ir_module_module +WHERE state = 'installed' +ORDER BY name; +") + +# Check if container exists and is running +if ! doas docker ps --format "{{.Names}}" | grep -q "^${UUID}$"; then + echo "Error: Container $UUID is not running" >&2 + exit 1 +fi + +# Get addons_path from odoo.conf in container (try multiple possible locations) +ADDONS_PATH_RAW="" +for ODOO_CONF in "/etc/odoo.conf" "/etc/odoo/odoo.conf" "/opt/odoo/etc/odoo.conf"; do + ADDONS_PATH_RAW=$(doas docker exec "$UUID" grep "^addons_path" "$ODOO_CONF" 2>/dev/null | sed 's/.*=//' | xargs) + [ -n "$ADDONS_PATH_RAW" ] && break +done + +# Also check standard Odoo library locations +ODOO_LIB_PATHS="" +# Check standard docker installation path +if doas docker exec "$UUID" test -d "/lib/python3/dist-packages/odoo/addons" 2>/dev/null; then + ODOO_LIB_PATHS="/lib/python3/dist-packages/odoo/addons" +fi +# Also check other possible locations +for lib_path in $(doas docker exec "$UUID" find /usr/lib /usr/local/lib -maxdepth 3 -type d -path "*/python3*/site-packages/odoo/addons" 2>/dev/null); do + ODOO_LIB_PATHS="$ODOO_LIB_PATHS $lib_path" +done + +# Collect all module directories from filesystem +FILESYSTEM_MODULES=$(mktemp) +trap "rm -f $FILESYSTEM_MODULES" EXIT + +# Collect from addons_path directories +if [ -n "$ADDONS_PATH_RAW" ]; then + OLD_IFS="$IFS" + IFS=',' read -ra ADDPATHS <<< "$ADDONS_PATH_RAW" + IFS="$OLD_IFS" + for addons_dir in "${ADDPATHS[@]}"; do + addons_dir=$(echo "$addons_dir" | xargs) + [ -z "$addons_dir" ] && continue + + # Get all directories in this addons path + doas docker exec "$UUID" find "$addons_dir" -maxdepth 1 -type d 2>/dev/null | \ + sed "s|^$addons_dir/||" | sed "s|^$addons_dir$||" | grep -v "^$" >> "$FILESYSTEM_MODULES" + done +fi + +# Also collect from standard Odoo library locations +if [ -n "$ODOO_LIB_PATHS" ]; then + for lib_path in $ODOO_LIB_PATHS; do + lib_path=$(echo "$lib_path" | xargs) + [ -z "$lib_path" ] && continue + + # Get all directories in this library path + doas docker exec "$UUID" find "$lib_path" -maxdepth 1 -type d 2>/dev/null | \ + sed "s|^$lib_path/||" | sed "s|^$lib_path$||" | grep -v "^$" >> "$FILESYSTEM_MODULES" + done +fi + +# Sort and deduplicate filesystem modules +sort -u "$FILESYSTEM_MODULES" > "${FILESYSTEM_MODULES}.sorted" +mv "${FILESYSTEM_MODULES}.sorted" "$FILESYSTEM_MODULES" + +# Process database modules and find missing ones +MISSING_MODULES=$(mktemp) +trap "rm -f $FILESYSTEM_MODULES $MISSING_MODULES" EXIT + +# Process each line from database CSV (skip header) +echo "$DB_MODULES_CSV" | tail -n +2 | while IFS= read -r line; do + [ -z "$line" ] && continue + + # Extract module name from first CSV field + # Handle quoted CSV: extract first field, remove surrounding quotes + if echo "$line" | grep -q '^"'; then + # First field is quoted - extract between first quote and comma + name=$(echo "$line" | sed 's/^"\([^"]*\)".*/\1/') + else + # First field is not quoted - extract up to first comma + name=$(echo "$line" | cut -d',' -f1) + fi + + name=$(echo "$name" | xargs) + [ -z "$name" ] && continue + + # Check if module exists in filesystem + if ! grep -Fxq "$name" "$FILESYSTEM_MODULES"; then + # Module not found in filesystem - output the full CSV line + echo "$line" >> "$MISSING_MODULES" + fi +done + +# Output missing modules if any were found +if [ -s "$MISSING_MODULES" ]; then + echo "Missing modules:" + echo "$DB_MODULES_CSV" | head -n 1 # Output CSV header + cat "$MISSING_MODULES" +fi diff --git a/app/sbin/ODOO_19/restore b/app/sbin/ODOO_19/restore new file mode 100755 index 0000000..5e55637 --- /dev/null +++ b/app/sbin/ODOO_19/restore @@ -0,0 +1,68 @@ +#!/bin/bash +export PATH=/4PROJECTS/bin:$PATH +if [ ! -n "$2" ]; then + echo "Missing Parameters " + exit 0 +fi + +UUID=$1 +echo "UUID: $UUID" + +source /4server/sbin/helpers + +get_contract_info + + +#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql') + +echo "PASSWORD $ODOO_DB_PASSWORD" + +echo "Restoring $FILENAME to $UUID" +echo "status of container" +doas docker ps -a --filter "id=$UUID" +echo "POSTGRES HOST: $POSTGRES_HOST" +BACKUP="/mnt/backup/$2" +TEMPLATE="/mnt/db_images/$2" +doas docker exec "$UUID" /bin/bash -c "[ -f $TEMPLATE ]" + +if doas docker exec "$UUID" /bin/bash -c "[ -f $BACKUP ]"; then + FILENAME="$BACKUP" +elif doas docker exec "$UUID" /bin/bash -c "[ -f $TEMPLATE ]"; then + FILENAME="$TEMPLATE" +else + echo "File not exists" + exit 0 +fi + + +### DELETE AND CREATE DATABASE +PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql -h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -d postgres -c " +SELECT pg_terminate_backend(pid) FROM pg_stat_activity +WHERE datname = '$UUID' AND pid <> pg_backend_pid(); +" +PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql -h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -d postgres -c " +DROP DATABASE IF EXISTS \"$UUID\"; +" + +PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \ + -h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres \ + -c "ALTER ROLE \"$UUID\" CREATEDB;" + + +doas docker exec "$UUID" rm -rf /home/odoo/.local/share/Odoo/filestore +doas docker exec "$UUID" rm -rf /root/.local/share/Odoo/filestore + +doas docker exec "$UUID" odoo db --db_host beedb -w "$ODOO_DB_PASSWORD" -r "$UUID" load "$UUID" $FILENAME -f + +PGPASSWORD="$POSTGRES_ADMIN_PASSWORD" psql \ + -h "$POSTGRES_HOST" -U "$POSTGRES_ADMIN_USER" -p "$POSTGRES_PORT" -d postgres \ + -c "ALTER ROLE \"$UUID\" NOCREATEDB;" + + +doas docker exec "$UUID" cp -r /root/.local/share/Odoo/filestore /home/odoo/.local/share/Odoo/filestore +doas docker exec "$UUID" chown -R odoo:odoo /home/odoo/.local + +doas docker exec "$UUID" mkdir -p /var/lib/odoo/.local/share/Odoo/ +doas docker exec "$UUID" ln -s /home/odoo/.local/share/Odoo/filestore /var/lib/odoo/.local/share/Odoo/filestore + +docker restart "$UUID" diff --git a/app/sbin/ODOO_19/shell b/app/sbin/ODOO_19/shell new file mode 100755 index 0000000..340a83b --- /dev/null +++ b/app/sbin/ODOO_19/shell @@ -0,0 +1,22 @@ +#!/bin/bash +export PATH=/4PROJECTS/bin:$PATH +if [ ! -n "$1" ]; then + echo "Missing Parameters " + exit 0 +fi + +UUID=$1 +echo "UUID: $UUID" + +source /4server/sbin/helpers + +get_contract_info + + +#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql') + +echo "PASSWORD $ODOO_DB_PASSWORD" + +echo "POSTGRES HOST: $POSTGRES_HOST" + +doas docker exec -it "$UUID" odoo shell --db_host beedb --db_password="$ODOO_DB_PASSWORD" -d "$UUID" --db_user="$UUID" diff --git a/app/sbin/ODOO_19/updateModules b/app/sbin/ODOO_19/updateModules new file mode 100755 index 0000000..7e0b575 --- /dev/null +++ b/app/sbin/ODOO_19/updateModules @@ -0,0 +1,60 @@ +#!/bin/bash +export PATH=/4PROJECTS/bin:$PATH + +# -------------------------------------------------- +# Validate parameter +# -------------------------------------------------- +if [ -z "$1" ]; then + echo "Missing Parameters " + exit 1 +fi + +UUID=$1 + +# -------------------------------------------------- +# Load environment helpers +# -------------------------------------------------- +source /4server/sbin/helpers + +if ! get_contract_info "$UUID"; then + echo "Error: Failed to load contract info for $UUID" >&2 + exit 1 +fi + +# -------------------------------------------------- +# Verify container exists +# -------------------------------------------------- +if ! doas docker ps -a --format "{{.Names}}" | grep -q "^${UUID}$"; then + echo "Error: Container $UUID does not exist" >&2 + exit 1 +fi + +echo "Updating all Odoo modules for database: $UUID" +echo "Using DB host: $POSTGRES_HOST" + +# -------------------------------------------------- +# Run Odoo update inside container +# -------------------------------------------------- +if ! doas docker exec -u odoo "$UUID" odoo \ + -u all \ + -d "$UUID" \ + --stop-after-init \ + --db_host="$POSTGRES_HOST" \ + --db_port="$POSTGRES_PORT" \ + --db_user="$POSTGRES_ADMIN_USER" \ + --db_password="$POSTGRES_ADMIN_PASSWORD" +then + echo "Error: Odoo module update failed for $UUID" >&2 + exit 1 +fi + +echo "Module update completed successfully for $UUID" + +# -------------------------------------------------- +# Restart container +# -------------------------------------------------- +echo "Starting container $UUID" +/4server/sbin/startContainer "$UUID" + +echo "Done." +exit 0 diff --git a/app/sbin/api b/app/sbin/api new file mode 100755 index 0000000..a0c21f1 --- /dev/null +++ b/app/sbin/api @@ -0,0 +1,580 @@ +#!/usr/bin/env python3 +import json +import os +import re +import sqlite3 +import subprocess +import time +from collections import deque +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, Optional + +import psutil +import uvicorn +from fastapi import Depends, FastAPI, HTTPException, Response +from fastapi.responses import PlainTextResponse, RedirectResponse +from fastapi.security.api_key import APIKeyHeader +from pydantic import BaseModel + +# ---------------------- Constants ---------------------- +DB_PATH = "/4server/data/contracts.db" +BIN_PATH = "/4server/sbin" +API_KEY = os.getenv("API_KEY", "your-secret-api-key") +VERSION = "API: 0.0.9" + +# ---------------------- FastAPI App ---------------------- +app = FastAPI() +api_key_header = APIKeyHeader(name="X-API-Key") + + +def verify_api_key(key: str = Depends(api_key_header)): + if key != API_KEY: + raise HTTPException(status_code=403, detail="Unauthorized") + + +# ---------------------- Helpers ---------------------- +def run_command(cmd: list[str]) -> str: + """Run a shell command and return stdout or raise HTTPException on error.""" + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + error_msg = ( + f"Command failed\n" + f"Command: {' '.join(cmd)}\n" + f"Return code: {result.returncode}\n" + f"Stdout: {result.stdout.strip()}\n" + f"Stderr: {result.stderr.strip() or 'None'}" + ) + raise HTTPException(status_code=500, detail=error_msg) + return result.stdout.strip() + + +def init_db(): + """Initialize the database with containers table.""" + os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS containers ( + ID INTEGER PRIMARY KEY AUTOINCREMENT, + UUID CHAR(50) UNIQUE, + email CHAR(100), + expires DATE, + tags TEXT, + env TEXT, + affiliate CHAR(30), + image CHAR(50), + history TEXT, + comment TEXT, + domains TEXT, + status CHAR(20), + created DATE, + bump DATE, + secret TEXT, + contract TEXT, + git integer, + backup_slots integer, + hdd integer, + workers integer, + utm_source text, + utm_medium text, + utm_campaign text, + domains_slots integer, + secret_list TEXT, + env_list TEXT + ) + """) + conn.commit() + conn.close() + + +def execute_db(query: str, params: tuple = (), fetch: bool = False): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute(query, params) + conn.commit() + data = cursor.fetchall() if fetch else None + conn.close() + if data and fetch: + return [dict(row) for row in data] + return None + + +# ---------------------- Models ---------------------- +class ContractItem(BaseModel): + quantity: int + name: str + product_id: int + features: Dict[str, Any] + + +class ContainerModel(BaseModel): + UUID: str + email: Optional[str] = None + expires: Optional[str] = None + tags: Optional[str] = None + image: Optional[str] = None + history: Optional[str] = None + comment: Optional[str] = None + domains: Optional[str] = None + status: Optional[str] = None + created: Optional[str] = None + bump: Optional[str] = None + git: Optional[int] = None + backup_slots: Optional[int] = None + hdd: Optional[int] = None + workers: Optional[int] = None + utm_source: Optional[str] = None + utm_medium: Optional[str] = None + utm_campaign: Optional[str] = None + domains_slots: Optional[int] = None + 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") + + +import json +from typing import Any, Dict, Optional + +from fastapi import Depends, FastAPI +from fastapi.responses import RedirectResponse +from pydantic import BaseModel + +app = FastAPI() + + +# ---------------------- Routes ---------------------- +@app.get("/", include_in_schema=False) +def redirect_to_odoo(): + return RedirectResponse(url="https://ODOO4PROJECTS.com") + + +@app.post("/container/update", dependencies=[Depends(verify_api_key)]) +def update_container(request: ContainerModel): + # Fetch existing record + existing = execute_db( + "SELECT * FROM containers WHERE UUID = ?", (request.UUID,), fetch=True + ) + if not existing: + # If record does not exist, insert a new one with all given fields + execute_db( + """ + INSERT INTO containers (UUID, email, expires, tags, image, history, + comment, domains, status, created, bump, git, backup_slots, hdd, workers, + 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 + existing = existing[0] # Assuming fetch returns list of dicts + updates = {} + params = [] + + # Only add fields that are not None in the request + for field in ContainerModel.__fields__: + if field == "UUID": + continue + value = getattr(request, field) + if value is not None: + updates[field] = value + params.append(value) + + if updates: + # Build SQL dynamically + set_clause = ", ".join(f"{k}=?" for k in updates.keys()) + params.append(request.UUID) # UUID for WHERE clause + execute_db(f"UPDATE containers SET {set_clause} WHERE UUID=?", tuple(params)) + return { + "UUID": request.UUID, + "status": "updated", + "fields_updated": list(updates.keys()), + } + + return {"UUID": request.UUID, "status": "no_change"} + + +@app.post("/container/start", dependencies=[Depends(verify_api_key)]) +def start_container(request: UUIDRequest): + return {"message": run_command([f"{BIN_PATH}/startContainer", request.UUID])} + + +@app.post("/container/restartAllContainers", dependencies=[Depends(verify_api_key)]) +def start_container(request: ImageRequest): + return {"message": run_command([f"{BIN_PATH}/restartContainers", request.image])} + + +@app.post("/container/stop", dependencies=[Depends(verify_api_key)]) +def stop_container(request: UUIDRequest): + try: + return {"message": run_command([f"{BIN_PATH}/stopContainer", request.UUID])} + except HTTPException: + # Command failed, but continue without error to the API + return {"message": "Container stop command executed (may have failed)"} + + +@app.post("/container/nuke", dependencies=[Depends(verify_api_key)]) +def nuke_container(request: UUIDRequest): + status = execute_db( + "SELECT status FROM containers WHERE UUID=?", (request.UUID,), fetch=True + ) + if not status or status[0]["status"] != "nuke": + raise HTTPException(400, "Container status is not 'nuke'") + return {"message": run_command([f"{BIN_PATH}/nukeContainer", request.UUID])} + + +@app.post("/container/info", dependencies=[Depends(verify_api_key)]) +def info_container(request: Optional[UUIDRequest] = None): + # Fields to select + fields = [ + "ID", + "UUID", + "email", + "expires", + "tags", + "image", + "history", + "comment", + "domains", + "status", + "created", + "git", + "backup_slots", + "hdd", + "workers", + "utm_source", + "utm_medium", + "utm_campaign", + "domains_slots", + "env_list", + ] + field_str = ", ".join(fields) + + # Execute query + if request: + rows = execute_db( + f"SELECT {field_str} FROM containers WHERE UUID=?", + (str(request.UUID),), + fetch=True, + ) + else: + rows = execute_db(f"SELECT {field_str} FROM containers", fetch=True) + + # Map rows to dicts safely + containers = [] + for row in rows: + if isinstance(row, dict): + # Already a dict (e.g., some DB wrappers) + containers.append(row) + else: + # Tuple/list -> map with fields + containers.append(dict(zip(fields, row))) + + # Wrap in n8n JSON format + n8n_items = [{"json": container} for container in containers] + + return n8n_items + + +@app.post("/container/bump", dependencies=[Depends(verify_api_key)]) +def bump_container(request: UUIDRequest): + today = datetime.utcnow().strftime("%Y-%m-%d") + execute_db("UPDATE containers SET bump=? WHERE UUID=?", (today, request.UUID)) + msg = run_command([f"{BIN_PATH}/bumpContainer", request.UUID]) + return {"message": msg, "bump_date": today} + + +@app.post("/container/quota", dependencies=[Depends(verify_api_key)]) +def container_quota(request: UUIDRequest): + output = run_command( + [ + "docker", + "stats", + request.UUID, + "--no-stream", + "--format", + "{{.MemUsage}},{{.BlockIO}}", + ] + ) + mem_usage, disk_usage = output.split(",") + return {"memory_usage": mem_usage, "disk_io": disk_usage} + + +# ---------------------- SYSTEM ---------------------- +@app.get("/system/containers", dependencies=[Depends(verify_api_key)]) +def get_containers(): + return Response( + content=run_command([f"{BIN_PATH}/getContainers"]), + media_type="application/json", + ) + + +@app.get("/system/images", dependencies=[Depends(verify_api_key)]) +def list_images(): + images = run_command(["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"]) + return {"images": images.split("\n")} + + +@app.get("/system/cpu", dependencies=[Depends(verify_api_key)]) +def get_cpu_log(): + CPU_LOG_PATH = Path("/4server/data/log/cpu.log") + if not CPU_LOG_PATH.exists(): + raise HTTPException(status_code=404, detail="CPU log file not found") + + try: + with CPU_LOG_PATH.open("r") as f: + content = f.read() + return PlainTextResponse(content) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error reading CPU log: {e}") + + +@app.get("/system/info", dependencies=[Depends(verify_api_key)]) +def get_system_info(): + try: + alpine_version = None + last_update = None + bump_dates = execute_db( + "SELECT MAX(bump) AS latest_bump FROM containers", fetch=True + )[0]["latest_bump"] + if os.path.exists("/4server/data/update"): + with open("/4server/data/update") as f: + last_update = f.read().strip() + if os.path.exists("/etc/alpine-release"): + with open("/etc/alpine-release") as f: + alpine_version = f.read().strip() + mem = psutil.virtual_memory() + disk = psutil.disk_usage("/") + cpu_count = psutil.cpu_count(logical=True) + return { + "alpine_version": alpine_version, + "last_update": last_update, + "latest_bump": bump_dates, + "version": VERSION, + "resources": { + "memory": { + "total": mem.total, + "available": mem.available, + "used": mem.used, + }, + "disk": {"total": disk.total, "used": disk.used, "free": disk.free}, + "cpu_count": cpu_count, + }, + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/system/pull", dependencies=[Depends(verify_api_key)]) +def pull_all_images(): + return {"message": run_command([f"{BIN_PATH}/pullAllContainers"])} + + +@app.post("/system/restartAllContainers", dependencies=[Depends(verify_api_key)]) +def restart_all_containers(request: ImageRequest): + if not request.image: + raise HTTPException(status_code=400, detail="Image name is required") + return {"message": run_command([f"{BIN_PATH}/restartContainers", request.image])} + + +@app.post("/client/git", dependencies=[Depends(verify_api_key)]) +def git_tool(request: CommandRequest): + if request.method == 1: + command = [f"{BIN_PATH}/gitPull", request.uuid] + elif request.method == 2: + command = [f"{BIN_PATH}/gitRevert", request.uuid] + else: + raise HTTPException(status_code=400, detail="Invalid method") + + output = run_command(command) + return {"message": output} + + +@app.post("/client/addGitKey", dependencies=[Depends(verify_api_key)]) +def add_git_key(request: AddKeyRequest): + if not request.key or not request.uuid: + raise HTTPException(status_code=400, detail="Key and uuid are required") + + file_path = f"/4server/data/{request.uuid}/git-server/keys/id_rsa.pub" + + try: + # Ensure the directory exists + os.makedirs(os.path.dirname(file_path), exist_ok=True) + # Write the key to the file + with open(file_path, "w", encoding="utf-8") as f: + f.write(request.key) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save key: {str(e)}") + + return {"message": f"Key saved for container {request.uuid}", "file": file_path} + + +@app.get("/client/audit/{uuid}", dependencies=[Depends(verify_api_key)]) +def audit_odoo(uuid: str): + return {"message": run_command([f"{BIN_PATH}/ODOO_19/listModules", uuid])} + + +@app.get("/client/logs/{uuid}", dependencies=[Depends(verify_api_key)]) +async def get_odoo_log_summary(uuid: str): + if not re.fullmatch(r"[0-9a-fA-F\-]+", uuid): + raise HTTPException( + status_code=400, + detail="Invalid UUID format. Only numbers, letters a-f, and '-' are allowed.", + ) + + BASE_LOG_DIR = "/4server/data" + + # Build file paths as strings + project_dir = os.path.join(BASE_LOG_DIR, uuid) + odoo_log_file = os.path.join(project_dir, "logs", "odoo.log") + git_log_file = os.path.join(project_dir, "logs", "git.log") + + if not os.path.isfile(odoo_log_file): + raise HTTPException(status_code=404, detail="Odoo log file not found") + + # --- Helper variables and functions --- + IMPORTANT_PATTERNS = [ + re.compile(r"odoo\.addons\."), # Any Odoo addon log + re.compile(r"Job '.*' starting"), # Cron job start + re.compile(r"Job '.*' fully done"), # Cron job end + re.compile(r"ERROR"), # Errors + re.compile(r"WARNING"), # Warnings + re.compile(r"Traceback"), # Tracebacks + re.compile(r"Error"), + ] + + def is_important_line(line: str) -> bool: + return any(p.search(line) for p in IMPORTANT_PATTERNS) + + def read_last_lines(file_path: str, max_lines: int) -> list[str]: + """ + Read the last `max_lines` from the file efficiently. + """ + if not os.path.isfile(file_path): + return [] + last_lines = deque(maxlen=max_lines) + with open(file_path, "r", encoding="utf-8", errors="ignore") as f: + for line in f: + last_lines.append(line.strip()) + return list(last_lines) + + # --- Main logic --- + try: + # Last 500 lines from Odoo log + last_500_lines = read_last_lines(odoo_log_file, 500) + important_odoo_lines = [ + line for line in last_500_lines if is_important_line(line) + ] + + # Last 50 lines from git.log + last_50_git_lines = read_last_lines(git_log_file, 50) + + return { + "uuid": str(uuid), + "important_odoo_log_lines": important_odoo_lines, + "last_git_log_lines": last_50_git_lines, + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error reading log files: {e}") + + +# ------------------------ BACKUP HANDLING ------------------------------------- +@app.post("/backup/import", dependencies=[Depends(verify_api_key)]) +def backup_import(request: ImportRequest): + if not request.filename: + raise HTTPException(status_code=400, detail="Filename is required") + + command = [f"{BIN_PATH}/ODOO_19/import", request.filename] + output = run_command(command) + return {"message": output} + + +@app.post("/backup/move", dependencies=[Depends(verify_api_key)]) +def backup_move(request: MoveRequest): + if not request.source or not request.destination: + raise HTTPException( + status_code=400, detail="Source and destination are required" + ) + + if not os.path.exists(request.source): + raise HTTPException(status_code=404, detail="Source file does not exist") + + # Use shell command to move the file + command = ["mv", request.source, request.destination] # Linux/macOS + # For Windows, use: command = ["move", request.source, request.destination] + + output = run_command(command) + return { + "message": f"Moved {request.source} to {request.destination}", + "output": output, + } + + +@app.get("/backup/borgpush", dependencies=[Depends(verify_api_key)]) +def backup_borgpush(): + return {"message": run_command(["doas", "-u", "4server", f"{BIN_PATH}/borgpush"])} + + +# ---------------------- Entry Point ---------------------- +if __name__ == "__main__": + print(VERSION) + init_db() + time.sleep(25) + uvicorn.run(app, host="10.5.0.1", port=8888) diff --git a/app/sbin/api.orig b/app/sbin/api.orig new file mode 100755 index 0000000..43f5506 --- /dev/null +++ b/app/sbin/api.orig @@ -0,0 +1,591 @@ +#!/usr/bin/env python3 +import json +import os +import re +import sqlite3 +import subprocess +import time +from collections import deque +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, Optional + +import psutil +import uvicorn +from fastapi import Depends, FastAPI, HTTPException, Response +from fastapi.responses import PlainTextResponse, RedirectResponse +from fastapi.security.api_key import APIKeyHeader +from pydantic import BaseModel + +# ---------------------- Constants ---------------------- +DB_PATH = "/4server/data/contracts.db" +BIN_PATH = "/4server/sbin" +API_KEY = os.getenv("API_KEY", "your-secret-api-key") +VERSION = "API: 0.0.9" + +# ---------------------- FastAPI App ---------------------- +app = FastAPI() +api_key_header = APIKeyHeader(name="X-API-Key") + + +def verify_api_key(key: str = Depends(api_key_header)): + if key != API_KEY: + raise HTTPException(status_code=403, detail="Unauthorized") + + +# ---------------------- Helpers ---------------------- +def run_command(cmd: list[str]) -> str: + """Run a shell command and return stdout or raise HTTPException on error.""" + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + error_msg = ( + f"Command failed\n" + f"Command: {' '.join(cmd)}\n" + f"Return code: {result.returncode}\n" + f"Stdout: {result.stdout.strip()}\n" + f"Stderr: {result.stderr.strip() or 'None'}" + ) + raise HTTPException(status_code=500, detail=error_msg) + return result.stdout.strip() + + +def init_db(): + """Initialize the database with containers table.""" + os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS containers ( + ID INTEGER PRIMARY KEY AUTOINCREMENT, + UUID CHAR(50) UNIQUE, + email CHAR(100), + expires DATE, + tags TEXT, + env TEXT, + affiliate CHAR(30), + image CHAR(50), + history TEXT, + comment TEXT, + domains TEXT, + status CHAR(20), + created DATE, + bump DATE, + secret TEXT, + contract TEXT, + git integer, + backup_slots integer, + hdd integer, + workers integer, + utm_source text, + utm_medium text, + utm_campaign text, + domains_slots integer, + secret_list TEXT, + env_list TEXT + ) + """) + conn.commit() + conn.close() + + +def execute_db(query: str, params: tuple = (), fetch: bool = False): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute(query, params) + conn.commit() + data = cursor.fetchall() if fetch else None + conn.close() + if data and fetch: + return [dict(row) for row in data] + return None + + +# ---------------------- Models ---------------------- +class ContractItem(BaseModel): + quantity: int + name: str + product_id: int + features: Dict[str, Any] + + +class ContainerModel(BaseModel): + UUID: str + email: Optional[str] = None + expires: Optional[str] = None + tags: Optional[str] = None + image: Optional[str] = None + history: Optional[str] = None + comment: Optional[str] = None + domains: Optional[str] = None + status: Optional[str] = None + created: Optional[str] = None + bump: Optional[str] = None + git: Optional[int] = None + backup_slots: Optional[int] = None + hdd: Optional[int] = None + workers: Optional[int] = None + utm_source: Optional[str] = None + utm_medium: Optional[str] = None + utm_campaign: Optional[str] = None + domains_slots: Optional[int] = None +<<<<<<< HEAD + secret_list: Optional[str] = None +======= +>>>>>>> c583649 (hmm) + env_list: Optional[str] = None + + +class UUIDRequest(BaseModel): + UUID: str + + +class CommandRequest(BaseModel): + uuid: str + method: int + + +class ImportRequest(BaseModel): + filename: str + + +class MoveRequest(BaseModel): + source: str + destination: str + + +class AddKeyRequest(BaseModel): + key: str + uuid: str + + +class ImageRequest(BaseModel): + image: str + + +# ---------------------- Routes ---------------------- +@app.get("/", include_in_schema=False) +def redirect_to_odoo(): + return RedirectResponse(url="https://ODOO4PROJECTS.com") + + +import json +from typing import Any, Dict, Optional + +from fastapi import Depends, FastAPI +from fastapi.responses import RedirectResponse +from pydantic import BaseModel + +app = FastAPI() + + +# ---------------------- Routes ---------------------- +@app.get("/", include_in_schema=False) +def redirect_to_odoo(): + return RedirectResponse(url="https://ODOO4PROJECTS.com") + + +@app.post("/container/update", dependencies=[Depends(verify_api_key)]) +def update_container(request: ContainerModel): + # Fetch existing record + existing = execute_db( + "SELECT * FROM containers WHERE UUID = ?", (request.UUID,), fetch=True + ) + if not existing: + # If record does not exist, insert a new one with all given fields + execute_db( + """ + INSERT INTO containers (UUID, email, expires, tags, image, history, + comment, domains, status, created, bump, git, backup_slots, hdd, workers, +<<<<<<< HEAD + utm_source, utm_medium, utm_campaign, domains_slots, secret_list, env_list) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +======= + utm_source, utm_medium, utm_campaign, domains_slots, env_list) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +>>>>>>> c583649 (hmm) + """, + ( + request.UUID, + request.email, + request.expires, + request.tags, + request.image, + request.history, + request.comment, + request.domains, + request.status, + request.created, + request.bump, + request.git, + request.backup_slots, + request.hdd, + request.workers, + request.utm_source, + request.utm_medium, + request.utm_campaign, + request.domains_slots, +<<<<<<< HEAD + request.secret_list, +======= +>>>>>>> c583649 (hmm) + request.env_list, + ), + ) + return {"UUID": request.UUID, "status": "created"} + + # Existing record found, do partial update + existing = existing[0] # Assuming fetch returns list of dicts + updates = {} + params = [] + + # Only add fields that are not None in the request + for field in ContainerModel.__fields__: + if field == "UUID": + continue + value = getattr(request, field) + if value is not None: + updates[field] = value + params.append(value) + + if updates: + # Build SQL dynamically + set_clause = ", ".join(f"{k}=?" for k in updates.keys()) + params.append(request.UUID) # UUID for WHERE clause + execute_db(f"UPDATE containers SET {set_clause} WHERE UUID=?", tuple(params)) + return { + "UUID": request.UUID, + "status": "updated", + "fields_updated": list(updates.keys()), + } + + return {"UUID": request.UUID, "status": "no_change"} + + +@app.post("/container/start", dependencies=[Depends(verify_api_key)]) +def start_container(request: UUIDRequest): + return {"message": run_command([f"{BIN_PATH}/startContainer", request.UUID])} + + +@app.post("/container/restartAllContainers", dependencies=[Depends(verify_api_key)]) +def start_container(request: ImageRequest): + return {"message": run_command([f"{BIN_PATH}/restartContainers", request.image])} + + +@app.post("/container/stop", dependencies=[Depends(verify_api_key)]) +def stop_container(request: UUIDRequest): + try: + return {"message": run_command([f"{BIN_PATH}/stopContainer", request.UUID])} + except HTTPException: + # Command failed, but continue without error to the API + return {"message": "Container stop command executed (may have failed)"} + + +@app.post("/container/nuke", dependencies=[Depends(verify_api_key)]) +def nuke_container(request: UUIDRequest): + status = execute_db( + "SELECT status FROM containers WHERE UUID=?", (request.UUID,), fetch=True + ) + if not status or status[0]["status"] != "nuke": + raise HTTPException(400, "Container status is not 'nuke'") + return {"message": run_command([f"{BIN_PATH}/nukeContainer", request.UUID])} + + +@app.post("/container/info", dependencies=[Depends(verify_api_key)]) +def info_container(request: Optional[UUIDRequest] = None): + # Fields to select + fields = [ + "ID", + "UUID", + "email", + "expires", + "tags", + "image", + "history", + "comment", + "domains", + "status", + "created", + "git", + "backup_slots", + "hdd", + "workers", + "utm_source", + "utm_medium", + "utm_campaign", + "domains_slots", + "env_list", + ] + field_str = ", ".join(fields) + + # Execute query + if request: + rows = execute_db( + f"SELECT {field_str} FROM containers WHERE UUID=?", + (str(request.UUID),), + fetch=True, + ) + else: + rows = execute_db(f"SELECT {field_str} FROM containers", fetch=True) + + # Map rows to dicts safely + containers = [] + for row in rows: + if isinstance(row, dict): + # Already a dict (e.g., some DB wrappers) + containers.append(row) + else: + # Tuple/list -> map with fields + containers.append(dict(zip(fields, row))) + + # Wrap in n8n JSON format + n8n_items = [{"json": container} for container in containers] + + return n8n_items + + +@app.post("/container/bump", dependencies=[Depends(verify_api_key)]) +def bump_container(request: UUIDRequest): + today = datetime.utcnow().strftime("%Y-%m-%d") + execute_db("UPDATE containers SET bump=? WHERE UUID=?", (today, request.UUID)) + msg = run_command([f"{BIN_PATH}/bumpContainer", request.UUID]) + return {"message": msg, "bump_date": today} + + +@app.post("/container/quota", dependencies=[Depends(verify_api_key)]) +def container_quota(request: UUIDRequest): + output = run_command( + [ + "docker", + "stats", + request.UUID, + "--no-stream", + "--format", + "{{.MemUsage}},{{.BlockIO}}", + ] + ) + mem_usage, disk_usage = output.split(",") + return {"memory_usage": mem_usage, "disk_io": disk_usage} + + +# ---------------------- SYSTEM ---------------------- +@app.get("/system/containers", dependencies=[Depends(verify_api_key)]) +def get_containers(): + return Response( + content=run_command([f"{BIN_PATH}/getContainers"]), + media_type="application/json", + ) + + +@app.get("/system/images", dependencies=[Depends(verify_api_key)]) +def list_images(): + images = run_command(["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"]) + return {"images": images.split("\n")} + + +@app.get("/system/cpu", dependencies=[Depends(verify_api_key)]) +def get_cpu_log(): + CPU_LOG_PATH = Path("/4server/data/log/cpu.log") + if not CPU_LOG_PATH.exists(): + raise HTTPException(status_code=404, detail="CPU log file not found") + + try: + with CPU_LOG_PATH.open("r") as f: + content = f.read() + return PlainTextResponse(content) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error reading CPU log: {e}") + + +@app.get("/system/info", dependencies=[Depends(verify_api_key)]) +def get_system_info(): + try: + alpine_version = None + last_update = None + bump_dates = execute_db( + "SELECT MAX(bump) AS latest_bump FROM containers", fetch=True + )[0]["latest_bump"] + if os.path.exists("/4server/data/update"): + with open("/4server/data/update") as f: + last_update = f.read().strip() + if os.path.exists("/etc/alpine-release"): + with open("/etc/alpine-release") as f: + alpine_version = f.read().strip() + mem = psutil.virtual_memory() + disk = psutil.disk_usage("/") + cpu_count = psutil.cpu_count(logical=True) + return { + "alpine_version": alpine_version, + "last_update": last_update, + "latest_bump": bump_dates, + "version": VERSION, + "resources": { + "memory": { + "total": mem.total, + "available": mem.available, + "used": mem.used, + }, + "disk": {"total": disk.total, "used": disk.used, "free": disk.free}, + "cpu_count": cpu_count, + }, + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/system/pull", dependencies=[Depends(verify_api_key)]) +def pull_all_images(): + return {"message": run_command([f"{BIN_PATH}/pullAllContainers"])} + + +@app.post("/system/restartAllContainers", dependencies=[Depends(verify_api_key)]) +def restart_all_containers(request: ImageRequest): + if not request.image: + raise HTTPException(status_code=400, detail="Image name is required") + return {"message": run_command([f"{BIN_PATH}/restartContainers", request.image])} + + +@app.post("/client/git", dependencies=[Depends(verify_api_key)]) +def git_tool(request: CommandRequest): + if request.method == 1: + command = [f"{BIN_PATH}/gitPull", request.uuid] + elif request.method == 2: + command = [f"{BIN_PATH}/gitRevert", request.uuid] + else: + raise HTTPException(status_code=400, detail="Invalid method") + + output = run_command(command) + return {"message": output} + + +@app.post("/client/addGitKey", dependencies=[Depends(verify_api_key)]) +def add_git_key(request: AddKeyRequest): + if not request.key or not request.uuid: + raise HTTPException(status_code=400, detail="Key and uuid are required") + + file_path = f"/4server/data/{request.uuid}/git-server/keys/id_rsa.pub" + + try: + # Ensure the directory exists + os.makedirs(os.path.dirname(file_path), exist_ok=True) + # Write the key to the file + with open(file_path, "w", encoding="utf-8") as f: + f.write(request.key) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save key: {str(e)}") + + return {"message": f"Key saved for container {request.uuid}", "file": file_path} + + +@app.get("/client/audit/{uuid}", dependencies=[Depends(verify_api_key)]) +def audit_odoo(uuid: str): + return {"message": run_command([f"{BIN_PATH}/ODOO_19/listModules", uuid])} + + +@app.get("/client/logs/{uuid}", dependencies=[Depends(verify_api_key)]) +async def get_odoo_log_summary(uuid: str): + if not re.fullmatch(r"[0-9a-fA-F\-]+", uuid): + raise HTTPException( + status_code=400, + detail="Invalid UUID format. Only numbers, letters a-f, and '-' are allowed.", + ) + + BASE_LOG_DIR = "/4server/data" + + # Build file paths as strings + project_dir = os.path.join(BASE_LOG_DIR, uuid) + odoo_log_file = os.path.join(project_dir, "logs", "odoo.log") + git_log_file = os.path.join(project_dir, "logs", "git.log") + + if not os.path.isfile(odoo_log_file): + raise HTTPException(status_code=404, detail="Odoo log file not found") + + # --- Helper variables and functions --- + IMPORTANT_PATTERNS = [ + re.compile(r"odoo\.addons\."), # Any Odoo addon log + re.compile(r"Job '.*' starting"), # Cron job start + re.compile(r"Job '.*' fully done"), # Cron job end + re.compile(r"ERROR"), # Errors + re.compile(r"WARNING"), # Warnings + re.compile(r"Traceback"), # Tracebacks + re.compile(r"Error"), + ] + + def is_important_line(line: str) -> bool: + return any(p.search(line) for p in IMPORTANT_PATTERNS) + + def read_last_lines(file_path: str, max_lines: int) -> list[str]: + """ + Read the last `max_lines` from the file efficiently. + """ + if not os.path.isfile(file_path): + return [] + last_lines = deque(maxlen=max_lines) + with open(file_path, "r", encoding="utf-8", errors="ignore") as f: + for line in f: + last_lines.append(line.strip()) + return list(last_lines) + + # --- Main logic --- + try: + # Last 500 lines from Odoo log + last_500_lines = read_last_lines(odoo_log_file, 500) + important_odoo_lines = [ + line for line in last_500_lines if is_important_line(line) + ] + + # Last 50 lines from git.log + last_50_git_lines = read_last_lines(git_log_file, 50) + + return { + "uuid": str(uuid), + "important_odoo_log_lines": important_odoo_lines, + "last_git_log_lines": last_50_git_lines, + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error reading log files: {e}") + + +# ------------------------ BACKUP HANDLING ------------------------------------- +@app.post("/backup/import", dependencies=[Depends(verify_api_key)]) +def backup_import(request: ImportRequest): + if not request.filename: + raise HTTPException(status_code=400, detail="Filename is required") + + command = [f"{BIN_PATH}/ODOO_19/import", request.filename] + output = run_command(command) + return {"message": output} + + +@app.post("/backup/move", dependencies=[Depends(verify_api_key)]) +def backup_move(request: MoveRequest): + if not request.source or not request.destination: + raise HTTPException( + status_code=400, detail="Source and destination are required" + ) + + if not os.path.exists(request.source): + raise HTTPException(status_code=404, detail="Source file does not exist") + + # Use shell command to move the file + command = ["mv", request.source, request.destination] # Linux/macOS + # For Windows, use: command = ["move", request.source, request.destination] + + output = run_command(command) + return { + "message": f"Moved {request.source} to {request.destination}", + "output": output, + } + + +@app.get("/backup/borgpush", dependencies=[Depends(verify_api_key)]) +def backup_borgpush(): + return {"message": run_command(["doas", "-u", "4server", f"{BIN_PATH}/borgpush"])} + + +# ---------------------- Entry Point ---------------------- +if __name__ == "__main__": + print(VERSION) + init_db() + time.sleep(25) + uvicorn.run(app, host="10.5.0.1", port=8888) diff --git a/app/sbin/backupContainer b/app/sbin/backupContainer new file mode 100755 index 0000000..dbcb578 --- /dev/null +++ b/app/sbin/backupContainer @@ -0,0 +1,42 @@ +#!/bin/bash +exec >> /4server/data/log/backupContainer.log 2>&1 +echo "$(date '+%Y-%m-%d %H:%M') Backup container $1" + +source /4server/sbin/helpers + +BIN_PATH="/4server/sbin" + +UUID="$1" + +if [[ -z "$UUID" ]]; then + echo "Usage: $0 " + exit 1 +fi + +get_contract_info + +# Extract the second part of UUID (split by "-") +SECOND_PART=$(echo "$UUID" | cut -d'-' -f2) + +echo "Conatiner Type $UUID" + +# Decide which script to run +case "$SECOND_PART" in + 001) + "$BIN_PATH/backup/N8N" + ;; + 002) + "$BIN_PATH/backup/ODOO" + ;; + 003) + "$BIN_PATH/backup/ODOO" + ;; + 004) + "$BIN_PATH/backup/ODOO" + ;; + + *) + echo "Unknown UUID type: $SECOND_PART" + exit 2 + ;; +esac diff --git a/app/sbin/borgpush b/app/sbin/borgpush new file mode 100755 index 0000000..4bedf9f --- /dev/null +++ b/app/sbin/borgpush @@ -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" diff --git a/app/sbin/checkCalls b/app/sbin/checkCalls new file mode 100755 index 0000000..6cd6e81 --- /dev/null +++ b/app/sbin/checkCalls @@ -0,0 +1,41 @@ +#!/bin/bash + +cd /4server/data/ + +while : +do + +for dir in ???-???-*; do + if [ -d "${dir}/cc" ]; then + if [ -f "${dir}/cc/backup" ]; then + echo "BACKUP for: ${dir%/}" + /4server/sbin/backupContainer ${dir%/} 2 + rm "${dir}/cc/backup" + fi + + if [ -f "${dir}/cc/restart" ]; then + echo "Restart for: ${dir%/}" + /4server/sbin/startContainer ${dir%/} + rm "${dir}/cc/restart" + fi + + if [ -f "${dir}/cc/restore" ]; then + FILENAME=$(head -n 1 "${dir}/cc/restore") + echo "Restore for: ${dir%/} - $FILENAME" + /4server/sbin/ODOO_19/restore ${dir%/} $FILENAME + rm "${dir}/cc/restore" + fi + fi + + if [ -d "${dir}/n8n" ]; then + if [ -f "${dir}/n8n/database.sqlite-journal" ]; then + echo "Restart N8N (journal)for: ${dir%/}" + /4server/sbin/startContainer ${dir%/} + fi + fi + +done + +sleep 60 + +done diff --git a/app/sbin/cleanTmp b/app/sbin/cleanTmp new file mode 100755 index 0000000..2b9f527 --- /dev/null +++ b/app/sbin/cleanTmp @@ -0,0 +1,8 @@ +#!/bin/bash + +while true; do + find /4server/tmp -type f -atime +2 -delete + find /4server/tmp -type d -empty -delete + sleep 1d +done + diff --git a/app/sbin/contractInfo b/app/sbin/contractInfo new file mode 100755 index 0000000..0913191 --- /dev/null +++ b/app/sbin/contractInfo @@ -0,0 +1,14 @@ +export PATH=/4PROJECTS/bin:$PATH +if [ ! -n "$1" ]; then + echo "Missing Parameters " + exit 0 +fi + +UUID=$1 +echo "UUID: $UUID" + +source /4server/sbin/helpers + +get_contract_info + +env diff --git a/app/sbin/cpu b/app/sbin/cpu new file mode 100755 index 0000000..cd71686 --- /dev/null +++ b/app/sbin/cpu @@ -0,0 +1,39 @@ +#!/bin/sh + +# Log file +OUTPUT="/4server/data/log/cpu_idle.log" + +# Sampling interval in seconds +INTERVAL=60 + +while true; do + DATA="" # buffer to store idle samples + + # Current date for the measurement period + DATE=$(date "+%Y-%m-%d") + + echo "Starting measurement for $DATE" + + while [ "$(date +%H:%M)" != "23:45" ]; do + # Get idle CPU percentage + IDLE=$(mpstat 1 1 | awk '/Average/ {print $12}') + + # Append to buffer + DATA="$DATA$IDLE\n" + + sleep $INTERVAL + done + + # Write all data to log file with date + # Only one line per day: Date + space-separated idle samples + echo -n "$DATE " >> "$OUTPUT" + echo -e "$DATA" | tr '\n' ' ' >> "$OUTPUT" + echo >> "$OUTPUT" # newline at the end + echo "Measurement for $DATE written to $OUTPUT" + + # Wait until 00:15 to start next day + while [ "$(date +%H:%M)" != "00:15" ]; do + sleep 30 + done +done + diff --git a/app/sbin/getContainers b/app/sbin/getContainers new file mode 100755 index 0000000..544032a --- /dev/null +++ b/app/sbin/getContainers @@ -0,0 +1,61 @@ +#!/bin/bash + +# Collect running container IDs +container_ids=$(docker ps -q) + +# Start JSON array +echo "[" + +first=true + +for cid in $container_ids; do + # Get basic info + name=$(docker inspect --format '{{.Name}}' "$cid" | sed 's/^\/\(.*\)/\1/') + image=$(docker inspect --format '{{.Config.Image}}' "$cid") + container_id=$(docker inspect --format '{{.Id}}' "$cid") + + # RAM usage in bytes (from docker stats) + mem_usage=$(docker stats --no-stream --format "{{.MemUsage}}" "$cid") + # mem_usage looks like "12.34MiB / 1.944GiB" — extract the used part + mem_used=$(echo "$mem_usage" | awk '{print $1}') + + # Convert mem_used to MiB as a number + # Support KiB, MiB, GiB units + ram_usage=$(echo "$mem_used" | awk ' + { + val = substr($0, 1, length($0)-3) + unit = substr($0, length($0)-2, 3) + if (unit == "KiB") print val / 1024 + else if (unit == "MiB") print val + else if (unit == "GiB") print val * 1024 + else print 0 + }') + + # Traefik labels (extract labels JSON) + labels=$(docker inspect --format '{{json .Config.Labels}}' "$cid" | jq -r 'to_entries | map(select(.key | test("^traefik.*"))) | map({(.key): .value}) | add') + + # Comma between JSON objects + if [ "$first" = false ]; then + echo "," + fi + first=false + + # Output JSON object + jq -n \ + --arg name "$name" \ + --arg id "$container_id" \ + --arg image "$image" \ + --argjson ram "$ram_usage" \ + --argjson tags "$labels" \ + '{ + name: $name, + id: $id, + image: $image, + ram_mb: $ram, + traefik_tags: $tags + }' +done + +# End JSON array +echo "]" + diff --git a/app/sbin/gitPull b/app/sbin/gitPull new file mode 100755 index 0000000..ba2d78c --- /dev/null +++ b/app/sbin/gitPull @@ -0,0 +1,4 @@ +#!/bin/bash + +docker exec $1 /gitPull + diff --git a/app/sbin/gitRevert b/app/sbin/gitRevert new file mode 100755 index 0000000..6f6d3e2 --- /dev/null +++ b/app/sbin/gitRevert @@ -0,0 +1,4 @@ +#!/bin/bash + +docker exec $1 /gitRollBack + diff --git a/app/sbin/helpers b/app/sbin/helpers new file mode 100755 index 0000000..3e7b56a --- /dev/null +++ b/app/sbin/helpers @@ -0,0 +1,51 @@ +#!/bin/bash + +POSTGRES_HOST="${POSTGRES_HOST:-beedb}" +POSTGRES_PORT="${POSTGRES_PORT:-5432}" +POSTGRES_ADMIN_USER="${POSTGRES_ADMIN_USER:-1gtT0sf8klB9lDbYZD9}" +POSTGRES_ADMIN_PASSWORD="${POSTGRES_ADMIN_PASSWORD:-ZpSwWNafyy9GhY2gzHw}" + +DB_PATH="/4server/data/contracts.db" + + +get_contract_info() { + + echo "get_contract_info $UUID" + + # Single CTE: the table is scanned once and all projections come from it + while IFS="=" read -r key value; do + if [ -n "$key" ]; then + export "$key=$value" + fi + done < <(sqlite3 "$DB_PATH" " + WITH c AS (SELECT * FROM containers WHERE UUID='$UUID' LIMIT 1) + SELECT 'UUID=' || COALESCE(UUID, '') FROM c UNION ALL + SELECT 'EXPIRES=' || COALESCE(expires, '') FROM c UNION ALL + SELECT 'IMAGE=' || COALESCE(image, '') FROM c UNION ALL + SELECT 'STATUS=' || COALESCE(status, '') FROM c UNION ALL + SELECT 'CREATED=' || COALESCE(created, '') FROM c UNION ALL + SELECT 'SECRET_LIST=' || COALESCE(secret_list, '') FROM c UNION ALL + SELECT 'CONTAINERDBID=' || COALESCE(id, '') FROM c UNION ALL + SELECT 'GIT=' || COALESCE(git, '') FROM c UNION ALL + SELECT 'BACKUP_SLOTS=' || COALESCE(backup_slots, '') FROM c UNION ALL + SELECT 'HDD=' || COALESCE(hdd, '') FROM c UNION ALL + SELECT 'WORKERS=' || COALESCE(workers, '') FROM c UNION ALL + SELECT 'DOMAINS_SLOTS=' || COALESCE(domains_slots, '') FROM c; + ") + + # Parse SECRET_LIST: backslash-separated key=value pairs (e.g. worex1=1\pswl=22) + # - printf avoids echo interpreting escape sequences + # - tr -d '\r\n' strips any embedded newlines the DB value may contain + # - tr '\\' '\n' turns each backslash separator into a real newline + # - %%=* / #*= split on the FIRST = only, so base64 values like tok=ab== are safe + if [ -n "$SECRET_LIST" ]; then + while IFS= read -r pair || [ -n "$pair" ]; do + _key="${pair%%=*}" + _value="${pair#*=}" + if [ -n "$_key" ]; then + export "$_key=$_value" + fi + done < <(printf '%s' "$SECRET_LIST" | tr -d '\r\n' | tr '\\' '\n') + fi + export ODOO_DB_PASSWORD=$psql +} diff --git a/app/sbin/helpers.orig b/app/sbin/helpers.orig new file mode 100755 index 0000000..6f3712d --- /dev/null +++ b/app/sbin/helpers.orig @@ -0,0 +1,87 @@ +#!/bin/bash + +POSTGRES_HOST="${POSTGRES_HOST:-beedb}" +POSTGRES_PORT="${POSTGRES_PORT:-5432}" +POSTGRES_ADMIN_USER="${POSTGRES_ADMIN_USER:-1gtT0sf8klB9lDbYZD9}" +POSTGRES_ADMIN_PASSWORD="${POSTGRES_ADMIN_PASSWORD:-ZpSwWNafyy9GhY2gzHw}" + +DB_PATH="/4server/data/contracts.db" + + +get_contract_info() { + + echo "get_contract_info $UUID" + + # Single CTE: the table is scanned once and all projections come from it + while IFS="=" read -r key value; do + if [ -n "$key" ]; then + export "$key=$value" + fi + done < <(sqlite3 "$DB_PATH" " + WITH c AS (SELECT * FROM containers WHERE UUID='$UUID' LIMIT 1) + SELECT 'UUID=' || COALESCE(UUID, '') FROM c UNION ALL + SELECT 'EXPIRES=' || COALESCE(expires, '') FROM c UNION ALL + SELECT 'IMAGE=' || COALESCE(image, '') FROM c UNION ALL + SELECT 'STATUS=' || COALESCE(status, '') FROM c UNION ALL + SELECT 'CREATED=' || COALESCE(created, '') FROM c UNION ALL + SELECT 'SECRET_LIST=' || COALESCE(secret_list, '') FROM c UNION ALL + SELECT 'CONTAINERDBID=' || COALESCE(id, '') FROM c UNION ALL + SELECT 'GIT=' || COALESCE(git, '') FROM c UNION ALL + SELECT 'BACKUP_SLOTS=' || COALESCE(backup_slots, '') FROM c UNION ALL + SELECT 'HDD=' || COALESCE(hdd, '') FROM c UNION ALL + SELECT 'WORKERS=' || COALESCE(workers, '') FROM c UNION ALL + SELECT 'DOMAINS_SLOTS=' || COALESCE(domains_slots, '') FROM c; + ") + + # Parse SECRET_LIST: backslash-separated key=value pairs (e.g. worex1=1\pswl=22) + # - printf avoids echo interpreting escape sequences + # - tr -d '\r\n' strips any embedded newlines the DB value may contain + # - tr '\\' '\n' turns each backslash separator into a real newline + # - %%=* / #*= split on the FIRST = only, so base64 values like tok=ab== are safe + if [ -n "$SECRET_LIST" ]; then + while IFS= read -r pair || [ -n "$pair" ]; do + _key="${pair%%=*}" + _value="${pair#*=}" + if [ -n "$_key" ]; then + export "$_key=$_value" + fi + done < <(printf '%s' "$SECRET_LIST" | tr -d '\r\n' | tr '\\' '\n') + fi +<<<<<<< HEAD + export ODOO_DB_PASSWORD=$psql +======= +done < <(sqlite3 "$DB_PATH" " + SELECT 'UUID=' || UUID FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'EMAIL=' || email FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'EXPIRES=' || expires FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'TAGS=' || replace(replace(tags, char(10), ''), char(13), '') FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'ENV=' || replace(replace(env, char(10), ''), char(13), '') FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'IMAGE=' || image FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'DOMAINS=' || domains FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'STATUS=' || status FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'CREATED=' || created FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'SECRET_LIST=' || COALESCE(secret_list, '') FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'CONTAINERDBID=' || id FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'BUMP=' || bump FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'GIT=' || COALESCE(git, '') FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'BACKUP_SLOTS=' || COALESCE(backup_slots, '') FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'HDD=' || COALESCE(hdd, '') FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'WORKERS=' || COALESCE(workers, '') FROM containers WHERE UUID='$UUID' + UNION ALL SELECT 'DOMAINS_SLOTS=' || COALESCE(domains_slots, '') FROM containers WHERE UUID='$UUID'; +") + + +# Parse ENV JSON into individual env vars +eval $(echo "$ENV" | jq -r 'to_entries | .[] | "export \(.key | ascii_upcase)=\(.value)"') + +# Parse SECRET_LIST (backslash-separated key=value pairs, e.g. worex1=1\pswl=22) +if [ -n "$SECRET_LIST" ]; then + while IFS='=' read -r key value; do + if [ -n "$key" ]; then + export "$key=$value" + fi + done < <(echo "$SECRET_LIST" | tr '\\' '\n') +fi + +>>>>>>> c583649 (hmm) +} diff --git a/app/sbin/migrate b/app/sbin/migrate new file mode 100755 index 0000000..f75e4cf --- /dev/null +++ b/app/sbin/migrate @@ -0,0 +1,16 @@ +#!/bin/bash + +sqlite3 /4server/data/contracts.db < +# Example: ./start_by_uuid.sh abc-001-xxxx-xxxx-xxxx +exec > /4server/data/log/nukeContainer.log 2>&1 +echo "$(date '+%Y-%m-%d %H:%M') Nuke container $1" + +source /4server/sbin/helpers + +BIN_PATH="/4server/sbin" + +UUID="$1" + +if [[ -z "$UUID" ]]; then + echo "Usage: $0 " + exit 1 +fi + + +get_container_status() { + local uuid="$1" + # Get the container ID or name matching the UUID + CONTAINER_ID=$(docker ps -a --filter "name=$uuid" --format "{{.ID}}") + if [[ -z "$CONTAINER_ID" ]]; then + echo "not_found" + return + fi + + STATUS=$(docker inspect -f '{{.State.Status}}' "$CONTAINER_ID") + echo "$STATUS" +} + +# Check if container exists +STATUS=$(get_container_status "$UUID") +if [[ "$STATUS" == "running" ]]; then + echo "Container $UUID is still running. Aborting deletion." + exit 2 +fi + + + + +get_contract_info + +# Extract the second part of UUID (split by "-") +SECOND_PART=$(echo "$UUID" | cut -d'-' -f2) + +# Decide which script to run +case "$SECOND_PART" in + 001) + "$BIN_PATH/nuke/n8n" + ;; + 002) + "$BIN_PATH/nuke/ODOO_19" + ;; + *) + echo "Unknown UUID type: $SECOND_PART" + exit 2 + ;; +esac + +#sqlite3 "/4server/data/contracts.db" < + +CONTAINER=$1 + +if [ -z "$CONTAINER" ]; then + echo "Usage: $0 " + 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 - < /4server/data/log/system_pull.log 2>&1 +echo "$(date '+%Y-%m-%d %H:%M') Pulling images" + +docker ps -a --format '{{.Image}}' \ + | grep -vE '^[0-9a-f]{12,}$' \ + | sort -u \ + | xargs -n1 docker pull + + + + + + diff --git a/app/sbin/restartContainers b/app/sbin/restartContainers new file mode 100755 index 0000000..cbd86a4 --- /dev/null +++ b/app/sbin/restartContainers @@ -0,0 +1,23 @@ +#!/bin/bash + +# Check that a search string was provided +if [ -z "$1" ]; then + echo "Usage: $0 " + 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 + diff --git a/app/sbin/sql b/app/sbin/sql new file mode 100755 index 0000000..ba1d057 --- /dev/null +++ b/app/sbin/sql @@ -0,0 +1,3 @@ +#!/bin/bash + +doas docker exec -it $1 bash -c 'PGPASSWORD="$PASSWORD" psql -h "$HOST" -U "$USER"' diff --git a/app/sbin/start/ODOO_17 b/app/sbin/start/ODOO_17 new file mode 100755 index 0000000..d5995bd --- /dev/null +++ b/app/sbin/start/ODOO_17 @@ -0,0 +1,127 @@ +#!/bin/bash +echo "START ODOO 17" +# Load functions +source /4server/sbin/ODOO_19/ODOO_19.lib +source /4server/sbin/helpers +get_contract_info + +# Config variables +UUID="${UUID:-default}" +BRANCH="${BRANCH:-release}" + +ODOO_DB_USER="${UUID}" +#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql') + + +echo "ENV: $HDD $DOMAIN_SLOTS $BACKUP_SLOTS $CONTAINERDBID" +echo "DBID: $CONTAINERDBID" + +BASEURL="${BASEURL:-/4server/data/$UUID}" +DATA_DIR="$BASEURL/odoo/" +CUSTOM_DIR="$BASEURL/git/$UUID/custom/" +ENTERPRISE_DIR="$BASEURL/git/$UUID/enterprise/" +LOGS_DIR="$BASEURL/logs/" +CONFIG_DIR="$BASEURL/config/" +CC_DIR="$BASEURL/cc/" +BACKUP_DIR="/BACKUP/$UUID" +GIT_DIR="$BASEURL/git-server/" +INSTALL_DIR="$BASEURL/install/" +SSH_DIR="$BASEURL/.ssh/" +ETC_DIR="$BASEURL/etc/" + +SERVER_IP=$(ip -4 addr show eth0 | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1) + +LABEL_DOMAINS=("$UUID.odoo4projects.com") +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 "[DEBUG] No additional domain file found at $BASEURL/etc/domain" +fi + +RULE="" +for d in "${LABEL_DOMAINS[@]}"; do + RESOLVED_IP=$(nslookup "$d" 2>/dev/null | awk '/^Address: / { print $2 }' | head -n1 || true) + if [ -z "$RESOLVED_IP" ]; then + echo "[DEBUG] Could not resolve $d, skipping" + continue + fi + if [ "$RESOLVED_IP" = "$SERVER_IP" ]; then + RULE+=" || Host(\`$d\`)" + else + echo "[DEBUG] Skipping $d (does not match SERVER_IP $SERVER_IP)" + fi +done +RULE="${RULE# || }" +DOMAIN_LABEL="traefik.http.routers.$UUID.rule=$RULE" +echo "[DEBUG] Final Traefik label: $DOMAIN_LABEL" + +docker exec "$UUID" mkdir -p /var/lib/odoo/.local/share/Odoo/ +docker exec "$UUID" ln -s /home/odoo/.local/share/Odoo/filestore /var/lib/odoo/.local/share/Odoo/filestore + + +find "$BASEURL" -type d -exec chmod 777 {} \; +PORT=$((CONTAINERDBID + 2200)) +echo "PORT $PORT" +mkdir -p ${ETC_DIR} +echo "GITPATH ${ETC_DIR}gitpath" +touch ${ETC_DIR}gitpath + +mkdir -p ${GIT_DIR}keys +touch ${GIT_DIR}keys/id_rsa.pub + + +echo "git clone \"ssh://git@${UUID}.odoo4projects.com:${PORT}/git-server/repos/odoo.git\"" > "${ETC_DIR}/gitpath" + + +docker stop "$UUID" 2>/dev/null +docker rm "$UUID" 2>/dev/null + +EXTRA_DOCKER_PARAMETER="" + +docker run -d --name "$UUID" \ + --network 4server_4projects \ + --restart=always \ + $EXTRA_DOCKER_PARAMETER \ + -v "$DATA_DIR/odoo-web-data:/home/odoo/.local/share/Odoo" \ + -v "$CUSTOM_DIR:/mnt/addons/custom" \ + -v "$ENTERPRISE_DIR:/mnt/addons/enterprise" \ + -v "$LOGS_DIR:/mnt/logs" \ + -v "$CC_DIR:/mnt/cc" \ + -v "$BACKUP_DIR:/mnt/backup" \ + -v "$GIT_DIR:/git-server" \ + -v "$INSTALL_DIR:/mnt/install" \ + -v "$SSH_DIR:/etc/sshkey" \ + -v "$ETC_DIR:/mnt/etc" \ + -p "$PORT:22" \ + -e HOST="beedb" \ + -e USER="$ODOO_DB_USER" \ + -e PASSWORD="$ODOO_DB_PASSWORD" \ + -e UUID="$UUID" \ + -e HDD="$HDD" \ + -e DOMAIN_SLOTS="$DOMAIN_SLOTS" \ + -e BACKUP_SLOTS="$BACKUP_SLOTS" \ + -e WORKER="$WORKER" \ + -e GIT="$GIT" \ + --label "$DOMAIN_LABEL" \ + --label "traefik.http.services.$UUID.loadbalancer.server.port=8069" \ + --label "traefic.http.routers.$UUID.entrypoints=web, websecure" \ + --label "traefik.http.routers.$UUID.tls.certresolver=production" \ + --label "traefik.http.routers.$UUID.tls=true" \ + --label "traefik.http.routers.$UUID.service=$UUID" \ + docker.odoo4projects.com/4projects/odoo_17:$BRANCH + +docker exec "$UUID" mkdir -p /var/lib/odoo/.local/share/Odoo +docker exec "$UUID" rm -rf /var/lib/odoo/.local/share/Odoo/filestore +docker exec "$UUID" ln -s /home/odoo/.local/share/Odoo/filestore /var/lib/odoo/.local/share/Odoo/filestore + + +docker exec $UUID chown -R odoo:odoo /home/odoo/.local +docker exec $UUID chown -R odoo:odoo /var/lib/odoo/.local/share/Odoo +docker exec $UUID chown -R odoo:odoo /mnt/* +docker exec $UUID chown odoo:odoo /git-server/keys/id_rsa.pub + + +check_and_create_db diff --git a/app/sbin/start/ODOO_18 b/app/sbin/start/ODOO_18 new file mode 100755 index 0000000..f76575b --- /dev/null +++ b/app/sbin/start/ODOO_18 @@ -0,0 +1,127 @@ +#!/bin/bash + +# Load functions +source /4server/sbin/ODOO_19/ODOO_19.lib +source /4server/sbin/helpers +get_contract_info + +# Config variables +UUID="${UUID:-default}" +BRANCH="${BRANCH:-release}" + +ODOO_DB_USER="${UUID}" +#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql') + + +echo "ENV: $HDD $DOMAIN_SLOTS $BACKUP_SLOTS $CONTAINERDBID" +echo "DBID: $CONTAINERDBID" + +BASEURL="${BASEURL:-/4server/data/$UUID}" +DATA_DIR="$BASEURL/odoo/" +CUSTOM_DIR="$BASEURL/git/$UUID/custom/" +ENTERPRISE_DIR="$BASEURL/git/$UUID/enterprise/" +LOGS_DIR="$BASEURL/logs/" +CONFIG_DIR="$BASEURL/config/" +CC_DIR="$BASEURL/cc/" +BACKUP_DIR="/BACKUP/$UUID" +GIT_DIR="$BASEURL/git-server/" +INSTALL_DIR="$BASEURL/install/" +SSH_DIR="$BASEURL/.ssh/" +ETC_DIR="$BASEURL/etc/" + +SERVER_IP=$(ip -4 addr show eth0 | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1) + +LABEL_DOMAINS=("$UUID.odoo4projects.com") +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 "[DEBUG] No additional domain file found at $BASEURL/etc/domain" +fi + +RULE="" +for d in "${LABEL_DOMAINS[@]}"; do + RESOLVED_IP=$(nslookup "$d" 2>/dev/null | awk '/^Address: / { print $2 }' | head -n1 || true) + if [ -z "$RESOLVED_IP" ]; then + echo "[DEBUG] Could not resolve $d, skipping" + continue + fi + if [ "$RESOLVED_IP" = "$SERVER_IP" ]; then + RULE+=" || Host(\`$d\`)" + else + echo "[DEBUG] Skipping $d (does not match SERVER_IP $SERVER_IP)" + fi +done +RULE="${RULE# || }" +DOMAIN_LABEL="traefik.http.routers.$UUID.rule=$RULE" +echo "[DEBUG] Final Traefik label: $DOMAIN_LABEL" + +docker exec "$UUID" mkdir -p /var/lib/odoo/.local/share/Odoo/ +docker exec "$UUID" ln -s /home/odoo/.local/share/Odoo/filestore /var/lib/odoo/.local/share/Odoo/filestore + + +find "$BASEURL" -type d -exec chmod 777 {} \; +PORT=$((CONTAINERDBID + 2200)) +echo "PORT $PORT" +mkdir -p ${ETC_DIR} +echo "GITPATH ${ETC_DIR}gitpath" +touch ${ETC_DIR}gitpath + +mkdir -p ${GIT_DIR}keys +touch ${GIT_DIR}keys/id_rsa.pub + + +echo "git clone \"ssh://git@${UUID}.odoo4projects.com:${PORT}/git-server/repos/odoo.git\"" > "${ETC_DIR}/gitpath" + + +docker stop "$UUID" 2>/dev/null +docker rm "$UUID" 2>/dev/null + +EXTRA_DOCKER_PARAMETER="" + +docker run -d --name "$UUID" \ + --network 4server_4projects \ + --restart=always \ + $EXTRA_DOCKER_PARAMETER \ + -v "$DATA_DIR/odoo-web-data:/home/odoo/.local/share/Odoo" \ + -v "$CUSTOM_DIR:/mnt/addons/custom" \ + -v "$ENTERPRISE_DIR:/mnt/addons/enterprise" \ + -v "$LOGS_DIR:/mnt/logs" \ + -v "$CC_DIR:/mnt/cc" \ + -v "$BACKUP_DIR:/mnt/backup" \ + -v "$GIT_DIR:/git-server" \ + -v "$INSTALL_DIR:/mnt/install" \ + -v "$SSH_DIR:/etc/sshkey" \ + -v "$ETC_DIR:/mnt/etc" \ + -p "$PORT:22" \ + -e HOST="beedb" \ + -e USER="$ODOO_DB_USER" \ + -e PASSWORD="$ODOO_DB_PASSWORD" \ + -e UUID="$UUID" \ + -e HDD="$HDD" \ + -e DOMAIN_SLOTS="$DOMAIN_SLOTS" \ + -e BACKUP_SLOTS="$BACKUP_SLOTS" \ + -e WORKER="$WORKER" \ + -e GIT="$GIT" \ + --label "$DOMAIN_LABEL" \ + --label "traefik.http.services.$UUID.loadbalancer.server.port=8069" \ + --label "traefic.http.routers.$UUID.entrypoints=web, websecure" \ + --label "traefik.http.routers.$UUID.tls.certresolver=production" \ + --label "traefik.http.routers.$UUID.tls=true" \ + --label "traefik.http.routers.$UUID.service=$UUID" \ + docker.odoo4projects.com/4projects/odoo_18:$BRANCH + +docker exec "$UUID" mkdir -p /var/lib/odoo/.local/share/Odoo +docker exec "$UUID" rm -rf /var/lib/odoo/.local/share/Odoo/filestore +docker exec "$UUID" ln -s /home/odoo/.local/share/Odoo/filestore /var/lib/odoo/.local/share/Odoo/filestore + + +docker exec $UUID chown -R odoo:odoo /home/odoo/.local +docker exec $UUID chown -R odoo:odoo /var/lib/odoo/.local/share/Odoo +docker exec $UUID chown -R odoo:odoo /mnt/* +docker exec $UUID chown odoo:odoo /git-server/keys/id_rsa.pub + + +check_and_create_db diff --git a/app/sbin/start/ODOO_19 b/app/sbin/start/ODOO_19 new file mode 100755 index 0000000..25a7c33 --- /dev/null +++ b/app/sbin/start/ODOO_19 @@ -0,0 +1,125 @@ +#!/bin/bash + +# Load functions +source /4server/sbin/ODOO_19/ODOO_19.lib +source /4server/sbin/helpers +get_contract_info + +# Config variables +UUID="${UUID:-default}" +BRANCH="${BRANCH:-release}" + +ODOO_DB_USER="${UUID}" +#export ODOO_DB_PASSWORD=$(echo "$SECRET" | jq -r '.psql') +echo "***** $ODOO_DB_PASSWORD" + +echo "ENV: $HDD $DOMAIN_SLOTS $BACKUP_SLOTS $CONTAINERDBID" +echo "DBID: $CONTAINERDBID" + +BASEURL="${BASEURL:-/4server/data/$UUID}" +DATA_DIR="$BASEURL/odoo/" +CUSTOM_DIR="$BASEURL/git/$UUID/custom/" +ENTERPRISE_DIR="$BASEURL/git/$UUID/enterprise/" +LOGS_DIR="$BASEURL/logs/" +CONFIG_DIR="$BASEURL/config/" +CC_DIR="$BASEURL/cc/" +BACKUP_DIR="/BACKUP/$UUID" +GIT_DIR="$BASEURL/git-server/" +INSTALL_DIR="$BASEURL/install/" +SSH_DIR="$BASEURL/.ssh/" +ETC_DIR="$BASEURL/etc/" + +SERVER_IP=$(ip -4 addr show eth0 | awk '/inet/ {print $2}' | cut -d/ -f1 | head -n1) + +LABEL_DOMAINS=("$UUID.odoo4projects.com") +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 "[DEBUG] No additional domain file found at $BASEURL/etc/domain" +fi + +RULE="" +for d in "${LABEL_DOMAINS[@]}"; do + RESOLVED_IP=$(nslookup "$d" 2>/dev/null | awk '/^Address: / { print $2 }' | head -n1 || true) + if [ -z "$RESOLVED_IP" ]; then + echo "[DEBUG] Could not resolve $d, skipping" + continue + fi + if [ "$RESOLVED_IP" = "$SERVER_IP" ]; then + RULE+=" || Host(\`$d\`)" + else + echo "[DEBUG] Skipping $d (does not match SERVER_IP $SERVER_IP)" + fi +done +RULE="${RULE# || }" +DOMAIN_LABEL="traefik.http.routers.$UUID.rule=$RULE" +echo "[DEBUG] Final Traefik label: $DOMAIN_LABEL" + + + +find "$BASEURL" -type d -exec chmod 777 {} \; +PORT=$((CONTAINERDBID + 2200)) +echo "PORT $PORT" +mkdir -p ${ETC_DIR} +chmod 777 ${ETC_DIR} +echo "GITPATH ${ETC_DIR}gitpath" +touch ${ETC_DIR}gitpath + +mkdir -p ${BACKUP_DIR} +chmod 777 ${BACKUP_DIR} + +mkdir -p ${GIT_DIR}keys +chmod 777 ${GIT_DIR} +touch ${GIT_DIR}keys/id_rsa.pub + + +echo "git clone \"ssh://git@${UUID}.odoo4projects.com:${PORT}/git-server/repos/odoo.git\"" > "${ETC_DIR}/gitpath" + + +docker stop "$UUID" 2>/dev/null +docker rm "$UUID" 2>/dev/null + +EXTRA_DOCKER_PARAMETER="" + +docker run -d --name "$UUID" \ + --network 4server_4projects \ + --restart=always \ + $EXTRA_DOCKER_PARAMETER \ + -v "$DATA_DIR/odoo-web-data:/home/odoo/.local/share/Odoo" \ + -v "$CUSTOM_DIR:/mnt/addons/custom" \ + -v "$ENTERPRISE_DIR:/mnt/addons/enterprise" \ + -v "$LOGS_DIR:/mnt/logs" \ + -v "$CC_DIR:/mnt/cc" \ + -v "$BACKUP_DIR:/mnt/backup" \ + -v "$GIT_DIR:/git-server" \ + -v "$INSTALL_DIR:/mnt/install" \ + -v "$SSH_DIR:/etc/sshkey" \ + -v "$ETC_DIR:/mnt/etc" \ + -p "$PORT:22" \ + -e HOST="beedb" \ + -e USER="$ODOO_DB_USER" \ + -e PASSWORD="$ODOO_DB_PASSWORD" \ + -e UUID="$UUID" \ + -e HDD="$HDD" \ + -e DOMAIN_SLOTS="$DOMAIN_SLOTS" \ + -e BACKUP_SLOTS="$BACKUP_SLOTS" \ + -e WORKER="$WORKER" \ + -e GIT="$GIT" \ + --label "$DOMAIN_LABEL" \ + --label "traefik.http.services.$UUID.loadbalancer.server.port=8069" \ + --label "traefic.http.routers.$UUID.entrypoints=web, websecure" \ + --label "traefik.http.routers.$UUID.tls.certresolver=production" \ + --label "traefik.http.routers.$UUID.tls=true" \ + --label "traefik.http.routers.$UUID.service=$UUID" \ + docker.odoo4projects.com/4projects/odoo_19:$BRANCH + + +docker exec $UUID chown -R odoo:odoo /home/odoo/.local +docker exec $UUID chown -R odoo:odoo /mnt/* +docker exec $UUID chown -R git:git /git-server +chmod 777 /4server/data/$UUID/cc + +check_and_create_db diff --git a/app/sbin/start/n8n b/app/sbin/start/n8n new file mode 100755 index 0000000..acec4b1 --- /dev/null +++ b/app/sbin/start/n8n @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +#--label "traefik.http.routers.${UUID}.middlewares=cors-headers@file" \ +echo "Start N8N container ${UUID}" + +# Get the hostname of the machine +HOSTNAME=$(hostname) + +mkdir -p /4server/data/${UUID}/n8n +mkdir -p /4server/data/${UUID}/data +mkdir -p /4server/data/${UUID}/backup + +chmod 777 /4server/data/${UUID}/n8n +chmod 777 /4server/data/${UUID}/data +chmod 777 /4server/data/${UUID}/backup + +# Stop the container if it exists +if docker ps -a --format '{{.Names}}' | grep -q "^${UUID}$"; then + echo "$(date '+%Y-%m-%d %H:%M') - stopping existing container $UUID" + docker stop "$UUID" + docker rm "$UUID" +fi + + + +docker run -d \ + --name "$UUID" \ + -p 5678 \ + --cap-add=SYS_ADMIN \ + --security-opt seccomp=unconfined \ + --restart=always \ + -e N8N_EMAIL_MODE=smtp \ + -e N8N_SMTP_HOST="smtp.strato.de" \ + -e N8N_SMTP_PORT=587 \ + -e N8N_SMTP_USER="n8n@odoo4projects.com" \ + -e N8N_SMTP_PASS="Airbus12N@N" \ + -e N8N_SMTP_SENDER="n8n " \ + -e N8N_SMTP_SSL=false \ + -e N8N_HOST="${UUID}.odoo4projects.com" \ + -e N8N_PORT=5678 \ + -e N8N_PROTOCOL=https \ + -e N8N_FORMDATA_FILE_SIZE_MAX=3000 \ + -e N8N_TRUST_PROXY=true \ + -e NODE_ENV=production \ + -e EXECUTIONS_PRUNE=true \ + -e EXECUTIONS_PRUNE_MAX_AGE=48 \ + -e WEBHOOK_URL="https://${UUID}.odoo4projects.com/" \ + -e GENERIC_TIMEZONE="UTC-3" \ + -e N8N_BLOCK_ENV_ACCESS_AND_PROCESS_INFORMATION=true \ + -e N8N_CUSTOM_EXTENSIONS="/usr/local/share/n8n/custom" \ + -v "/4server/data/${UUID}/n8n:/home/node/.n8n" \ + -v "/4server/data/${UUID}/data:/data" \ + -v "/4server/data/${UUID}/backup:/backup" \ + --label "traefik.enable=true" \ + --label "traefik.http.routers.${UUID}.rule=Host(\`${UUID}.odoo4projects.com\`)" \ + --label "traefik.http.routers.${UUID}.entrypoints=web,websecure" \ + --label "traefik.http.routers.${UUID}.tls=true" \ + --label "traefik.http.routers.${UUID}.tls.certresolver=production" \ + --label "traefik.http.services.${UUID}.loadbalancer.server.port=5678" \ + --network 4server_4projects \ + docker.odoo4projects.com/4projects/n8n:release + +echo "Started $1" diff --git a/app/sbin/startContainer b/app/sbin/startContainer new file mode 100755 index 0000000..4abdcb1 --- /dev/null +++ b/app/sbin/startContainer @@ -0,0 +1,65 @@ +#!/bin/bash +# Usage: ./start_by_uuid.sh +# 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') Start container $1" + +source /4server/sbin/helpers + +BIN_PATH="/4server/sbin" + +UUID="$1" + +if [[ -z "$UUID" ]]; then + echo "Usage: $0 " + 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") + # 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 + + + +get_contract_info + +# Extract the second part of UUID (split by "-") +SECOND_PART=$(echo "$UUID" | cut -d'-' -f2) + +# Decide which script to run +case "$SECOND_PART" in + 001) + "$BIN_PATH/start/n8n" + ;; + 002) + "$BIN_PATH/start/ODOO_18" + ;; + + + 003) + "$BIN_PATH/start/ODOO_19" + ;; + + 004) + "$BIN_PATH/start/ODOO_17" + ;; + + + + *) + echo "Unknown UUID type: $SECOND_PART" + exit 2 + ;; +esac diff --git a/app/sbin/stopContainer b/app/sbin/stopContainer new file mode 100755 index 0000000..0d1bdec --- /dev/null +++ b/app/sbin/stopContainer @@ -0,0 +1,6 @@ +#!/bin/bash +exec > /4server/data/log/stopContainer.log 2>&1 +echo "$(date '+%Y-%m-%d %H:%M') Stop container $1" + +docker stop $1 + diff --git a/app/templates/.profile b/app/templates/.profile new file mode 100644 index 0000000..16250f2 --- /dev/null +++ b/app/templates/.profile @@ -0,0 +1,7 @@ +# ~/.bashrc +clear +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 . +cd /4server diff --git a/app/templates/4server b/app/templates/4server new file mode 100644 index 0000000..6dd8490 --- /dev/null +++ b/app/templates/4server @@ -0,0 +1 @@ +API_KEY={{API_KEY}} diff --git a/app/templates/docker-compose.yml b/app/templates/docker-compose.yml new file mode 100644 index 0000000..fd418c8 --- /dev/null +++ b/app/templates/docker-compose.yml @@ -0,0 +1,42 @@ +services: + + beedb: + image: postgres:16 + restart: always + environment: + - POSTGRES_DB=postgres + - POSTGRES_PASSWORD=ZpSwWNafyy9GhY2gzHw + - POSTGRES_USER=1gtT0sf8klB9lDbYZD9 + volumes: + - /4server/data/postgres/data/:/var/lib/postgresql/data/ + - /4server/data/postgres/pg_backup/:/BACKUP/ + - /4server/data/postgres/etc/:/etc/postgresql/16/main/ + + networks: + 4projects: + ipv4_address: 10.5.0.200 + + traefik: + image: docker.io/library/traefik:3.1 + container_name: traefik + ports: + - 80:80 + - 443:443 + volumes: + - /run/docker.sock:/run/docker.sock:ro + - /4server/data/traefik/etc:/etc/traefik + - /4server/data/traefik/certs:/certs + - /4server/data/log/traefik-logs:/var/log/traefik + networks: + - 4projects + restart: unless-stopped + +networks: + 4projects: + driver: bridge + ipam: + config: + - subnet: 10.5.0.0/16 + gateway: 10.5.0.1 + ip_range: 10.5.0.0/26 + diff --git a/app/templates/etc/doas.d/4server.conf b/app/templates/etc/doas.d/4server.conf new file mode 100644 index 0000000..b0a80eb --- /dev/null +++ b/app/templates/etc/doas.d/4server.conf @@ -0,0 +1 @@ +permit nopass 4server as root diff --git a/app/templates/etc/fail2ban/jail.conf b/app/templates/etc/fail2ban/jail.conf new file mode 100644 index 0000000..3f20e55 --- /dev/null +++ b/app/templates/etc/fail2ban/jail.conf @@ -0,0 +1,10 @@ +[DEFAULT] +bantime = 1h +findtime = 30m +maxretry = 1 + +[sshd] +enabled = true +port = ssh +filter = sshd +logpath = /var/log/auth.log diff --git a/app/templates/extlinux.conf b/app/templates/extlinux.conf new file mode 100644 index 0000000..6b66748 --- /dev/null +++ b/app/templates/extlinux.conf @@ -0,0 +1,23 @@ +SERIAL ttyS0 115200 +DEFAULT menu.c32 +PROMPT 0 +MENU TITLE Alpine/Linux Boot Menu +MENU HIDDEN +MENU AUTOBOOT Alpine will be booted automatically in # seconds. +TIMEOUT 100 +LABEL virt + + MENU LABEL Linux virt + LINUX vmlinuz-virt + INITRD initramfs-virt + APPEND root=UUID=15665bb5-c475-4ac8-9479-d49df97d5ccb modules=sd-mod,usb-storage,ext4 console=ttyS0,115200n8 console=ttyAMA0,115200n8 + +LABEL lts + MENU DEFAULT + MENU LABEL Linux lts + LINUX vmlinuz-lts + INITRD initramfs-lts + APPEND root=UUID=15665bb5-c475-4ac8-9479-d49df97d5ccb modules=sd-mod,usb-storage,ext4 console=ttyS0,115200n8 console=ttyAMA0,115200n8 + +MENU SEPARATOR + diff --git a/app/templates/extlinux.conf-DigitalOcean b/app/templates/extlinux.conf-DigitalOcean new file mode 100644 index 0000000..6b66748 --- /dev/null +++ b/app/templates/extlinux.conf-DigitalOcean @@ -0,0 +1,23 @@ +SERIAL ttyS0 115200 +DEFAULT menu.c32 +PROMPT 0 +MENU TITLE Alpine/Linux Boot Menu +MENU HIDDEN +MENU AUTOBOOT Alpine will be booted automatically in # seconds. +TIMEOUT 100 +LABEL virt + + MENU LABEL Linux virt + LINUX vmlinuz-virt + INITRD initramfs-virt + APPEND root=UUID=15665bb5-c475-4ac8-9479-d49df97d5ccb modules=sd-mod,usb-storage,ext4 console=ttyS0,115200n8 console=ttyAMA0,115200n8 + +LABEL lts + MENU DEFAULT + MENU LABEL Linux lts + LINUX vmlinuz-lts + INITRD initramfs-lts + APPEND root=UUID=15665bb5-c475-4ac8-9479-d49df97d5ccb modules=sd-mod,usb-storage,ext4 console=ttyS0,115200n8 console=ttyAMA0,115200n8 + +MENU SEPARATOR + diff --git a/app/templates/extlinux.conf-hostinger b/app/templates/extlinux.conf-hostinger new file mode 100644 index 0000000..b9a951b --- /dev/null +++ b/app/templates/extlinux.conf-hostinger @@ -0,0 +1,23 @@ +SERIAL ttyS0 115200 +DEFAULT menu.c32 +PROMPT 0 +MENU TITLE Alpine/Linux Boot Menu +MENU HIDDEN +MENU AUTOBOOT Alpine will be booted automatically in # seconds. +TIMEOUT 100 +LABEL virt + + MENU LABEL Linux virt + LINUX vmlinuz-virt + INITRD initramfs-virt + APPEND root=LABEL=/ modules=sd-mod,usb-storage,ext4 console=ttyS0,115200n8 console=ttyAMA0,115200n8 + +LABEL lts + MENU DEFAULT + MENU LABEL Linux lts + LINUX vmlinuz-lts + INITRD initramfs-lts + APPEND root=LABEL=/ modules=sd-mod,usb-storage,ext4 console=ttyS0,115200n8 console=ttyAMA0,115200n8 + +MENU SEPARATOR + diff --git a/app/templates/hostname b/app/templates/hostname new file mode 100644 index 0000000..5332dc4 --- /dev/null +++ b/app/templates/hostname @@ -0,0 +1 @@ +{{HOSTNAME}} diff --git a/app/templates/hosts b/app/templates/hosts new file mode 100644 index 0000000..d4e1d7a --- /dev/null +++ b/app/templates/hosts @@ -0,0 +1,2 @@ +127.0.0.1 {{hostname}} localhost +10.5.0.200 beedb diff --git a/app/templates/init.d/api b/app/templates/init.d/api new file mode 100755 index 0000000..4301576 --- /dev/null +++ b/app/templates/init.d/api @@ -0,0 +1,30 @@ +#!/sbin/openrc-run + +name="api" +description="4server API Service" + +# Command uses Python inside the venv +command="/4server/sbin/api" +command_args="" +pidfile="/run/${RC_SVCNAME}.pid" +command_background="yes" + +respawn_delay=5 # seconds to wait before restart +respawn_max=0 # 0 = unlimited restarts + +# Load environment variables if needed +if [ -f /etc/4server ]; then + . /etc/4server + export $(cut -d= -f1 /etc/4server) +fi + +# Logs +output_log="/4server/data/log/api.log" +error_log="/4server/data/log/api.log" + +depend() { + need net + use logger dns + after firewall +} + diff --git a/app/templates/init.d/checkCalls b/app/templates/init.d/checkCalls new file mode 100755 index 0000000..e5288ea --- /dev/null +++ b/app/templates/init.d/checkCalls @@ -0,0 +1,30 @@ +#!/sbin/openrc-run + +name="checkCalls" +description="check container service calls" + +# Command uses Python inside the venv +command="/4server/sbin/checkCalls" +command_args="" +pidfile="/run/${RC_SVCNAME}.pid" +command_background="yes" + +directory="/4server" +command_user="root" + +respawn_delay=5 # seconds to wait before restart +respawn_max=0 # 0 = unlimited restarts + + + + +# Load environment variables if needed +output_log="/4server/data/log/checkCalls.log" +error_log="/4server/data/log/checkCalls.log" + +depend() { + need net + use logger dns + after firewall +} + diff --git a/app/templates/init.d/cleanTmp b/app/templates/init.d/cleanTmp new file mode 100755 index 0000000..311bcf1 --- /dev/null +++ b/app/templates/init.d/cleanTmp @@ -0,0 +1,17 @@ +#!/sbin/openrc-run +# OpenRC service for /4server/sbin/cleanTmp + +name="cleanTmp" +description="Looping /tmp cleaner that removes files older than 2 days" + +command="/4server/sbin/cleanTmp" +command_background="yes" +pidfile="/run/${RC_SVCNAME}.pid" +output_log="/4server/data/log/checkCalls.log" +error_log="/4server/data/log/checkCalls.log" + +depend() { + need localmount + after bootmisc +} + diff --git a/app/templates/init.d/cpu b/app/templates/init.d/cpu new file mode 100755 index 0000000..368fc08 --- /dev/null +++ b/app/templates/init.d/cpu @@ -0,0 +1,17 @@ +#!/sbin/openrc-run +# OpenRC service for /4server/sbin/cpu + +name="cpu" +description="Logs cpu usage" + +command="/4server/sbin/cpu" +command_background="yes" +pidfile="/run/cpu.pid" +output_log="/4server/data/log/cpu.log" +error_log="/4server/data/log/cpu.log" + +depend() { + need localmount + after bootmisc +} + diff --git a/app/templates/init.d/nebula b/app/templates/init.d/nebula new file mode 100755 index 0000000..73ef02e --- /dev/null +++ b/app/templates/init.d/nebula @@ -0,0 +1,12 @@ +#!/sbin/openrc-run +command="/usr/bin/nebula" +command_args="-config /etc/nebula/config.yml" +command_background="yes" +pidfile="/run/nebula.pid" +name="nebula" + +depend() { + need localmount + after networking +} + diff --git a/app/templates/init.d/ping_service b/app/templates/init.d/ping_service new file mode 100755 index 0000000..43672d7 --- /dev/null +++ b/app/templates/init.d/ping_service @@ -0,0 +1,17 @@ +#!/sbin/openrc-run +description="Ping 192.168.9.1 every minute" + +pidfile="/run/ping_check.pid" + +start() { + ebegin "Starting ping_check service" + start-stop-daemon --start --background --make-pidfile --pidfile $pidfile --exec /bin/sh -- -c "while true; do ping -c 1 192.168.9.1; sleep 60; done" + eend $? +} + +stop() { + ebegin "Stopping ping_check service" + start-stop-daemon --stop --pidfile $pidfile + eend $? +} + diff --git a/app/templates/nebula/ca.crt b/app/templates/nebula/ca.crt new file mode 100644 index 0000000..814039a --- /dev/null +++ b/app/templates/nebula/ca.crt @@ -0,0 +1 @@ +{{NEBULA_CA}} diff --git a/app/templates/nebula/config.yml b/app/templates/nebula/config.yml new file mode 100644 index 0000000..83656ee --- /dev/null +++ b/app/templates/nebula/config.yml @@ -0,0 +1,65 @@ +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: +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 + + + + + diff --git a/app/templates/nebula/host.crt b/app/templates/nebula/host.crt new file mode 100644 index 0000000..a19c09a --- /dev/null +++ b/app/templates/nebula/host.crt @@ -0,0 +1 @@ +{{NEBULA_CRT}} diff --git a/app/templates/nebula/host.key b/app/templates/nebula/host.key new file mode 100644 index 0000000..8ca51de --- /dev/null +++ b/app/templates/nebula/host.key @@ -0,0 +1 @@ +{{NEBULA_KEY}} diff --git a/app/templates/pg_hba.conf b/app/templates/pg_hba.conf new file mode 100644 index 0000000..d424dd3 --- /dev/null +++ b/app/templates/pg_hba.conf @@ -0,0 +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 + diff --git a/app/templates/repositories b/app/templates/repositories new file mode 100644 index 0000000..4da1d4e --- /dev/null +++ b/app/templates/repositories @@ -0,0 +1,3 @@ +https://dl-cdn.alpinelinux.org/alpine/v3.22/main +https://dl-cdn.alpinelinux.org/alpine/v3.22/community + diff --git a/app/templates/ssh/id_ed25519 b/app/templates/ssh/id_ed25519 new file mode 100644 index 0000000..2846a8f --- /dev/null +++ b/app/templates/ssh/id_ed25519 @@ -0,0 +1 @@ +{{SSH_PRIVATE}} diff --git a/app/templates/ssh/id_ed25519.pub b/app/templates/ssh/id_ed25519.pub new file mode 100644 index 0000000..fd2373f --- /dev/null +++ b/app/templates/ssh/id_ed25519.pub @@ -0,0 +1 @@ +{{SSH_PUBLIC}} diff --git a/app/templates/traefik.yaml b/app/templates/traefik.yaml new file mode 100644 index 0000000..7519104 --- /dev/null +++ b/app/templates/traefik.yaml @@ -0,0 +1,103 @@ +global: + checkNewVersion: false + sendAnonymousUsage: false + +accesslog: + filePath: /var/log/traefik/access.log + +api: + dashboard: false + disableDashboardAd: true + insecure: false + +entryPoints: + web: + address: :80 + # -- (Optional) Redirect all HTTP to HTTPS + http: + redirections: + entryPoint: + to: websecure + scheme: https + websecure: + # http: + # middlewares: + # - crowdsec-bouncer@file + address: :443 + transport: + respondingTimeouts: + readTimeout: 0 + writeTimeout: 0 + idleTimeout: 42 + +# -- Configure your CertificateResolver here... +certificatesResolvers: +# staging: +# acme: +# email: your-email@example.com +# storage: /etc/traefik/certs/acme.json +# caServer: "https://acme-staging-v02.api.letsencrypt.org/directory" +# httpChallenge: +# entryPoint: web + + production: + acme: + email: oliver@odoo4projects.com + storage: /certs/acme.json + caServer: "https://acme-v02.api.letsencrypt.org/directory" + httpChallenge: + entryPoint: web + +# -- (Optional) Disable TLS Cert verification check +# serversTransport: +# insecureSkipVerify: true + +# -- (Optional) Overwrite Default Certificates +# tls: +# stores: +# default: +# defaultCertificate: +# certFile: /etc/traefik/certs/cert.pem +# keyFile: /etc/traefik/certs/cert-key.pem +# -- (Optional) Disable TLS version 1.0 and 1.1 +# options: +# default: +# minVersion: VersionTLS12 + +providers: + docker: + # -- (Optional) Enable this, if you want to expose all containers automatically + exposedByDefault: true + file: + directory: /etc/traefik + watch: true + + +http: + middlewares: + crowdsec-bouncer: + forwardauth: + address: http://bouncer-traefik:8080/api/v1/forwardAuth + trustForwardHeader: true + + cors-headers: + headers: + accessControlAllowCredentials: true + + + routers: + api-router: + rule: "Host(`{{HOSTNAME}}.odoo4projects.com`)" + service: api-service + entryPoints: + - websecure + tls: + certResolver: production + + services: + api-service: + loadBalancer: + servers: + - url: "http://10.5.0.1:8888" + + diff --git a/app/test/api.json b/app/test/api.json new file mode 100644 index 0000000..f6c5f78 --- /dev/null +++ b/app/test/api.json @@ -0,0 +1,233 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Container Management API", + "version": "0.0.5", + "description": "API for managing containers and system resources." + }, + "servers": [ + { "url": "http://localhost:8888" } + ], + "components": { + "securitySchemes": { + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } + }, + "schemas": { + "ContainerModel": { + "type": "object", + "properties": { + "UUID": { "type": "string" }, + "email": { "type": "string" }, + "expires": { "type": "string", "format": "date" }, + "tags": { "type": "string" }, + "env": { "type": "string" }, + "affiliate": { "type": "string" }, + "image": { "type": "string" }, + "history": { "type": "string" }, + "comment": { "type": "string" }, + "domains": { "type": "string" }, + "status": { "type": "string" }, + "created": { "type": "string", "format": "date" }, + "bump": { "type": "string", "format": "date" } + }, + "required": ["UUID", "email", "expires", "status", "created"] + }, + "ContainerIDRequest": { + "type": "object", + "properties": { + "container_id": { "type": "string" } + }, + "required": ["container_id"] + }, + "InfoContainerRequest": { + "type": "object", + "properties": { + "container_id": { "type": "string" } + } + } + } + }, + "security": [{ "ApiKeyAuth": [] }], + "paths": { + "/": { + "get": { + "summary": "Root redirect", + "responses": { + "302": { "description": "Redirect to ODOO4PROJECTS.com" } + } + } + }, + "/container/update": { + "post": { + "summary": "Create or update container", + "security": [{ "ApiKeyAuth": [] }], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/ContainerModel" } + } + } + }, + "responses": { + "200": { + "description": "Container updated or created", + "content": { "application/json": { "example": { "message": "Container updated or created" } } } + } + } + } + }, + "/container/start": { + "post": { + "summary": "Start a container", + "security": [{ "ApiKeyAuth": [] }], + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContainerIDRequest" } } } + }, + "responses": { "200": { "description": "Container started" } } + } + }, + "/container/stop": { + "post": { + "summary": "Stop a container", + "security": [{ "ApiKeyAuth": [] }], + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContainerIDRequest" } } } + }, + "responses": { "200": { "description": "Container stopped" } } + } + }, + "/container/nuke": { + "post": { + "summary": "Nuke a container", + "description": "Permanently deletes container if its status is 'nuke'.", + "security": [{ "ApiKeyAuth": [] }], + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContainerIDRequest" } } } + }, + "responses": { + "200": { "description": "Container nuked" }, + "400": { "description": "Container status is not 'nuke'" } + } + } + }, + "/container/info": { + "post": { + "summary": "Get container info", + "security": [{ "ApiKeyAuth": [] }], + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InfoContainerRequest" } } } + }, + "responses": { "200": { "description": "Container info list" } } + } + }, + "/container/bump": { + "post": { + "summary": "Bump a container", + "description": "Updates bump date and runs bump script.", + "security": [{ "ApiKeyAuth": [] }], + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContainerIDRequest" } } } + }, + "responses": { + "200": { "description": "Bump successful", "content": { "application/json": {} } } + } + } + }, + "/container/quota": { + "post": { + "summary": "Get container quota", + "security": [{ "ApiKeyAuth": [] }], + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ContainerIDRequest" } } } + }, + "responses": { + "200": { + "description": "Quota usage", + "content": { + "application/json": { + "example": { + "memory_usage": "50MiB / 2GiB", + "disk_io": "10MB / 200MB" + } + } + } + } + } + } + }, + "/system/containers": { + "get": { + "summary": "List running containers", + "security": [{ "ApiKeyAuth": [] }], + "responses": { + "200": { + "description": "Containers in system", + "content": { "application/json": {} } + } + } + } + }, + "/system/images": { + "get": { + "summary": "List system images", + "security": [{ "ApiKeyAuth": [] }], + "responses": { + "200": { + "description": "List of docker images", + "content": { + "application/json": { + "example": { + "images": ["repo1:tag", "repo2:tag"] + } + } + } + } + } + } + }, + "/system/info": { + "get": { + "summary": "Get system info", + "security": [{ "ApiKeyAuth": [] }], + "responses": { + "200": { + "description": "System resource info", + "content": { + "application/json": { + "example": { + "alpine_version": "3.18.2", + "last_update": "2025-08-01", + "latest_bump": "2025-08-10", + "version": "API: 0.0.5", + "resources": { + "memory": { "total": 16777216, "available": 8388608, "used": 8388608 }, + "disk": { "total": 100000000, "used": 50000000, "free": 50000000 }, + "cpu_count": 8 + } + } + } + } + } + } + } + }, + "/system/pull": { + "post": { + "summary": "Pull all images", + "security": [{ "ApiKeyAuth": [] }], + "responses": { + "200": { + "description": "All images pulled", + "content": { "application/json": { "example": { "message": "All images pulled" } } } + } + } + } + } + } +} + diff --git a/app/test/runner b/app/test/runner new file mode 100755 index 0000000..22365c9 --- /dev/null +++ b/app/test/runner @@ -0,0 +1,53 @@ +#!/bin/bash +# +# run_all_tests.sh — simple runner for test_api.sh +# + +API_SCRIPT="./test_api.sh" + +# Make sure test_api.sh is executable +if [ ! -x "$API_SCRIPT" ]; then + echo "Error: $API_SCRIPT not found or not executable" + exit 1 +fi + +# List of tests to run (must match functions in test_api.sh) +TESTS=( + root_redirect + update_container + start_container + stop_container + nuke_container + info_all + info_one + bump_container + container_quota + system_containers + system_images + system_info + system_pull +) + +ok_count=0 +fail_count=0 + +echo "Running API tests..." +echo "====================" + +for test in "${TESTS[@]}"; do + # Run test, capture HTTP status code only + status=$($API_SCRIPT $test -s -o /dev/null -w "%{http_code}") + + if [[ "$status" == "200" || "$status" == "302" ]]; then + echo "[OK ] $test" + ((ok_count++)) + else + echo "[NOK] $test (HTTP $status)" + ((fail_count++)) + fi +done + +echo "====================" +echo "Summary: $ok_count OK, $fail_count NOK" +exit $fail_count + diff --git a/app/test/test b/app/test/test new file mode 100755 index 0000000..7e125a7 --- /dev/null +++ b/app/test/test @@ -0,0 +1,152 @@ +#!/bin/bash +# +# test_api.sh — cURL test suite for Container Management API +# + +# ========= CONFIG ========= +API_KEY="your-secret-api-key" +CONTAINER_ID="001-002-123e4567-e89b-12d3-a456-426614174000" # sample UUID +BASE_URL="https://dev.odoo4projects.com" +# ========================== + +# --- Functions for each endpoint --- + +root_redirect() { + curl -i "$BASE_URL/" +} + +update_container() { + curl -k -X POST "$BASE_URL/container/update" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $API_KEY" \ + -d '{ + "UUID": "'"$CONTAINER_ID"'", + "email": "user@example.com", + "expires": "2025-12-31", + "status": "active", + "created": "2025-08-16", + "tags": "N8N, SQLITE", + "affiliate": "OLIVER", + "domains": "N8N.local", + "status":"hot", + "env": { + "BRANCH": "release" + }, + "secret": { + "psql": "5Sg9gDxYQH44QNSPyOx" + } + + }' +} + +start_container() { + curl -k -X POST "$BASE_URL/container/start" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $API_KEY" \ + -d '{ "container_id": "'"$CONTAINER_ID"'" }' +} + +stop_container() { + curl -k -X POST "$BASE_URL/container/stop" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $API_KEY" \ + -d '{ "container_id": "'"$CONTAINER_ID"'" }' +} + +nuke_container() { + curl -k -X POST "$BASE_URL/container/nuke" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $API_KEY" \ + -d '{ "container_id": "'"$CONTAINER_ID"'" }' +} + +info_all() { + curl -k -X POST "$BASE_URL/container/info" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $API_KEY" \ + -d '{}' +} + +info_one() { + curl -k -X POST "$BASE_URL/container/info" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $API_KEY" \ + -d '{ "container_id": "'"$CONTAINER_ID"'" }' +} + +bump_container() { + curl -k -X POST "$BASE_URL/container/bump" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $API_KEY" \ + -d '{ "container_id": "'"$CONTAINER_ID"'" }' +} + +container_quota() { + curl -k -X POST "$BASE_URL/container/quota" \ + -H "Content-Type: application/json" \ + -H "X-API-Key: $API_KEY" \ + -d '{ "container_id": "'"$CONTAINER_ID"'" }' +} + +system_containers() { + curl -k -X GET "$BASE_URL/system/containers" \ + -H "X-API-Key: $API_KEY" +} + +system_images() { + curl -k -X GET "$BASE_URL/system/images" \ + -H "X-API-Key: $API_KEY" +} + +system_info() { + curl -k -X GET "$BASE_URL/system/info" \ + -H "X-API-Key: $API_KEY" +} + +system_pull() { + curl -k -X POST "$BASE_URL/system/pull" \ + -H "X-API-Key: $API_KEY" +} + +# --- Help message --- +show_help() { + cat < + +Available commands: + root_redirect - Test root redirect + update_container - Create or update container + start_container - Start container + stop_container - Stop container + nuke_container - Nuke container (status must be "nuke") + info_all - Get info for all containers + info_one - Get info for one container + bump_container - Bump container + container_quota - Show container quota + system_containers - List running containers + system_images - List docker images + system_info - Show system information + system_pull - Pull all container images + +Examples: + $0 update_container + $0 system_info +EOF +} + +# --- Main --- +if [ $# -eq 0 ] || [ "$1" == "-h" ] || [ "$1" == "--help" ]; then + show_help + exit 0 +fi + +cmd="$1" +shift +if declare -f "$cmd" >/dev/null 2>&1; then + "$cmd" "$@" +else + echo "Unknown command: $cmd" + show_help + exit 1 +fi + diff --git a/app/todo.txt b/app/todo.txt new file mode 100644 index 0000000..25bed3f --- /dev/null +++ b/app/todo.txt @@ -0,0 +1,3 @@ +create user +set bash as default user + diff --git a/app/update b/app/update new file mode 100755 index 0000000..5595d66 --- /dev/null +++ b/app/update @@ -0,0 +1,96 @@ +#!/bin/bash + +### SYSTEM SETUP +rex doas mkdir -p /4server +rex doas chmod 777 /4server +rex doas chown 4server:4server /4server +rex mkdir -p /4server/data/log +template templates/hosts /etc/hosts + +### BACKUP DIR +rex doas mkdir -p /BACKUP +rex doas chmod 777 /BACKUP +rex doas chown 4server:4server /BACKUP + + +### TMP DIR +rex doas mkdir -p /4server/tmp +rex doas chmod 777 /4server/tmp +rex doas chown 4server:4server /4server/tmp + + + +template templates/.profile /home/4server/.profile + +### PACKAGES +template templates/repositories /etc/apk/repositories +rex "doas apk update && doas apk upgrade" +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 + +### own bins +echo "Running prsync ./sbin" +prsync -h "/app/host_vars/hosts" -avz ./sbin/ /4server/sbin/ + +### POSTGRESS +rex mkdir -p /4server/data/postgres/etc +template templates/pq_hba.conf /4server/data/postgres/etc/ + + + + +### API + +#INSTALL API KEYS +template templates/4server /etc/4server +rex doas chown root:root /etc/4server +rex doas chmod 600 /etc/4server + +#INSTALL API SERVICE +template templates/init.d/api /etc/init.d/api +rex doas chmod 0755 /etc/init.d/api +rex doas chown root:root /etc/init.d/api +rex doas rc-update add api default +rex doas rc-service api restart + +#INSTALL checkCalls SERVICE +template templates/init.d/checkCalls /etc/init.d/checkCalls +rex doas chmod 0755 /etc/init.d/checkCalls +rex doas chown root:root /etc/init.d/checkCalls +rex doas rc-update add checkCalls default +rex doas rc-service checkCalls restart + +#INSTALL cleanTmp SERVICE +template templates/init.d/cleanTmp /etc/init.d/cleanTmp +rex doas chmod 0755 /etc/init.d/cleanTmp +rex doas chown root:root /etc/init.d/cleanTmp +rex doas rc-update add cleanTmp default +rex doas rc-service cleanTmp restart + +#INSTALL cpu service +template templates/init.d/cpu /etc/init.d/cpu +rex doas chmod 0755 /etc/init.d/cpu +rex doas chown root:root /etc/init.d/cpu +rex doas rc-update add cpu default +rex doas rc-service cpu restart + +### Infrastructure +##### Docker +rex doas rc-service docker start +rex doas rc-update add docker default +rex doas rc-update del docker boot + +#LOGIN ODOO4PROJECTS DOCKER REPO +rex "echo 'Airbus12docker' | doas docker login docker.odoo4projects.com -u admin --password-stdin" + + +rex mkdir -p /4server/data/traefik/etc +template templates/traefik.yaml /4server/data/traefik/etc/traefik.yaml + +rex mkdir -p /4server/data/traefik/etc/certs +echo "prsync traefik certs" +prsync -h "/app/host_vars/hosts" -avz ./etc/traefik/certs/* /4server/data/traefik/etc/certs/ + +template templates/docker-compose.yml /4server/docker-compose.yml +rex doas docker-compose -f /4server/docker-compose.yml up -d --force-recreate diff --git a/app/update_sbin b/app/update_sbin new file mode 100755 index 0000000..fde64f0 --- /dev/null +++ b/app/update_sbin @@ -0,0 +1,13 @@ +#!/bin/bash + +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* diff --git a/app/vault/close b/app/vault/close new file mode 100755 index 0000000..eef46b3 --- /dev/null +++ b/app/vault/close @@ -0,0 +1,35 @@ +#!/bin/sh +set -euo pipefail + +MAPPER_NAME="host_vars_crypt" +MOUNT_POINT="/app/host_vars" + +# Unmount if mounted +if mountpoint -q "$MOUNT_POINT"; then + echo "Unmounting $MOUNT_POINT..." + umount "$MOUNT_POINT" +else + echo "$MOUNT_POINT is not mounted." +fi + + + +if cryptsetup status "$MAPPER_NAME" >/dev/null 2>&1; then + echo "Closing stale mapping $MAPPER_NAME..." + if ! cryptsetup close "$MAPPER_NAME"; then + echo "cryptsetup close failed, forcing dmsetup remove..." + dmsetup remove --force --retry "$MAPPER_NAME" || true + fi +fi + + +# Close the LUKS/dm-crypt device if open +if [ -e "/dev/mapper/$MAPPER_NAME" ]; then + echo "Closing /dev/mapper/$MAPPER_NAME..." + cryptsetup close "$MAPPER_NAME" +else + echo "Mapper $MAPPER_NAME is not active." +fi + +echo "Vault is now closed." + diff --git a/app/vault/create b/app/vault/create new file mode 100755 index 0000000..f8ca7ee --- /dev/null +++ b/app/vault/create @@ -0,0 +1,45 @@ +#!/bin/sh +set -euo pipefail + +VAULT_DIR="/app/vault" +VAULT_FILE="$VAULT_DIR/host_vars.img" +MAPPER_NAME="host_vars_crypt" +MOUNT_POINT="/app/host_vars" +SIZE_MB=25 + +# Prepare directories +mkdir -p "$VAULT_DIR" +mkdir -p "$MOUNT_POINT" + +# Create 5MB backing file if it doesn't exist +if [ ! -f "$VAULT_FILE" ]; then + echo "Creating $SIZE_MB MB vault file at $VAULT_FILE" + dd if=/dev/zero of="$VAULT_FILE" bs=1M count=$SIZE_MB +fi + +# Setup LUKS encryption if not already formatted +if ! cryptsetup isLuks "$VAULT_FILE"; then + echo "Formatting with LUKS (you will be prompted for a passphrase)..." + cryptsetup luksFormat "$VAULT_FILE" +fi + +# Open the encrypted volume +if ! [ -e "/dev/mapper/$MAPPER_NAME" ]; then + echo "Opening encrypted volume..." + cryptsetup open "$VAULT_FILE" "$MAPPER_NAME" +fi + +# Create filesystem if not already present +if ! blkid /dev/mapper/"$MAPPER_NAME" >/dev/null 2>&1; then + echo "Creating ext4 filesystem..." + mkfs.ext4 /dev/mapper/"$MAPPER_NAME" +fi + +# Mount it +if ! mountpoint -q "$MOUNT_POINT"; then + echo "Mounting at $MOUNT_POINT" + mount /dev/mapper/"$MAPPER_NAME" "$MOUNT_POINT" +fi + +echo "Encrypted volume is ready and mounted at $MOUNT_POINT" + diff --git a/app/vault/host_vars.img b/app/vault/host_vars.img new file mode 100644 index 0000000..ddf447b Binary files /dev/null and b/app/vault/host_vars.img differ diff --git a/app/vault/open b/app/vault/open new file mode 100755 index 0000000..43841d3 --- /dev/null +++ b/app/vault/open @@ -0,0 +1,50 @@ +#!/bin/sh +set -euo pipefail + +VAULT_FILE="/app/vault/host_vars.img" +MAPPER_NAME="host_vars_crypt" +MOUNT_POINT="/app/host_vars" +LOOP_DEVICE="/dev/loop50" + +mkdir -p "$MOUNT_POINT" + +# Always close if active +if cryptsetup status "$MAPPER_NAME" >/dev/null 2>&1; then + echo "Closing stale mapping $MAPPER_NAME..." + cryptsetup close "$MAPPER_NAME" +fi + +# Detach loop device if already in use +if losetup "$LOOP_DEVICE" >/dev/null 2>&1; then + echo "Detaching stale loop device $LOOP_DEVICE..." + losetup -d "$LOOP_DEVICE" +fi + +# Create loop device if missing +if [ ! -e "$LOOP_DEVICE" ]; then + echo "Creating loop device $LOOP_DEVICE..." + mknod "$LOOP_DEVICE" b 7 50 + chmod 660 "$LOOP_DEVICE" +fi + +# Attach vault file to loop device +echo "Attaching $VAULT_FILE to $LOOP_DEVICE..." +losetup "$LOOP_DEVICE" "$VAULT_FILE" + +# Open encrypted volume +echo "Opening encrypted volume..." +cryptsetup open "$LOOP_DEVICE" "$MAPPER_NAME" + +# Format if needed +if ! blkid "/dev/mapper/$MAPPER_NAME" >/dev/null 2>&1; then + echo "No filesystem found, creating ext4..." + mkfs.ext4 "/dev/mapper/$MAPPER_NAME" +fi + +# Mount +echo "Mounting at $MOUNT_POINT..." +mount "/dev/mapper/$MAPPER_NAME" "$MOUNT_POINT" + +echo "Vault is mounted at $MOUNT_POINT" + + diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..ff99ace --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,12 @@ +services: + alpine: + build: + context: ./alpine + volumes: + - ./app:/app + - ./exchange:/app/exchange + tty: true + privileged: true + extra_hosts: + - "dev:192.168.9.221" + diff --git a/mount b/mount new file mode 100755 index 0000000..8ade029 --- /dev/null +++ b/mount @@ -0,0 +1,2 @@ +sudo borg mount ssh://e7e9h45y@e7e9h45y.repo.borgbase.com/./repo /mnt + diff --git a/start b/start new file mode 100755 index 0000000..7e1225f --- /dev/null +++ b/start @@ -0,0 +1,15 @@ +#!/bin/bash + +SERVICE_NAME="alpine" + +# Check if the container is running +RUNNING=$(docker compose ps -q $SERVICE_NAME) + +if [ -z "$RUNNING" ]; then + echo "Container not running. Starting..." + docker compose up -d $SERVICE_NAME +fi + +# Connect to the running container +docker compose exec -it $SERVICE_NAME sh +