Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -85,36 +87,6 @@ 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

# 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
Expand All @@ -123,43 +95,16 @@ if [ ! -z "$ADDITIONAL_PROTOCOLS" ]; then

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

port=$(echo "$line" | awk '{print $3}')
list_all_protocols+=("$protocol")
list_all_ports+=("$protocol_port")
list_all_ports+=("$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" \
Expand All @@ -168,13 +113,11 @@ 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"
--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" \
Expand All @@ -183,10 +126,11 @@ else
--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 "httpPort=$http_port")"
fi

ssh_port="$(jq -r '.data.services[] | select(.type == "tcp" and .internalPort == 22) | .externalPort' <<< "$response")"

# Results
# Define high-contrast colors
BOLD='\033[1m'
Expand All @@ -212,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

Expand Down
17 changes: 4 additions & 13 deletions create-a-container/bin/test-curl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ set -euo pipefail
# Usage function
usage() {
cat <<EOF
Usage: $0 <hostname> <ipv4Address> <username> <osRelease> <containerId> <macAddress> <aiContainer> <sshPort> <httpPort> [additionalProtocols] [additionalPorts]
Usage: $0 <hostname> <ipv4Address> <username> <osRelease> <containerId> <macAddress> <aiContainer> <httpPort> [additionalProtocols]

Register a container via the API endpoint.

Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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}"
Expand All @@ -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"
)

Expand All @@ -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 ""
Expand Down
27 changes: 27 additions & 0 deletions create-a-container/models/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
62 changes: 62 additions & 0 deletions create-a-container/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions create-a-container/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading