From 3be9b43b509925883d9eec7065fe3c0173d50bad Mon Sep 17 00:00:00 2001 From: Robert Gingras Date: Tue, 11 Nov 2025 15:38:55 +0000 Subject: [PATCH 1/6] Add stream support in the nginx template --- create-a-container/server.js | 5 +- create-a-container/views/nginx-conf.ejs | 167 +++++++++++++++--------- nginx-reverse-proxy/pull-config.sh | 2 +- 3 files changed, 110 insertions(+), 64 deletions(-) diff --git a/create-a-container/server.js b/create-a-container/server.js index 77542748..8ac750b9 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -206,11 +206,12 @@ app.get('/containers', requireAuth, async (req, res) => { // Generate nginx configuration for a container app.get('/nginx.conf', async (req, res) => { const services = await Service.findAll({ - where: { type: 'http' }, include: [{ model: Container }] }); + const httpServices = services.filter(s => s.type === 'http'); + const streamServices = services.filter(s => s.type === 'tcp' || s.type === 'udp'); res.contentType('text/plain'); - return res.render('nginx-conf', { services }); + return res.render('nginx-conf', { httpServices, streamServices }); }); // Create container diff --git a/create-a-container/views/nginx-conf.ejs b/create-a-container/views/nginx-conf.ejs index a736d478..75f82dc2 100644 --- a/create-a-container/views/nginx-conf.ejs +++ b/create-a-container/views/nginx-conf.ejs @@ -1,72 +1,117 @@ -server_names_hash_bucket_size 128; +user nginx; +worker_processes auto; -<% services.forEach((service, index) => { %> -server { - listen 443 ssl; - listen [::]:443 ssl; - listen 443 quic; - listen [::]:443 quic; - http2 on; - http3 on; - - server_name <%= service.externalHostname %>.opensource.mieweb.org; +error_log /var/log/nginx/error.log notice; +pid /run/nginx.pid; - # SSL certificates - ssl_certificate /root/.acme.sh/opensource.mieweb.org/fullchain.cer; - ssl_certificate_key /root/.acme.sh/opensource.mieweb.org/opensource.mieweb.org.key; - - # Modern TLS configuration - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; - ssl_prefer_server_ciphers off; - - # SSL session optimization - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 10m; - ssl_session_tickets off; - - # OCSP stapling - ssl_stapling on; - ssl_stapling_verify on; - ssl_trusted_certificate /root/.acme.sh/opensource.mieweb.org/fullchain.cer; - resolver 1.1.1.1 8.8.8.8 valid=300s; - resolver_timeout 5s; +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + include /etc/nginx/conf.d/*.conf; + + server_names_hash_bucket_size 128; - # Security headers - add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Alt-Svc 'h3=":443"; ma=86400' always; + <%_ httpServices.forEach((service, index) => { _%> + server { + listen 443 ssl; + listen [::]:443 ssl; + listen 443 quic; + listen [::]:443 quic; + http2 on; + http3 on; + + server_name <%= service.externalHostname %>.opensource.mieweb.org; - # Proxy settings - location / { - proxy_pass http://<%= service.Container.ipv4Address %>:<%= service.internalPort %>; - proxy_http_version 1.1; + # SSL certificates + ssl_certificate /root/.acme.sh/opensource.mieweb.org/fullchain.cer; + ssl_certificate_key /root/.acme.sh/opensource.mieweb.org/opensource.mieweb.org.key; - # Proxy headers - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Port $server_port; + # Modern TLS configuration + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384'; + ssl_prefer_server_ciphers off; - # WebSocket support - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; + # SSL session optimization + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + ssl_session_tickets off; - # Timeouts - proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; + # OCSP stapling + ssl_stapling on; + ssl_stapling_verify on; + ssl_trusted_certificate /root/.acme.sh/opensource.mieweb.org/fullchain.cer; + resolver 1.1.1.1 8.8.8.8 valid=300s; + resolver_timeout 5s; - # Buffering (disable for SSE/streaming) - proxy_buffering off; - proxy_request_buffering off; + # Security headers + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Alt-Svc 'h3=":443"; ma=86400' always; - # Allow large uploads - client_max_body_size 100M; + # Proxy settings + location / { + proxy_pass http://<%= service.Container.ipv4Address %>:<%= service.internalPort %>; + proxy_http_version 1.1; + + # Proxy headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + # WebSocket support + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # Buffering (disable for SSE/streaming) + proxy_buffering off; + proxy_request_buffering off; + + # Allow large uploads + client_max_body_size 100M; + } + } + <%_ }) _%> +} + +stream { + log_format main '$remote_addr [$time_local] ' + '$protocol $status $bytes_sent $bytes_received ' + '$session_time'; + + access_log /var/log/nginx/stream-access.log main; + + <%_ streamServices.forEach((service, index) => { _%> + server { + listen <%= service.externalPort %><%= service.type === 'udp' ? ' udp' : '' %>; + proxy_pass <%= service.Container.ipv4Address %>:<%= service.internalPort %>; } + <%_ }) _%> } -<% }) %> \ No newline at end of file diff --git a/nginx-reverse-proxy/pull-config.sh b/nginx-reverse-proxy/pull-config.sh index e305ed9c..ad59899e 100755 --- a/nginx-reverse-proxy/pull-config.sh +++ b/nginx-reverse-proxy/pull-config.sh @@ -2,7 +2,7 @@ set -euo pipefail -CONF_FILE=/etc/nginx/conf.d/reverse-proxy.conf +CONF_FILE=/etc/nginx/nginx.conf ETAG_FILE="${CONF_FILE}.etag" TEMP_FILE="${CONF_FILE}.tmp" HEADERS_FILE="${CONF_FILE}.headers" From 30007f81b2d14f9a196fab2aebe04d45909a5ba6 Mon Sep 17 00:00:00 2001 From: Robert Gingras Date: Tue, 11 Nov 2025 16:29:59 +0000 Subject: [PATCH 2/6] Move ssh port assignment to create-a-container api --- .../var-lib-vz-snippets/register-container.sh | 25 ----------------- create-a-container/models/service.js | 27 +++++++++++++++++++ create-a-container/server.js | 3 ++- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh b/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh index 1770ba8b..ae97b826 100755 --- a/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh +++ b/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh @@ -92,29 +92,6 @@ else IPTABLES_IFACE="vmbr0" fi -# Check if this container already has a SSH port assigned in PREROUTING -existing_ssh_port=$(iptables -t nat -S PREROUTING | grep "to-destination $container_ip:22" | awk -F'--dport ' '{print $2}' | awk '{print $1}' | head -n 1 || true) - -if [[ -n "$existing_ssh_port" ]]; then - echo "ℹ️ Container already has SSH port $existing_ssh_port" - ssh_port="$existing_ssh_port" -else - # Get used SSH ports - used_ssh_ports=$(iptables -t nat -S PREROUTING | awk -F'--dport ' '/--dport / {print $2}' | awk '/22$/' | awk '{print $1}') - ssh_port=$(comm -23 <(seq 2222 2999 | sort) <(echo "$used_ssh_ports" | sort) | head -n 1) - - if [[ -z "$ssh_port" ]]; then - echo "❌ No available SSH ports found" - exit 2 - fi - - # SSH PREROUTING rule - iptables -t nat -A PREROUTING -i vmbr0 -p tcp --dport "$ssh_port" -j DNAT --to-destination "$container_ip:22" - - # SSH POSTROUTING rule - iptables -t nat -A POSTROUTING -o "$IPTABLES_IFACE" -p tcp -d "$container_ip" --dport 22 -j MASQUERADE - fi - # Take input file of protocols, check if the container already has a port assigned for those protocols in PREROUTING # Store all protocols and ports to write to JSON list later. if [ ! -z "$ADDITIONAL_PROTOCOLS" ]; then @@ -168,7 +145,6 @@ if [ ! -z "$ADDITIONAL_PROTOCOLS" ]; then --data-urlencode "containerId=$CTID" \ --data-urlencode "macAddress=$mac" \ --data-urlencode "aiContainer=$AI_CONTAINER" \ - --data-urlencode "sshPort=$ssh_port" \ --data-urlencode "httpPort=$http_port" \ --data-urlencode "additionalProtocols=$ss_protocols" \ --data-urlencode "additionalPorts=$ss_ports" @@ -183,7 +159,6 @@ else --data-urlencode "containerId=$CTID" \ --data-urlencode "macAddress=$mac" \ --data-urlencode "aiContainer=$AI_CONTAINER" \ - --data-urlencode "sshPort=$ssh_port" \ --data-urlencode "httpPort=$http_port" fi diff --git a/create-a-container/models/service.js b/create-a-container/models/service.js index 3bf9e22c..f8c4045e 100644 --- a/create-a-container/models/service.js +++ b/create-a-container/models/service.js @@ -5,6 +5,33 @@ module.exports = (sequelize, DataTypes) => { static associate(models) { Service.belongsTo(models.Container, { foreignKey: 'containerId' }); } + + // finds the next available external port for the given type in the specified range + static async nextAvailablePortInRange(type, minPort, maxPort) { + // Get all used ports for this type + const usedServices = await Service.findAll({ + where: { + type: type, + externalPort: { + [sequelize.Sequelize.Op.between]: [minPort, maxPort] + } + }, + attributes: ['externalPort'], + order: [['externalPort', 'ASC']] + }); + + const usedPorts = new Set(usedServices.map(s => s.externalPort)); + + // Find the first available port in the range + for (let port = minPort; port <= maxPort; port++) { + if (!usedPorts.has(port)) { + return port; + } + } + + // No available ports in range + throw new Error(`No available ports in range ${minPort}-${maxPort} for type ${type}`); + } } Service.init({ containerId: { diff --git a/create-a-container/server.js b/create-a-container/server.js index 8ac750b9..2c0ae1c2 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -260,6 +260,7 @@ app.post('/containers', async (req, res) => { const aiContainer = req.body.aiContainer || 'N'; const containerId = req.body.containerId; const nodeId = await getNodeForContainer(aiContainer, containerId); + const sshPort = await Service.nextAvailablePortInRange('tcp', 2000, 2999); const container = await Container.create({ ...req.body, @@ -277,7 +278,7 @@ app.post('/containers', async (req, res) => { containerId: container.id, type: 'tcp', internalPort: 22, - externalPort: req.body.sshPort, + externalPort: sshPort, tls: false, externalHostname: null }); From b3518990c0948b1ccb30af048a3186eb337da1d8 Mon Sep 17 00:00:00 2001 From: Robert Gingras Date: Tue, 11 Nov 2025 18:41:32 +0000 Subject: [PATCH 3/6] add morgan request logging --- create-a-container/package-lock.json | 62 ++++++++++++++++++++++++++++ create-a-container/package.json | 1 + create-a-container/server.js | 2 + 3 files changed, 65 insertions(+) diff --git a/create-a-container/package-lock.json b/create-a-container/package-lock.json index 35fe94e0..72450494 100644 --- a/create-a-container/package-lock.json +++ b/create-a-container/package-lock.json @@ -14,6 +14,7 @@ "express-session": "^1.18.2", "express-session-sequelize": "^2.3.0", "method-override": "^3.0.0", + "morgan": "^1.10.1", "mysql2": "^3.15.2", "nodemailer": "^7.0.9", "sequelize": "^6.37.7", @@ -192,6 +193,24 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1591,6 +1610,49 @@ "node": "*" } }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/create-a-container/package.json b/create-a-container/package.json index 05b17e17..678253b8 100644 --- a/create-a-container/package.json +++ b/create-a-container/package.json @@ -13,6 +13,7 @@ "express-session": "^1.18.2", "express-session-sequelize": "^2.3.0", "method-override": "^3.0.0", + "morgan": "^1.10.1", "mysql2": "^3.15.2", "nodemailer": "^7.0.9", "sequelize": "^6.37.7", diff --git a/create-a-container/server.js b/create-a-container/server.js index 2c0ae1c2..4805698e 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -2,6 +2,7 @@ require('dotenv').config(); const express = require('express'); const session = require('express-session'); +const morgan = require('morgan'); const SequelizeStore = require('express-session-sequelize')(session.Store); const flash = require('connect-flash'); const methodOverride = require('method-override'); @@ -26,6 +27,7 @@ app.set('view engine', 'ejs'); app.set('trust proxy', 1); // setup middleware +app.use(morgan('combined')); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Parse form data app.use(methodOverride((req, res) => { From 795a5a1b0d0d394025a5be80baef47a0f5c45489 Mon Sep 17 00:00:00 2001 From: Robert Gingras Date: Tue, 11 Nov 2025 18:57:46 +0000 Subject: [PATCH 4/6] allocate port forwards in api --- .../var-lib-vz-snippets/register-container.sh | 60 ++++--------------- create-a-container/server.js | 14 ++--- 2 files changed, 19 insertions(+), 55 deletions(-) diff --git a/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh b/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh index ae97b826..eff0a6cd 100755 --- a/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh +++ b/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh @@ -15,6 +15,13 @@ proxmox_user="$4" # Optional: AI_CONTAINER environment variable should be exported if running on AI node AI_CONTAINER="${AI_CONTAINER:-N}" +# Overridable API URL for testing +API_URL="${API_URL:-https://create-a-container.opensource.mieweb.org}" + +# Redirect stdout and stderr to a log file +LOGFILE="${LOGFILE:-/var/log/pve-hook-$CTID.log}" +exec > >(tee -a "$LOGFILE") 2>&1 + # run_pct_exec function to handle AI containers run_pct_exec() { local ctid="$1" @@ -59,11 +66,6 @@ run_pct_config() { esac } - -# Redirect stdout and stderr to a log file -LOGFILE="/var/log/pve-hook-$CTID.log" -exec > >(tee -a "$LOGFILE") 2>&1 - # Extract IP container_ip="" attempts=0 @@ -85,58 +87,21 @@ os_release=$(run_pct_exec "$CTID" grep '^ID=' /etc/os-release | cut -d'=' -f2 | # === NEW: Extract MAC address using cluster-aware function === mac=$(run_pct_config "$CTID" | grep -oP 'hwaddr=\K([^\s,]+)') -# Determine which interface to use for iptables rules -if [[ "${AI_CONTAINER^^}" == "FORTWAYNE" ]]; then - IPTABLES_IFACE="wg0" -else - IPTABLES_IFACE="vmbr0" -fi - # Take input file of protocols, check if the container already has a port assigned for those protocols in PREROUTING # Store all protocols and ports to write to JSON list later. if [ ! -z "$ADDITIONAL_PROTOCOLS" ]; then list_all_protocols=() - list_all_ports=() while read line; do protocol=$(echo "$line" | awk '{print $1}') - underlying_protocol=$(echo "$line" | awk '{print $2}') - default_port_number=$(echo "$line" | awk '{print $3}') - - protocol_port="" - existing_port=$(iptables -t nat -S PREROUTING | grep "to-destination $container_ip:$default_port_number" | awk -F'--dport ' '{print $2}' | awk '{print $1}' | head -n 1 || true) - - if [[ -n "$existing_port" ]]; then - # Port already exists, so just assign it to protocol_port - echo "ℹ️ This Container already has a $protocol port at $existing_port" - protocol_port="$existing_port" - else - used_protocol_ports=$(iptables -t nat -S PREROUTING | awk -F'--dport ' '/--dport / {print $2}' | awk '{print $1}') - protocol_port=$(comm -23 <(seq 10001 29999 | sort) <(echo "$used_protocol_ports" | sort) | head -n 1 || true) - - if [[ -z "protocol_port" ]]; then - echo "❌ No available $protocol ports found" - exit 2 - fi - - # Protocol PREROUTING rule - iptables -t nat -A PREROUTING -i vmbr0 -p "$underlying_protocol" --dport "$protocol_port" -j DNAT --to-destination "$container_ip:$default_port_number" - - # Protocol POSTROUTING rule - iptables -t nat -A POSTROUTING -o "$IPTABLES_IFACE" -p "$underlying_protocol" -d "$container_ip" --dport "$default_port_number" -j MASQUERADE - - fi - list_all_protocols+=("$protocol") - list_all_ports+=("$protocol_port") done < <(tac "$ADDITIONAL_PROTOCOLS") # Space Seperate Lists ss_protocols="$(IFS=, ; echo "${list_all_protocols[*]}")" - ss_ports="$(IFS=, ; echo "${list_all_ports[*]}")" # Register container with additional protocols via API - curl -X POST https://create-a-container.opensource.mieweb.org/containers \ + response="$(curl -X POST "$API_URL/containers" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "hostname=$hostname" \ --data-urlencode "ipv4Address=$container_ip" \ @@ -146,11 +111,10 @@ if [ ! -z "$ADDITIONAL_PROTOCOLS" ]; then --data-urlencode "macAddress=$mac" \ --data-urlencode "aiContainer=$AI_CONTAINER" \ --data-urlencode "httpPort=$http_port" \ - --data-urlencode "additionalProtocols=$ss_protocols" \ - --data-urlencode "additionalPorts=$ss_ports" + --data-urlencode "additionalProtocols=$ss_protocols")" else # Register container without additional protocols via API - curl -X POST https://create-a-container.opensource.mieweb.org/containers \ + response="$(curl -X POST "$API_URL/containers" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "hostname=$hostname" \ --data-urlencode "ipv4Address=$container_ip" \ @@ -159,9 +123,11 @@ else --data-urlencode "containerId=$CTID" \ --data-urlencode "macAddress=$mac" \ --data-urlencode "aiContainer=$AI_CONTAINER" \ - --data-urlencode "httpPort=$http_port" + --data-urlencode "httpPort=$http_port")" fi +ssh_port="$(jq -r '.sshPort' <<< "$response")" + # Results # Define high-contrast colors BOLD='\033[1m' diff --git a/create-a-container/server.js b/create-a-container/server.js index 4805698e..78876c13 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -262,7 +262,7 @@ app.post('/containers', async (req, res) => { const aiContainer = req.body.aiContainer || 'N'; const containerId = req.body.containerId; const nodeId = await getNodeForContainer(aiContainer, containerId); - const sshPort = await Service.nextAvailablePortInRange('tcp', 2000, 2999); + const sshPort = await Service.nextAvailablePortInRange('tcp', 2222, 2999); const container = await Container.create({ ...req.body, @@ -284,14 +284,12 @@ app.post('/containers', async (req, res) => { tls: false, externalHostname: null }); - if (req.body.additionalPorts && req.body.additionalProtocols) { - const additionalPorts = req.body.additionalPorts.split(',').map(p => p.trim()); + if (req.body.additionalProtocols) { const additionalProtocols = req.body.additionalProtocols.split(',').map(p => p.trim().toLowerCase()); - for (let i = 0; i < additionalPorts.length; i++) { - const port = parseInt(additionalPorts[i], 10); - const protocol = additionalProtocols[i].toLowerCase(); + additionalProtocols.forEach(async (protocol, _) => { const defaultPort = serviceMap[protocol].port; const underlyingProtocol = serviceMap[protocol].protocol; + const port = await Service.nextAvailablePortInRange(underlyingProtocol, 10001, 29999) const additionalService = await Service.create({ containerId: container.id, type: underlyingProtocol, @@ -300,9 +298,9 @@ app.post('/containers', async (req, res) => { tls: false, externalHostname: null }); - } + }); } - return res.json({ success: true }); + return res.json({ success: true, sshPort }); }); // Delete container From 3c778714727058d366817e1d93951222e91565f7 Mon Sep 17 00:00:00 2001 From: Robert Gingras Date: Tue, 11 Nov 2025 20:02:33 +0000 Subject: [PATCH 5/6] fix port display for additional ports --- .../var-lib-vz-snippets/register-container.sh | 9 +++++++-- create-a-container/server.js | 8 +++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh b/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh index eff0a6cd..0a6a1180 100755 --- a/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh +++ b/container-creation/intern-phxdc-pve1/var-lib-vz-snippets/register-container.sh @@ -91,10 +91,13 @@ mac=$(run_pct_config "$CTID" | grep -oP 'hwaddr=\K([^\s,]+)') # Store all protocols and ports to write to JSON list later. if [ ! -z "$ADDITIONAL_PROTOCOLS" ]; then list_all_protocols=() + list_all_ports=() while read line; do protocol=$(echo "$line" | awk '{print $1}') + port=$(echo "$line" | awk '{print $3}') list_all_protocols+=("$protocol") + list_all_ports+=("$port") done < <(tac "$ADDITIONAL_PROTOCOLS") # Space Seperate Lists @@ -126,7 +129,7 @@ else --data-urlencode "httpPort=$http_port")" fi -ssh_port="$(jq -r '.sshPort' <<< "$response")" +ssh_port="$(jq -r '.data.services[] | select(.type == "tcp" and .internalPort == 22) | .externalPort' <<< "$response")" # Results # Define high-contrast colors @@ -153,7 +156,9 @@ echo -e "🌐 ${BLUE}HTTP Port :${RESET} $http_port" # Additional protocols (if any) if [ ! -z "$ADDITIONAL_PROTOCOLS" ]; then for i in "${!list_all_protocols[@]}"; do - echo -e "📡 ${CYAN}${list_all_protocols[$i]} Port :${RESET} ${list_all_ports[$i]}" + internal_port="${list_all_ports[$i]}" + service_info="$(jq -r --arg port "$internal_port" '.data.services[] | select(.internalPort == ($port | tonumber)) | "\(.externalPort)/\(.type)"' <<< "$response")" + echo -e "📡 ${CYAN}${list_all_protocols[$i]} Port :${RESET} $service_info" done fi diff --git a/create-a-container/server.js b/create-a-container/server.js index 78876c13..0b1d9f04 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -284,9 +284,10 @@ app.post('/containers', async (req, res) => { tls: false, externalHostname: null }); + const services = [httpService, sshService]; if (req.body.additionalProtocols) { const additionalProtocols = req.body.additionalProtocols.split(',').map(p => p.trim().toLowerCase()); - additionalProtocols.forEach(async (protocol, _) => { + for (const protocol of additionalProtocols) { const defaultPort = serviceMap[protocol].port; const underlyingProtocol = serviceMap[protocol].protocol; const port = await Service.nextAvailablePortInRange(underlyingProtocol, 10001, 29999) @@ -298,9 +299,10 @@ app.post('/containers', async (req, res) => { tls: false, externalHostname: null }); - }); + services.push(additionalService); + } } - return res.json({ success: true, sshPort }); + return res.json({ success: true, data: { ...container.toJSON(), services } }); }); // Delete container From 1a57c447da468d982445c03f9cd84e3d802d8e39 Mon Sep 17 00:00:00 2001 From: Robert Gingras Date: Tue, 11 Nov 2025 20:02:41 +0000 Subject: [PATCH 6/6] update test-curl.sh --- create-a-container/bin/test-curl.sh | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/create-a-container/bin/test-curl.sh b/create-a-container/bin/test-curl.sh index a5e782bd..cca2caf7 100755 --- a/create-a-container/bin/test-curl.sh +++ b/create-a-container/bin/test-curl.sh @@ -5,7 +5,7 @@ set -euo pipefail # Usage function usage() { cat < [additionalProtocols] [additionalPorts] +Usage: $0 [additionalProtocols] Register a container via the API endpoint. @@ -17,12 +17,10 @@ Required Parameters: containerId Container ID (CTID) macAddress MAC address of the container aiContainer AI container type (N, PHOENIX, or FORTWAYNE) - sshPort External SSH port httpPort HTTP port Optional Parameters: additionalProtocols Comma-separated list of additional protocol names - additionalPorts Comma-separated list of additional port numbers Environment Variables: CONTAINER_API_URL Override the API endpoint URL @@ -36,7 +34,7 @@ Examples: $0 test-container 10.15.7.7 rgingras debian 123 AA:BB:CC:DD:EE:FF N 2222 80 "dns,smtp" "5353,2525" # Override URL - CONTAINER_API_URL=https://create-a-container-dev.opensource.mieweb.org/containers $0 test-container 10.15.7.7 rgingras debian 123 AA:BB:CC:DD:EE:FF N 2222 80 + CONTAINER_API_URL=https://create-a-container-dev.opensource.mieweb.org/containers $0 test-container 10.15.7.7 rgingras debian 123 AA:BB:CC:DD:EE:FF N 80 EOF exit 1 } @@ -56,12 +54,10 @@ osRelease="$4" containerId="$5" macAddress="$6" aiContainer="$7" -sshPort="$8" -httpPort="$9" +httpPort="$8" # Optional parameters -additionalProtocols="${10:-}" -additionalPorts="${11:-}" +additionalProtocols="${9:-}" # Default URL url="${CONTAINER_API_URL:-http://localhost:3000/containers}" @@ -83,7 +79,6 @@ curl_cmd=( --data-urlencode "containerId=$containerId" --data-urlencode "macAddress=$macAddress" --data-urlencode "aiContainer=$aiContainer" - --data-urlencode "sshPort=$sshPort" --data-urlencode "httpPort=$httpPort" ) @@ -92,10 +87,6 @@ if [[ -n "$additionalProtocols" ]]; then curl_cmd+=(--data-urlencode "additionalProtocols=$additionalProtocols") fi -if [[ -n "$additionalPorts" ]]; then - curl_cmd+=(--data-urlencode "additionalPorts=$additionalPorts") -fi - # Execute curl "${curl_cmd[@]}" echo ""