diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3fec32c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/alpine/.bashrc b/alpine/.bashrc index f674dae..8c4ae6c 100644 --- a/alpine/.bashrc +++ b/alpine/.bashrc @@ -1,9 +1,11 @@ # ~/.bashrc echo "command: mount_volume " -echo "alias: set_dev" +echo "alias: set_prod" -set_dev() { - export HOSTS_FILE="dev" +export hosts_file="/app/hosts.dev" + +set_prod() { + export HOSTS_FILE="/app/hosts.all" echo "HOSTS_FILE set to: $HOSTS_FILE" } diff --git a/alpine/Dockerfile b/alpine/Dockerfile index 30ee51e..6968989 100644 --- a/alpine/Dockerfile +++ b/alpine/Dockerfile @@ -33,11 +33,10 @@ COPY template /usr/bin/ COPY dpush /usr/bin/ COPY create_volume /usr/bin/ COPY mount_volume /usr/bin/ -COPY set_dev /usr/bin/ COPY .bashrc /root/.bashrc -WORKDIR /data +WORKDIR /app CMD ["bash"] diff --git a/alpine/ass b/alpine/ass index 2e9d116..6f255c8 100755 --- a/alpine/ass +++ b/alpine/ass @@ -1,7 +1,5 @@ #!/bin/bash -hosts_file="/mnt/encrypted_volume/${HOSTS_FILE:-hosts.txt}" - cmd="$*" echo "Running on hosts: $cmd" diff --git a/alpine/dpush b/alpine/dpush index 144b489..3927bbd 100644 --- a/alpine/dpush +++ b/alpine/dpush @@ -10,8 +10,6 @@ fi LOCAL_FILE="$1" REMOTE_PATH="$2" -hosts_file="/mnt/encrypted_volume/${HOSTS_FILE:-hosts.txt}" - if [ ! -f "$LOCAL_FILE" ]; then echo "Error: Local file '$LOCAL_FILE' not found." exit 1 diff --git a/alpine/set_dev b/alpine/set_dev deleted file mode 100755 index b15031e..0000000 --- a/alpine/set_dev +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -export HOSTS_FILE="dev" diff --git a/alpine/template b/alpine/template index e78e40e..98615b2 100755 --- a/alpine/template +++ b/alpine/template @@ -4,7 +4,6 @@ if [ "$#" -ne 3 ]; then echo "Usage: $0 " exit 1 fi -hosts_file="/mnt/encrypted_volume/${HOSTS_FILE:-hosts.txt}" localfile="$1" remotefile="$2" 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/bin/OD8N/sbin/api b/app/bin/OD8N/sbin/api new file mode 100755 index 0000000..02a11e4 --- /dev/null +++ b/app/bin/OD8N/sbin/api @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 + +from fastapi import FastAPI, HTTPException, Depends, Response +from fastapi.security.api_key import APIKeyHeader +from fastapi.responses import RedirectResponse +from pydantic import BaseModel +import psutil +import sqlite3 +import subprocess +import os +import sys +import uvicorn +from typing import Optional + +# Constants +DB_PATH = "/OD8N/data/contracts/contracts.db" +BIN_PATH = "/OD8N/sbin" +API_KEY = os.getenv("API_KEY", "your-secret-api-key") + +# FastAPI app +app = FastAPI() + +# Security +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") + + +# ---------------------- Database ---------------------- +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(36), + location CHAR(100), + email CHAR(100), + expires DATE, + tags TEXT, + env TEXT + ) + ''') + conn.commit() + conn.close() + + +# ---------------------- Models ---------------------- +class ContainerModel(BaseModel): + UUID: str + location: str + email: str + expires: str + tags: Optional[str] = None + env: Optional[str] = None + + +class StartContainerRequest(BaseModel): + uuid: str + email: str + + +# ---------------------- Routes ---------------------- +@app.get("/", include_in_schema=False) +def redirect_to_odoo(): + return RedirectResponse(url="https://OD8N.com") + + +@app.post("/startContainer", dependencies=[Depends(verify_api_key)]) +def start_container(request: StartContainerRequest): + try: + result = subprocess.run( + [os.path.join(BIN_PATH, "startContainer"), request.uuid, request.email], + capture_output=True, + text=True, + check=True + ) + return {"status": "success", "output": result.stdout} + except subprocess.CalledProcessError as e: + print(f"Error in /startContainer: {e.stderr}", file=sys.stderr) + raise HTTPException(status_code=500, detail=f"Command failed: {e.stderr}") + + +@app.get("/system", dependencies=[Depends(verify_api_key)]) +def get_system_info(): + try: + with open("/etc/alpine-release") as f: + version = f.read().strip() + return {"alpine_version": version} + except FileNotFoundError: + raise HTTPException(status_code=404, detail="File not found. Press play on tape") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/resources", dependencies=[Depends(verify_api_key)]) +def get_resources(): + mem = psutil.virtual_memory() + disk = psutil.disk_usage("/") + return { + "memory": {"total": mem.total, "available": mem.available, "used": mem.used}, + "disk": {"total": disk.total, "used": disk.used, "free": disk.free}, + "cpu_count": psutil.cpu_count(logical=True), + } + + +@app.get("/containers", dependencies=[Depends(verify_api_key)]) +def get_containers(): + result = subprocess.run(['/OD8N/sbin/getContainers'], capture_output=True, text=True) + if result.returncode != 0: + return Response(content='{"error": "Script failed"}', media_type="application/json", status_code=500) + return Response(content=result.stdout, media_type="application/json") + + +@app.post("/container", dependencies=[Depends(verify_api_key)]) +def upsert_container(container: ContainerModel): + try: + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + cursor.execute("SELECT 1 FROM containers WHERE UUID = ?", (container.UUID,)) + exists = cursor.fetchone() + + if exists: + cursor.execute(""" + UPDATE containers SET + location = ?, email = ?, expires = ?, tags = ?, env = ? + WHERE UUID = ? + """, ( + container.location, container.email, container.expires, + container.tags, container.env, container.UUID + )) + operation = "update" + else: + cursor.execute(""" + INSERT INTO containers (UUID, location, email, expires, tags, env) + VALUES (?, ?, ?, ?, ?, ?) + """, ( + container.UUID, container.location, container.email, + container.expires, container.tags, container.env + )) + operation = "insert" + + conn.commit() + return {"status": "success", "operation": operation} + except Exception as e: + print(f"Error in /container: {e}", file=sys.stderr) + raise HTTPException(status_code=500, detail=str(e)) + finally: + conn.close() + + +# ---------------------- Entry Point ---------------------- +if __name__ == "__main__": + print("Version 0.1") + init_db() + uvicorn.run(app, host="10.5.0.1", port=8888) + diff --git a/app/bin/OD8N/sbin/getContainers b/app/bin/OD8N/sbin/getContainers new file mode 100755 index 0000000..544032a --- /dev/null +++ b/app/bin/OD8N/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/bin/OD8N/sbin/startContainer b/app/bin/OD8N/sbin/startContainer new file mode 100755 index 0000000..90612e8 --- /dev/null +++ b/app/bin/OD8N/sbin/startContainer @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +exec > /OD8N/data/startContainer.log 2>&1 + +echo "$(date '+%Y-%m-%d %H:%M') - startContainer $1" + + +CONTAINER_NAME="$1" + +# Get the hostname of the machine +HOSTNAME=$(hostname) + +mkdir -p /OD8N/data/${CONTAINER_NAME}/n8n +mkdir -p /OD8N/data/${CONTAINER_NAME}/data + +sudo chmod 777 /OD8N/data/${CONTAINER_NAME}/n8n +sudo chmod 777 /OD8N/data/${CONTAINER_NAME}/data + + +# Stop the container if it exists +if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "$(date '+%Y-%m-%d %H:%M') - stopping existing container $CONTAINER_NAME" + docker stop "$CONTAINER_NAME" + docker rm "$CONTAINER_NAME" +fi + + + +docker run -d \ + --name "$CONTAINER_NAME" \ + -p 5678 \ + --cap-add=SYS_ADMIN \ + --security-opt seccomp=unconfined \ + --restart=always \ + -e N8N_HOST="${CONTAINER_NAME}.od8n.com" \ + -e N8N_PORT=5678 \ + -e N8N_PROTOCOL=https \ + -e NODE_ENV=production \ + -e WEBHOOK_URL="https://${CONTAINER_NAME}.od8n.com/" \ + -e GENERIC_TIMEZONE="UTC-3" \ + -v "/OD8N/data/${CONTAINER_NAME}/n8n:/home/node/.n8n" \ + -v "/OD8N/data/${CONTAINER_NAME}/data:/data" \ + --label "traefik.enable=true" \ + --label "traefik.http.routers.${CONTAINER_NAME}.rule=Host(\`${CONTAINER_NAME}.od8n.com\`)" \ + --label "traefik.http.routers.${CONTAINER_NAME}.entrypoints=web,websecure" \ + --label "traefik.http.routers.${CONTAINER_NAME}.tls=true" \ + --label "traefik.http.routers.${CONTAINER_NAME}.tls.certresolver=production" \ + --label "traefik.http.services.${CONTAINER_NAME}.loadbalancer.server.port=5678" \ + --network docker-compose_4projects \ + n8nio/n8n:latest + + +echo "Done $1" diff --git a/app/host_vars/boston b/app/host_vars/boston new file mode 100644 index 0000000..63cc16b --- /dev/null +++ b/app/host_vars/boston @@ -0,0 +1 @@ +API_KEY=4lnZRkRB7ke0A2zkX0T diff --git a/app/host_vars/dev b/app/host_vars/dev new file mode 100644 index 0000000..15411b6 --- /dev/null +++ b/app/host_vars/dev @@ -0,0 +1 @@ +API_KEY=4h6lDzAOVksuCqmhEB3 diff --git a/app/host_vars/london b/app/host_vars/london new file mode 100644 index 0000000..15411b6 --- /dev/null +++ b/app/host_vars/london @@ -0,0 +1 @@ +API_KEY=4h6lDzAOVksuCqmhEB3 diff --git a/app/host_vars/mumbai b/app/host_vars/mumbai new file mode 100644 index 0000000..97f0f96 --- /dev/null +++ b/app/host_vars/mumbai @@ -0,0 +1 @@ +API_KEY=4SSJxWKmuwblhzd3F5L diff --git a/app/host_vars/saopaulo b/app/host_vars/saopaulo new file mode 100644 index 0000000..9122bbb --- /dev/null +++ b/app/host_vars/saopaulo @@ -0,0 +1 @@ +API_KEY=7WxFrFAvQjVIJF1sLzl diff --git a/app/hosts.all b/app/hosts.all new file mode 100755 index 0000000..63abb1a --- /dev/null +++ b/app/hosts.all @@ -0,0 +1,4 @@ +saopaulo +mumbai +boston +london diff --git a/app/hosts.dev b/app/hosts.dev new file mode 100755 index 0000000..38f8e88 --- /dev/null +++ b/app/hosts.dev @@ -0,0 +1 @@ +dev diff --git a/app/od8n b/app/od8n new file mode 100755 index 0000000..e69de29 diff --git a/app/restart b/app/restart new file mode 100755 index 0000000..888a967 --- /dev/null +++ b/app/restart @@ -0,0 +1,4 @@ +#!/bin/ass + +doas apk update +doas apk upgrade diff --git a/app/templates/od8n b/app/templates/od8n new file mode 100644 index 0000000..473e62b --- /dev/null +++ b/app/templates/od8n @@ -0,0 +1 @@ +API_KEY={API_KEY} diff --git a/app/templates/od8n-api b/app/templates/od8n-api new file mode 100644 index 0000000..eaa1d0d --- /dev/null +++ b/app/templates/od8n-api @@ -0,0 +1,25 @@ +#!/sbin/openrc-run + +name="od8n-api" +description="OD8N API Service" + +command="/OD8N/sbin/api" +command_args="" +pidfile="/run/${RC_SVCNAME}.pid" +command_background="yes" + +if [ -f /etc/od8n ]; then + . /etc/od8n + export $(cut -d= -f1 /etc/od8n) +fi + +output_log="/OD8N/data/api.log" +error_log="/OD8N/data/api.log" + +depend() { + need net + use logger dns + after firewall +} + + diff --git a/app/update b/app/update new file mode 100755 index 0000000..e0658ef --- /dev/null +++ b/app/update @@ -0,0 +1,25 @@ +#!/bin/bash + +ass doas mkdir -p /OD8N +ass doas chmod 777 /OD8N +ass mkdir -p /OD8N/data + + +ass doas apk update +ass doas apk upgrade +ass doas apk add jq rsync mc vim + +prsync -h "$hosts_file" -avz ./bin/OD8N/sbin/ /OD8N/sbin/ + +#INSTALL API KEYS +template templates/od8n /OD8N/od8n ./host_vars +ass doas mv /OD8N/od8n /etc/od8n + +#INSTALL API SERVICE +template templates/od8n-api /OD8N/od8n-api ./host_vars/ +ass doas mv /OD8N/od8n-api /etc/init.d/od8n-api +ass doas chmod 0755 /etc/init.d/od8n-api +ass doas chown root:root /etc/init.d/od8n-api +ass doas rc-update add od8n-api default +ass doas rc-service od8n-api restart +ass doas rc-update add od8n-api default diff --git a/data/alpine.qcow2 b/data/alpine.qcow2 deleted file mode 100644 index 9e52b57..0000000 Binary files a/data/alpine.qcow2 and /dev/null differ diff --git a/docker-compose.yaml b/docker-compose.yaml index f3977c8..1a8e5be 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,7 +3,8 @@ services: build: context: ./alpine volumes: - - ./od8n:/data/od8n + - ./app:/app + - ./od8n:/app/od8n tty: true privileged: true diff --git a/start_vm b/start_vm index 16b1635..b393c6a 100755 --- a/start_vm +++ b/start_vm @@ -1,6 +1,6 @@ #/bin/bash -cp alpine.qcow2 data/alpine.qcow2 +cp alpine.qcow2 tmp/alpine.qcow2 -qemu-system-x86_64 -m 1024M -hda data/alpine.qcow2 -boot d -netdev user,id=net0,hostfwd=tcp::2222-:22 -device e1000,netdev=net0 -enable-kvm -cpu host +qemu-system-x86_64 -m 1024M -hda tmp/alpine.qcow2 -boot d -netdev user,id=net0,hostfwd=tcp::2222-:22 -device e1000,netdev=net0 -enable-kvm -cpu host