From 57da484eb9c10799bee6f1694fc3090fe293f43a Mon Sep 17 00:00:00 2001 From: Carter Myers <206+cmyers@users.noreply.github.mieweb.com> Date: Fri, 3 Oct 2025 15:09:25 -0700 Subject: [PATCH 1/4] Add EW AI container support to creation workflow Introduces AI container selection (Phoenix, Fort Wayne, or None) to the container creation form and workflow. Updates scripts to handle remote Proxmox hosts for AI containers, adds cluster-aware pct command wrappers, and ensures MAC address and CTID are included in port mapping. Validates AI container input and refactors logic for remote execution and provisioning. --- container-creation/configureLDAP.sh | 72 +++- container-creation/register-container.sh | 78 +++- .../ssh/create-container-new.sh | 359 ++++++++++++++++++ .../create-container-wrapper.sh | 16 +- create-a-container/views/form.html | 7 + 5 files changed, 505 insertions(+), 27 deletions(-) create mode 100644 container-creation/ssh/create-container-new.sh diff --git a/container-creation/configureLDAP.sh b/container-creation/configureLDAP.sh index 950601e2..a9b750fd 100755 --- a/container-creation/configureLDAP.sh +++ b/container-creation/configureLDAP.sh @@ -1,34 +1,70 @@ #!/bin/bash # Script to connect a container to the LDAP server via SSSD -# Last Modified by Maxwell Klema on July 29th, 2025 +# Last Modified by Maxwell Klema, updated October 3rd 2025 by Carter Myers # ----------------------------------------------------- -# Curl Pown.sh script to install SSSD and configure LDAP -pct enter $CONTAINER_ID <&2 + exit 1 + ;; +esac + +# === Wrappers for pct commands === +run_pct_exec() { + local ctid="$1" + shift + if [ -n "$TARGET_HYPERVISOR" ]; then + # Safe quoting for remote execution + local remote_cmd + printf -v remote_cmd '%q ' "$@" + ssh root@$TARGET_HYPERVISOR "pct exec $ctid -- $remote_cmd" + else + pct exec "$ctid" -- "$@" + fi +} + +run_pct() { + if [ -n "$TARGET_HYPERVISOR" ]; then + ssh root@$TARGET_HYPERVISOR "pct $*" + else + pct "$@" + fi +} + +# === LDAP / SSSD Configuration Steps === + +# Curl Pown.sh script to install SSSD and configure LDAP +run_pct_exec $CONTAINER_ID bash -c " cd /root && \ curl -O https://raw.githubusercontent.com/anishapant21/pown.sh/main/pown.sh > /dev/null 2>&1 && \ chmod +x pown.sh -EOF +" # Copy .env file to container ENV_FILE="/var/lib/vz/snippets/.env" -pct enter $CONTAINER_ID < /root/.env -$(cat "$ENV_FILE") -EOT -EOF +ENV_CONTENT=$(<"$ENV_FILE" sed 's/["\$`]/\\&/g') # Escape special characters +run_pct_exec $CONTAINER_ID bash -c "printf '%s\n' \"$ENV_CONTENT\" > /root/.env" # Run the pown.sh script to configure LDAP -pct exec $CONTAINER_ID -- bash -c "cd /root && ./pown.sh" > /dev/null 2>&1 +run_pct_exec $CONTAINER_ID bash -c "cd /root && ./pown.sh" > /dev/null 2>&1 -# remove ldap_tls_cert from /etc/sssd/sssd.conf -pct exec $CONTAINER_ID -- sed -i '/ldap_tls_cacert/d' /etc/sssd/sssd.conf > /dev/null 2>&1 +# Remove ldap_tls_cert from /etc/sssd/sssd.conf +run_pct_exec $CONTAINER_ID sed -i '/ldap_tls_cacert/d' /etc/sssd/sssd.conf > /dev/null 2>&1 # Add TLS_REQCERT to never in ROCKY - if [ "${LINUX_DISTRO^^}" == "ROCKY" ]; then - pct exec $CONTAINER_ID -- bash -c "echo 'TLS_REQCERT never' >> /etc/openldap/ldap.conf" > /dev/null 2>&1 - pct exec $CONTAINER_ID -- bash -c "authselect select sssd --force" > /dev/null 2>&1 - pct exec $CONTAINER_ID -- bash -c "systemctl restart sssd" > /dev/null 2>&1 -fi + run_pct_exec $CONTAINER_ID bash -c "echo 'TLS_REQCERT never' >> /etc/openldap/ldap.conf" > /dev/null 2>&1 + run_pct_exec $CONTAINER_ID bash -c "authselect select sssd --force" > /dev/null 2>&1 + run_pct_exec $CONTAINER_ID bash -c "systemctl restart sssd" > /dev/null 2>&1 +fi \ No newline at end of file diff --git a/container-creation/register-container.sh b/container-creation/register-container.sh index 2a0ee4fb..a3fae91d 100644 --- a/container-creation/register-container.sh +++ b/container-creation/register-container.sh @@ -2,7 +2,6 @@ set -euo pipefail - if [[ -z "${1-}" || -z "${2-}" || -z "${4-}" ]]; then echo "Usage: $0 " exit 1 @@ -13,6 +12,54 @@ http_port="$2" ADDITIONAL_PROTOCOLS="${3-}" proxmox_user="$4" +# Optional: AI_CONTAINER environment variable should be exported if running on AI node +AI_CONTAINER="${AI_CONTAINER:-N}" + +# run_pct_exec function to handle AI containers +run_pct_exec() { + local ctid="$1" + shift + local remote_cmd + printf -v remote_cmd '%q ' "$@" + + case "${AI_CONTAINER^^}" in + PHOENIX) + ssh root@10.15.0.6 "pct exec $ctid -- $remote_cmd" + ;; + FORTWAYNE) + ssh root@10.250.0.2 "pct exec $ctid -- $remote_cmd" + ;; + N|"") + pct exec "$ctid" -- "$@" + ;; + *) + echo "❌ Invalid AI_CONTAINER value: $AI_CONTAINER" >&2 + exit 1 + ;; + esac +} + +# run_pct_config function to fetch config +run_pct_config() { + local ctid="$1" + case "${AI_CONTAINER^^}" in + PHOENIX) + ssh root@10.15.0.6 "pct config $ctid" + ;; + FORTWAYNE) + ssh root@10.250.0.2 "pct config $ctid" + ;; + N|"") + pct config "$ctid" + ;; + *) + echo "❌ Invalid AI_CONTAINER value: $AI_CONTAINER" >&2 + exit 1 + ;; + esac +} + + # Redirect stdout and stderr to a log file LOGFILE="/var/log/pve-hook-$CTID.log" exec > >(tee -a "$LOGFILE") 2>&1 @@ -23,8 +70,8 @@ attempts=0 max_attempts=5 while [[ -z "$container_ip" && $attempts -lt $max_attempts ]]; do - container_ip=$(pct exec "$CTID" -- ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d'/' -f1) - [[ -z "$container_ip" ]] && sleep 2 && ((attempts++)) + container_ip=$(run_pct_exec "$CTID" ip -4 addr show eth0 | awk '/inet / {print $2}' | cut -d'/' -f1) + [[ -z "$container_ip" ]] && sleep 2 && ((attempts++)) done if [[ -z "$container_ip" ]]; then @@ -32,8 +79,11 @@ if [[ -z "$container_ip" ]]; then exit 1 fi -hostname=$(pct exec "$CTID" -- hostname) -os_release=$(pct exec "$CTID" -- grep '^ID=' /etc/os-release | cut -d'=' -f2 | tr -d "\"") +hostname=$(run_pct_exec "$CTID" hostname) +os_release=$(run_pct_exec "$CTID" grep '^ID=' /etc/os-release | cut -d'=' -f2 | tr -d '"') + +# === NEW: Extract MAC address using cluster-aware function === +mac=$(run_pct_config "$CTID" | grep -oP 'hwaddr=\K([^\s,]+)') # 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) @@ -102,7 +152,7 @@ if [ ! -z "$ADDITIONAL_PROTOCOLS" ]; then #Update NGINX port map JSON on the remote host safely using a heredoc and positional parameters - ssh root@10.15.20.69 bash -s -- "$hostname" "$container_ip" "$ssh_port" "$http_port" "$ss_protocols" "$ss_ports" "$proxmox_user" "$os_release" <<'EOF' + ssh root@10.15.20.69 bash -s -- "$hostname" "$container_ip" "$ssh_port" "$http_port" "$ss_protocols" "$ss_ports" "$proxmox_user" "$os_release" "$CTID" "$mac" <<'EOF' set -euo pipefail hostname="$1" @@ -113,6 +163,8 @@ protos_json=$(echo "$5" | tr ',' '\n' | jq -R . | jq -s .) ports_json=$(echo "$6" | tr ',' '\n' | jq -R . | jq -s 'map(tonumber)') user="$7" os_release="$8" +ctid="$9" +mac="${10}" jq --arg hn "$hostname" \ --arg ip "$container_ip" \ @@ -122,10 +174,14 @@ jq --arg hn "$hostname" \ --argjson http "$http_port" \ --argjson protos "$protos_json" \ --argjson ports_list "$ports_json" \ + --argjson ctid "$ctid" \ + --arg mac "$mac" \ '. + {($hn): { ip: $ip, user: $user, os_release: $osr, + ctid: $ctid, + mac: $mac, ports: ( reduce range(0; $protos | length) as $i ( {ssh: $ssh, http: $http}; . + { ($protos[$i]): $ports_list[$i]} @@ -137,7 +193,7 @@ nginx -s reload EOF else # Update NGINX port map JSON on the remote host safely using a heredoc and positional parameters - ssh root@10.15.20.69 bash -s -- "$hostname" "$container_ip" "$ssh_port" "$http_port" "$proxmox_user" "$os_release" <<'EOF' + ssh root@10.15.20.69 bash -s -- "$hostname" "$container_ip" "$ssh_port" "$http_port" "$proxmox_user" "$os_release" "$CTID" "$mac" <<'EOF' set -euo pipefail hostname="$1" @@ -146,6 +202,8 @@ ssh_port="$3" http_port="$4" user="$5" os_release="$6" +ctid="$7" +mac="$8" jq --arg hn "$hostname" \ --arg ip "$container_ip" \ @@ -153,10 +211,14 @@ jq --arg hn "$hostname" \ --arg osr "$os_release" \ --argjson http "$http_port" \ --argjson ssh "$ssh_port" \ + --argjson ctid "$ctid" \ + --arg mac "$mac" \ '. + {($hn): { ip: $ip, user: $user, os_release: $osr, + ctid: $ctid, + mac: $mac, ports: {ssh: $ssh, http: $http} }}' /etc/nginx/port_map.json > /tmp/port_map.json.new @@ -195,4 +257,4 @@ if [ ! -z "$ADDITIONAL_PROTOCOLS" ]; then fi # Bottom border -echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" \ No newline at end of file diff --git a/container-creation/ssh/create-container-new.sh b/container-creation/ssh/create-container-new.sh new file mode 100644 index 00000000..7890ede7 --- /dev/null +++ b/container-creation/ssh/create-container-new.sh @@ -0,0 +1,359 @@ +#!/bin/bash +# Script to create the pct container, run register container, and migrate container accordingly. +# Last Modified by October 3rd, 2025 by Carter Myers +# ----------------------------------------------------- + +BOLD='\033[1m' +BLUE='\033[34m' +MAGENTA='\033[35m' +GREEN='\033[32m' +RESET='\033[0m' + +cleanup() { + echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + echo "⚠️ Script was abruptly exited. Running cleanup tasks." + echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + pct_unlock $CTID_TEMPLATE + for file in \ + "/var/lib/vz/snippets/container-public-keys/$PUB_FILE" \ + "/var/lib/vz/snippets/container-port-maps/$PROTOCOL_FILE" \ + "/var/lib/vz/snippets/container-env-vars/$ENV_BASE_FOLDER" \ + "/var/lib/vz/snippets/container-services/$SERVICES_BASE_FILE" + do + [ -f "$file" ] && rm -rf "$file" + done + exit 1 +} + +echoContainerDetails() { + echo -e "📦 ${BLUE}Container ID :${RESET} $CONTAINER_ID" + echo -e "🌐 ${MAGENTA}Internal IP :${RESET} $CONTAINER_IP" + echo -e "🔗 ${GREEN}Domain Name :${RESET} https://$CONTAINER_NAME.opensource.mieweb.org" + echo -e "🛠️ ${BLUE}SSH Access :${RESET} ssh -p $SSH_PORT $PROXMOX_USERNAME@$CONTAINER_NAME.opensource.mieweb.org" + echo -e "🔑 ${BLUE}Container Password :${RESET} Your proxmox account password" + echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + echo -e "${BOLD}${MAGENTA}NOTE: Additional background scripts are being ran in detached terminal sessions.${RESET}" + echo -e "${BOLD}${MAGENTA}Wait up to two minutes for all processes to complete.${RESET}" + echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + echo -e "${BOLD}${BLUE}Still not working? Contact Max K. at maxklema@gmail.com${RESET}" + echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" +} + +trap cleanup SIGINT SIGTERM SIGHUP + +CONTAINER_NAME="$1" +GH_ACTION="$2" +HTTP_PORT="$3" +PROXMOX_USERNAME="$4" +USERNAME_ONLY="${PROXMOX_USERNAME%@*}" +PUB_FILE="$5" +PROTOCOL_FILE="$6" + +# Deployment ENVS +DEPLOY_ON_START="$7" +PROJECT_REPOSITORY="$8" +PROJECT_BRANCH="$9" +PROJECT_ROOT="${10}" +INSTALL_COMMAND=$(echo "${11}" | base64 -d) +BUILD_COMMAND=$(echo "${12}" | base64 -d) +START_COMMAND=$(echo "${13}" | base64 -d) +RUNTIME_LANGUAGE=$(echo "${14}" | base64 -d) +ENV_BASE_FOLDER="${15}" +SERVICES_BASE_FILE="${16}" +LINUX_DISTRO="${17}" +MULTI_COMPONENTS="${18}" +ROOT_START_COMMAND="${19}" +SELF_HOSTED_RUNNER="${20}" +VERSIONS_DICT=$(echo "${21}" | base64 -d) +AI_CONTAINER="${22}" # new argument from HTML form + +echo "PROJECT ROOT: \"$PROJECT_ROOT\"" +echo "AI_CONTAINER: \"$AI_CONTAINER\"" + +# === Determine target PVE host based on AI_CONTAINER === +# PHOENIX -> 10.15.0.6 (existing AI host) +# FORTWAYNE -> 10.250.0.2 (new WireGuard-connected host) +# N -> local execution (no SSH proxy) +case "${AI_CONTAINER^^}" in + PHOENIX) + TARGET_PVE_HOST="10.15.0.6" + ;; + FORTWAYNE) + TARGET_PVE_HOST="10.250.0.2" + ;; + N|"" ) + TARGET_PVE_HOST="" + ;; + *) + echo "Invalid AI_CONTAINER value: $AI_CONTAINER" + exit 1 + ;; +esac + +# Helper: returns true if we're using a remote PVE host (PHOENIX or FORTWAYNE) +is_remote_pve() { + [[ -n "$TARGET_PVE_HOST" ]] +} + +# === Wrapper for pct exec (and optionally pct commands for remote PVE) === +run_pct_exec() { + local ctid="$1" + shift + if is_remote_pve; then + ssh root@"$TARGET_PVE_HOST" "pct exec $ctid -- $*" + else + pct exec "$ctid" -- "$@" + fi +} + +run_pct() { + # $@ = full pct command, e.g., clone, set, start, etc. + if is_remote_pve; then + ssh root@"$TARGET_PVE_HOST" "pct $*" + else + pct "$@" + fi +} + +run_pveum() { + # Wrapper for pveum commands in remote case + if is_remote_pve; then + ssh root@"$TARGET_PVE_HOST" "pveum $*" + else + pveum "$@" + fi +} + +run_pvesh() { + if is_remote_pve; then + ssh root@"$TARGET_PVE_HOST" "pvesh $*" + else + pvesh "$@" + fi +} + +run_pct_push() { + local ctid="$1" + local src="$2" + local dest="$3" + if is_remote_pve; then + ssh root@"$TARGET_PVE_HOST" "pct push $ctid $src $dest" + else + pct push "$ctid" "$src" "$dest" + fi +} + +# === Template Selection & Clone === +if [[ "${AI_CONTAINER^^}" == "PHOENIX" ]]; then + echo "⏳ Phoenix AI container requested. Using template CTID 163..." + CTID_TEMPLATE="163" + # Request cluster nextid from the target (remote if configured, else local via run_pvesh) + CONTAINER_ID=$(run_pvesh get /cluster/nextid) + + echo "DEBUG: Cloning on TARGET_PVE_HOST=${TARGET_PVE_HOST:-local} CTID_TEMPLATE=${CTID_TEMPLATE} -> CONTAINER_ID=${CONTAINER_ID}" + run_pct clone $CTID_TEMPLATE $CONTAINER_ID \ + --hostname $CONTAINER_NAME \ + --full true + + run_pct set $CONTAINER_ID \ + --tags "$PROXMOX_USERNAME" \ + --tags "$LINUX_DISTRO" \ + --tags "AI" \ + --onboot 1 + + run_pct start $CONTAINER_ID + run_pveum aclmod /vms/$CONTAINER_ID --user "$PROXMOX_USERNAME@pve" --role PVEVMUser + +elif [[ "${AI_CONTAINER^^}" == "FORTWAYNE" ]]; then + echo "⏳ Fort Wayne AI container requested. Using template CTID 103 on 10.250.0.2..." + CTID_TEMPLATE="103" + # allocate nextid directly on Fort Wayne + CONTAINER_ID=$(ssh root@10.250.0.2 pvesh get /cluster/nextid) + + echo "DEBUG: Cloning on Fort Wayne (10.250.0.2) CTID_TEMPLATE=${CTID_TEMPLATE} -> CONTAINER_ID=${CONTAINER_ID}" + ssh root@10.250.0.2 pct clone $CTID_TEMPLATE $CONTAINER_ID \ + --hostname $CONTAINER_NAME \ + --full true + + ssh root@10.250.0.2 pct set $CONTAINER_ID \ + --tags "$PROXMOX_USERNAME" \ + --tags "$LINUX_DISTRO" \ + --tags "AI" \ + --onboot 1 + + ssh root@10.250.0.2 pct start $CONTAINER_ID + ssh root@10.250.0.2 pveum aclmod /vms/$CONTAINER_ID --user "$PROXMOX_USERNAME@pve" --role PVEVMUser + +else + REPO_BASE_NAME=$(basename -s .git "$PROJECT_REPOSITORY") + REPO_BASE_NAME_WITH_OWNER=$(echo "$PROJECT_REPOSITORY" | cut -d'/' -f4) + + TEMPLATE_NAME="template-$REPO_BASE_NAME-$REPO_BASE_NAME_WITH_OWNER" + # Search local and other known PVE (keeps original approach; will find local or remote templates depending on your environment) + CTID_TEMPLATE=$( { pct list; ssh root@10.15.0.5 'pct list'; } | awk -v name="$TEMPLATE_NAME" '$3 == name {print $1}') + + case "${LINUX_DISTRO^^}" in + DEBIAN) PACKAGE_MANAGER="apt-get" ;; + ROCKY) PACKAGE_MANAGER="dnf" ;; + esac + + if [ -z "$CTID_TEMPLATE" ]; then + case "${LINUX_DISTRO^^}" in + DEBIAN) CTID_TEMPLATE="160" ;; + ROCKY) CTID_TEMPLATE="138" ;; + esac + fi + + # For non-AI containers, allocate next ID locally and clone once here + CONTAINER_ID=$(pvesh get /cluster/nextid) + + echo "⏳ Cloning Container (non-AI)... CTID_TEMPLATE=${CTID_TEMPLATE} -> CONTAINER_ID=${CONTAINER_ID}" + run_pct clone $CTID_TEMPLATE $CONTAINER_ID \ + --hostname $CONTAINER_NAME \ + --full true + + echo "⏳ Setting Container Properties..." + run_pct set $CONTAINER_ID \ + --tags "$PROXMOX_USERNAME" \ + --tags "$LINUX_DISTRO" \ + --tags "LDAP" \ + --onboot 1 + + run_pct start $CONTAINER_ID + run_pveum aclmod /vms/$CONTAINER_ID --user "$PROXMOX_USERNAME@pve" --role PVEVMUser +fi + +# === Post-Provisioning (pct exec wrapped) === +if [ -f "/var/lib/vz/snippets/container-public-keys/$PUB_FILE" ]; then + echo "⏳ Appending Public Key..." + run_pct_exec $CONTAINER_ID touch ~/.ssh/authorized_keys > /dev/null 2>&1 + # Use a here-doc to reliably feed the pubkey to the remote pct exec + if is_remote_pve; then + # copy key file to remote PVE host temporarily then pct push it in case pct exec over ssh doesn't accept stdin redirection + scp "/var/lib/vz/snippets/container-public-keys/$PUB_FILE" root@"$TARGET_PVE_HOST":/tmp/"$PUB_FILE" > /dev/null 2>&1 || true + ssh root@"$TARGET_PVE_HOST" "pct push $CONTAINER_ID /tmp/$PUB_FILE /root/.ssh/authorized_keys >/dev/null 2>&1 || (pct exec $CONTAINER_ID -- bash -lc 'cat > ~/.ssh/authorized_keys' < /tmp/$PUB_FILE)" + ssh root@"$TARGET_PVE_HOST" "rm -f /tmp/$PUB_FILE" >/dev/null 2>&1 || true + else + run_pct_exec $CONTAINER_ID bash -c "cat > ~/.ssh/authorized_keys" < /var/lib/vz/snippets/container-public-keys/$PUB_FILE > /dev/null 2>&1 + rm -rf /var/lib/vz/snippets/container-public-keys/$PUB_FILE > /dev/null 2>&1 + fi +fi + +ROOT_PSWD=$(tr -dc 'A-Za-z0-9' /dev/null 2>&1 + +CONTAINER_IP="" +attempts=0 +max_attempts=10 + +while [[ -z "$CONTAINER_IP" && $attempts -lt $max_attempts ]]; do + CONTAINER_IP=$(run_pct_exec "$CONTAINER_ID" hostname -I | awk '{print $1}') + [[ -z "$CONTAINER_IP" ]] && sleep 2 && ((attempts++)) +done + +if [[ -z "$CONTAINER_IP" ]]; then + echo "❌ Timed out waiting for container to get an IP address." + exit 1 +fi + +echo "⏳ Updatng container packages..." +if [[ "${LINUX_DISTRO^^}" == "ROCKY" ]]; then + run_pct_exec $CONTAINER_ID bash -c "dnf upgrade -y" +else + run_pct_exec $CONTAINER_ID bash -c "apt-get update && apt-get upgrade -y" +fi + +echo "⏳ Configuring LDAP connection via SSSD..." +export AI_CONTAINER="$AI_CONTAINER" +source /var/lib/vz/snippets/helper-scripts/configureLDAP.sh + +echo "⏳ Setting up Wazuh-Agent..." +source /var/lib/vz/snippets/Wazuh/register-agent.sh + +if [ "${DEPLOY_ON_START^^}" == "Y" ]; then + source /var/lib/vz/snippets/helper-scripts/deployOnStart.sh + for file in \ + "/var/lib/vz/snippets/container-env-vars/$ENV_BASE_FOLDER" \ + "/var/lib/vz/snippets/container-services/$SERVICES_BASE_FILE" + do + [ -f "$file" ] && rm -rf "$file" > /dev/null 2>&1 + done +fi + +run_pct_exec $CONTAINER_ID bash -c "cd /root && touch container-updates.log" + +echo "⏳ Running Container Provision Script..." +if [ -f "/var/lib/vz/snippets/container-port-maps/$PROTOCOL_FILE" ]; then + /var/lib/vz/snippets/register-container.sh $CONTAINER_ID $HTTP_PORT /var/lib/vz/snippets/container-port-maps/$PROTOCOL_FILE "$USERNAME_ONLY" + rm -rf /var/lib/vz/snippets/container-port-maps/$PROTOCOL_FILE > /dev/null 2>&1 +else + /var/lib/vz/snippets/register-container.sh $CONTAINER_ID $HTTP_PORT "" "$PROXMOX_USERNAME" +fi + +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) + +echo "Adding container MOTD information..." +# port_map.json remains on central nginx host (10.15.20.69) — leave as-is unless you want to change that behavior +scp 10.15.20.69:/etc/nginx/port_map.json /tmp/port_map.json +CONTAINER_INFO=$(jq -r --arg hn "$CONTAINER_NAME" '.[$hn]' /tmp/port_map.json) + +if [ "$CONTAINER_INFO" != "null" ]; then + HOSTNAME="$CONTAINER_NAME" + IP=$(echo "$CONTAINER_INFO" | jq -r '.ip') + OWNER=$(echo "$CONTAINER_INFO" | jq -r '.user') + OS_RELEASE=$(echo "$CONTAINER_INFO" | jq -r '.os_release') + PORTS=$(echo "$CONTAINER_INFO" | jq -r '.ports | to_entries[] | "\(.key): \(.value)"' | paste -sd ", " -) + PROTOCOLS=$(echo "$CONTAINER_INFO" | jq -r '.ports | keys | join(", ")') + + cat < /tmp/container_motd +Container Information: +---------------------- +Hostname : $HOSTNAME +IP Address : $IP +Ports : $PORTS +Protocols : $PROTOCOLS +Primary Owner : $OWNER +OS Release : $OS_RELEASE +EOF +else + echo "No container info found for $CONTAINER_NAME" > /tmp/container_motd +fi + +run_pct_push $CONTAINER_ID /tmp/container_motd /etc/motd + +echoContainerDetails + +BUILD_COMMAND_B64=$(echo -n "$BUILD_COMMAND" | base64) +RUNTIME_LANGUAGE_B64=$(echo -n "$RUNTIME_LANGUAGE" | base64) +START_COMMAND_B64=$(echo -n "$START_COMMAND" | base64) + +# Only run start_services when this is NOT an AI container (previously referenced undefined $AI) +if [[ "${AI_CONTAINER^^}" != "PHOENIX" && "${AI_CONTAINER^^}" != "FORTWAYNE" ]]; then + CMD=( + bash /var/lib/vz/snippets/start_services.sh + "$CONTAINER_ID" + "$CONTAINER_NAME" + "$REPO_BASE_NAME" + "$REPO_BASE_NAME_WITH_OWNER" + "$SSH_PORT" + "$CONTAINER_IP" + "$PROJECT_ROOT" + "$ROOT_START_COMMAND" + "$DEPLOY_ON_START" + "$MULTI_COMPONENTS" + "$START_COMMAND_B64" + "$BUILD_COMMAND_B64" + "$RUNTIME_LANGUAGE_B64" + "$GH_ACTION" + "$PROJECT_BRANCH" + ) +fi + +QUOTED_CMD=$(printf ' %q' "${CMD[@]}") + +# Create detached tmux session to run the (possibly long) service start process +if [[ -n "${CMD[*]}" ]]; then + tmux new-session -d -s "$CONTAINER_NAME" "$QUOTED_CMD" +fi + +exit 0 \ No newline at end of file diff --git a/create-a-container/create-container-wrapper.sh b/create-a-container/create-container-wrapper.sh index 24ea26bc..1817f58d 100644 --- a/create-a-container/create-container-wrapper.sh +++ b/create-a-container/create-container-wrapper.sh @@ -36,6 +36,16 @@ CONTAINER_NAME="${CONTAINER_NAME,,}" LINUX_DISTRIBUTION="${LINUX_DISTRIBUTION,,}" DEPLOY_ON_START="${DEPLOY_ON_START,,}" +# Optional: AI_CONTAINER (default to "N" if not set) +AI_CONTAINER="${AI_CONTAINER:-N}" +AI_CONTAINER="${AI_CONTAINER^^}" # normalize + +# Validate allowed values +if [[ "$AI_CONTAINER" != "N" && "$AI_CONTAINER" != "PHOENIX" && "$AI_CONTAINER" != "FORTWAYNE" ]]; then + outputError "AI_CONTAINER must be one of: N, PHOENIX, FORTWAYNE." +fi + + # Validate Proxmox credentials using your Node.js authenticateUser USER_AUTHENTICATED=$(node /root/bin/js/runner.js authenticateUser "$PROXMOX_USERNAME" "$PROXMOX_PASSWORD") if [ "$USER_AUTHENTICATED" != "true" ]; then @@ -175,7 +185,7 @@ if [[ -n "$KEY_FILE" ]]; then fi # Run your create-container.sh remotely over SSH with corrected quoting and simplified variable -ssh -t root@10.15.0.4 "bash -c \"/var/lib/vz/snippets/create-container.sh \ +ssh -t root@10.15.0.4 "bash -c \"/var/lib/vz/snippets/create-container-new.sh \ '$CONTAINER_NAME' \ '$GH_ACTION' \ '$HTTP_PORT' \ @@ -195,6 +205,9 @@ ssh -t root@10.15.0.4 "bash -c \"/var/lib/vz/snippets/create-container.sh \ '$LINUX_DISTRIBUTION' \ '${MULTI_COMPONENT:-}' \ '${ROOT_START_COMMAND:-}' \ + '${SELF_HOSTED_RUNNER:-}' \ + '${VERSIONS_DICT:-}' \ + '$AI_CONTAINER' # Corrected: Pass the variable's value in the correct position \"" # Clean up temp files @@ -204,6 +217,7 @@ rm -f "${TEMP_SERVICES_FILE_PATH:-}" rm -rf "${ENV_FOLDER_PATH:-}" # Unset sensitive variables +unset CONFIRM_PASSWORD unset PUBLIC_KEY echo "✅ Container creation wrapper script finished successfully." \ No newline at end of file diff --git a/create-a-container/views/form.html b/create-a-container/views/form.html index 63274dc9..a8da9676 100644 --- a/create-a-container/views/form.html +++ b/create-a-container/views/form.html @@ -27,6 +27,13 @@

Create Your Container

+ + +
From 528aa75c364d32eb16cbff597504d98cca194b38 Mon Sep 17 00:00:00 2001 From: Carter Myers <206+cmyers@users.noreply.github.mieweb.com> Date: Fri, 10 Oct 2025 08:32:31 -0700 Subject: [PATCH 2/4] Add intern account request form and email notification Introduces a new /request-account.html page for interns to request accounts, with a backend endpoint that sends a notification email to DevOps upon submission. Also adds rate limiting to login, improves session security, and includes a test email endpoint for diagnostics. --- create-a-container/server.js | 334 +++++++++++------- create-a-container/views/request-account.html | 75 ++++ 2 files changed, 285 insertions(+), 124 deletions(-) create mode 100644 create-a-container/views/request-account.html diff --git a/create-a-container/server.js b/create-a-container/server.js index 5b97cc62..e5c0ac0c 100644 --- a/create-a-container/server.js +++ b/create-a-container/server.js @@ -1,162 +1,248 @@ +require('dotenv').config(); + const express = require('express'); const bodyParser = require('body-parser'); const session = require('express-session'); const { spawn, exec } = require('child_process'); const path = require('path'); const crypto = require('crypto'); -const fs = require('fs'); // Added fs module +const fs = require('fs'); +const rateLimit = require('express-rate-limit'); +const nodemailer = require('nodemailer'); // <-- added const app = express(); +app.use(express.json()); + +app.set('trust proxy', 1); -// A simple in-memory object to store job status and output const jobs = {}; // --- Middleware Setup --- -app.use(bodyParser.urlencoded({ extended: true })); -app.use(express.json()); -app.use(express.static('public')); +if (!process.env.SESSION_SECRET) { + throw new Error("SESSION_SECRET is not set in environment!"); +} + app.use(session({ - secret: 'A7d#9Lm!qW2z%Xf8@Rj3&bK6^Yp$0Nc', - resave: false, - saveUninitialized: true, - cookie: { secure: false } // Set to true if using HTTPS + secret: process.env.SESSION_SECRET, + resave: false, + saveUninitialized: true, + cookie: { secure: true } })); -// --- Route Handlers --- +app.use(express.static('public')); + +// --- Rate Limiter for Login --- +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, + message: { error: "Too many login attempts. Please try again later." } +}); + +// --- Nodemailer Setup --- +const transporter = nodemailer.createTransport({ + host: "opensource.mieweb.org", + port: 25, + secure: false, // use STARTTLS if supported + tls: { + rejectUnauthorized: false, // allow self-signed certs + }, +}); -// Serves the main container creation form, protected by login +// --- Routes --- +const PORT = 3000; +app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); + +// Serves the main container creation form app.get('/form.html', (req, res) => { - if (!req.session.user) { - return res.redirect('/'); // Redirect to login page if not authenticated - } - res.sendFile(path.join(__dirname, 'views', 'form.html')); + if (!req.session.user) { + return res.redirect('/'); + } + res.sendFile(path.join(__dirname, 'views', 'form.html')); }); -// Handles user login -app.post('/login', (req, res) => { - const { username, password } = req.body; - - exec(`node /root/bin/js/runner.js authenticateUser ${username} ${password}`, (err, stdout) => { - if (err) { - console.error("Login script execution error:", err); - return res.status(500).json({ error: "Server error during authentication." }); - } - - if (stdout.trim() === 'true') { - req.session.user = username; - req.session.proxmoxUsername = username; - req.session.proxmoxPassword = password; - return res.json({ success: true, redirect: '/form.html' }); - } else { - return res.status(401).json({ error: "Invalid credentials" }); - } - }); +// Handles login +app.post('/login', loginLimiter, (req, res) => { + const { username, password } = req.body; + const runner = spawn('node', ['/root/bin/js/runner.js', 'authenticateUser', username, password]); + let stdoutData = ''; + let stderrData = ''; + + runner.stdout.on('data', (data) => { + stdoutData += data.toString(); + }); + + runner.stderr.on('data', (data) => { + stderrData += data.toString(); + }); + + runner.on('close', (code) => { + if (code !== 0) { + console.error("Login script execution error:", stderrData); + return res.status(500).json({ error: "Server error during authentication." }); + } + + if (stdoutData.trim() === 'true') { + req.session.user = username; + req.session.proxmoxUsername = username; + req.session.proxmoxPassword = password; + return res.json({ success: true, redirect: '/form.html' }); + } else { + return res.status(401).json({ error: "Invalid credentials" }); + } + }); }); -// ✨ UPDATED: API endpoint to get user's containers +// Fetch user's containers app.get('/api/my-containers', (req, res) => { - if (!req.session.user) { - return res.status(401).json({ error: "Unauthorized" }); + if (!req.session.user) { + return res.status(401).json({ error: "Unauthorized" }); + } + const username = req.session.user.split('@')[0]; + const command = "ssh root@10.15.20.69 'cat /etc/nginx/port_map.json'"; + + exec(command, (err, stdout, stderr) => { + if (err) { + console.error("Error fetching port_map.json:", stderr); + return res.status(500).json({ error: "Could not fetch container list." }); } - // The username in port_map.json doesn't have the @pve suffix - const username = req.session.user.split('@')[0]; - - // Command to read the remote JSON file - const command = "ssh root@10.15.20.69 'cat /etc/nginx/port_map.json'"; - - exec(command, (err, stdout, stderr) => { - if (err) { - console.error("Error fetching port_map.json:", stderr); - return res.status(500).json({ error: "Could not fetch container list." }); - } - try { - const portMap = JSON.parse(stdout); - const userContainers = Object.entries(portMap) - // This check now ensures 'details' exists and has a 'user' property before comparing - .filter(([_, details]) => details && details.user === username) - .map(([name, details]) => ({ name, ...details })); - - res.json(userContainers); - } catch (parseError) { - console.error("Error parsing port_map.json:", parseError); - res.status(500).json({ error: "Could not parse container list." }); - } - }); + try { + const portMap = JSON.parse(stdout); + const userContainers = Object.entries(portMap) + .filter(([_, details]) => details && details.user === username) + .map(([name, details]) => ({ name, ...details })); + res.json(userContainers); + } catch (parseError) { + console.error("Error parsing port_map.json:", parseError); + res.status(500).json({ error: "Could not parse container list." }); + } + }); }); -// Kicks off the container creation script as a background job +// Create container app.post('/create-container', (req, res) => { - if (!req.session.user) { - return res.status(401).json({ error: "Unauthorized" }); - } - - const jobId = crypto.randomUUID(); - const commandEnv = { ...process.env, ...req.body, PROXMOX_USERNAME: req.session.proxmoxUsername, PROXMOX_PASSWORD: req.session.proxmoxPassword }; - const scriptPath = '/opt/container-creator/create-container-wrapper.sh'; - - jobs[jobId] = { status: 'running', output: '' }; + if (!req.session.user) { + return res.status(401).json({ error: "Unauthorized" }); + } + + const jobId = crypto.randomUUID(); + const commandEnv = { + ...process.env, + ...req.body, + PROXMOX_USERNAME: req.session.proxmoxUsername, + PROXMOX_PASSWORD: req.session.proxmoxPassword + }; + const scriptPath = '/opt/container-creator/create-container-wrapper.sh'; + + jobs[jobId] = { status: 'running', output: '' }; + + const command = `${scriptPath} 2>&1`; + const child = spawn('bash', ['-c', command], { env: commandEnv }); + + child.stdout.on('data', (data) => { + const message = data.toString(); + console.log(`[${jobId}]: ${message.trim()}`); + jobs[jobId].output += message; + }); + + child.on('close', (code) => { + console.log(`[${jobId}] process exited with code ${code}`); + jobs[jobId].status = (code === 0) ? 'completed' : 'failed'; + }); + + res.json({ success: true, redirect: `/status/${jobId}` }); +}); - const command = `${scriptPath} 2>&1`; - const child = spawn('bash', ['-c', command], { env: commandEnv }); +// Job status page +app.get('/status/:jobId', (req, res) => { + if (!jobs[req.params.jobId]) { + return res.status(404).send("Job not found."); + } + res.sendFile(path.join(__dirname, 'views', 'status.html')); +}); - child.stdout.on('data', (data) => { - const message = data.toString(); - console.log(`[${jobId}]: ${message.trim()}`); - jobs[jobId].output += message; - }); +// Log streaming +app.get('/api/stream/:jobId', (req, res) => { + const { jobId } = req.params; + if (!jobs[jobId]) { + return res.status(404).end(); + } + + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.flushHeaders(); + + res.write(`data: ${JSON.stringify(jobs[jobId].output)}\n\n`); + + let lastSentLength = jobs[jobId].output.length; + const intervalId = setInterval(() => { + const currentOutput = jobs[jobId].output; + if (currentOutput.length > lastSentLength) { + const newData = currentOutput.substring(lastSentLength); + res.write(`data: ${JSON.stringify(newData)}\n\n`); + lastSentLength = currentOutput.length; + } - child.on('close', (code) => { - console.log(`[${jobId}] process exited with code ${code}`); - jobs[jobId].status = (code === 0) ? 'completed' : 'failed'; - }); + if (jobs[jobId].status !== 'running') { + res.write(`event: close\ndata: Process finished with status: ${jobs[jobId].status}\n\n`); + clearInterval(intervalId); + res.end(); + } + }, 500); - res.json({ success: true, redirect: `/status/${jobId}` }); + req.on('close', () => { + clearInterval(intervalId); + res.end(); + }); }); -// Serves the status page for a specific job -app.get('/status/:jobId', (req, res) => { - if (!jobs[req.params.jobId]) { - return res.status(404).send("Job not found."); +// Serve the account request form +app.get('/request-account.html', (req, res) => { + res.sendFile(path.join(__dirname, 'views', 'request-account.html')); + }); + +app.post('/request-account', (req, res) => { + const { firstName, lastName, email, conclusionDate, reason } = req.body; + + const details = ` +New intern account request received for ${firstName} ${lastName}: + +Name: ${firstName} ${lastName} +Email: ${email} +Anticipated Intern Conclusion Date: ${conclusionDate} +Reason: ${reason} +`; + + const mailCmd = `echo "${details}" | mail -r accounts@opensource.mieweb.org -s "New Intern Account Request" devopsalerts@mieweb.com`; + + exec(mailCmd, (err, stdout, stderr) => { + if (err) { + console.error('Error sending email:', err); + console.error('stderr:', stderr); + return res.status(500).json({ error: 'Failed to send email notification to DevOps.' }); + } else { + console.log('DevOps notification sent successfully'); + console.log('stdout:', stdout); + return res.json({ success: true, message: 'Account request submitted successfully.' }); } - res.sendFile(path.join(__dirname, 'views', 'status.html')); + }); }); -// Streams the log output to the status page using Server-Sent Events (SSE) -app.get('/api/stream/:jobId', (req, res) => { - const { jobId } = req.params; - if (!jobs[jobId]) { - return res.status(404).end(); - } - res.setHeader('Content-Type', 'text/event-stream'); - res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Connection', 'keep-alive'); - res.flushHeaders(); - - res.write(`data: ${JSON.stringify(jobs[jobId].output)}\n\n`); - - let lastSentLength = jobs[jobId].output.length; - const intervalId = setInterval(() => { - const currentOutput = jobs[jobId].output; - if (currentOutput.length > lastSentLength) { - const newData = currentOutput.substring(lastSentLength); - res.write(`data: ${JSON.stringify(newData)}\n\n`); - lastSentLength = currentOutput.length; - } - - if (jobs[jobId].status !== 'running') { - res.write(`event: close\ndata: Process finished with status: ${jobs[jobId].status}\n\n`); - clearInterval(intervalId); - res.end(); - } - }, 500); - - req.on('close', () => { - clearInterval(intervalId); - res.end(); +// --- Email Test Endpoint --- +app.get('/send-test-email', async (req, res) => { + try { + const info = await transporter.sendMail({ + from: "accounts@opensource.mieweb.org", + to: "devopsalerts@mieweb.com", + subject: "Test email from opensource.mieweb.org", + text: "testing emails from opensource.mieweb.org" }); -}); -// --- Server Initialization --- -const PORT = 3000; -app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); \ No newline at end of file + console.log("Email sent:", info.response); + res.send(`✅ Email sent successfully: ${info.response}`); + } catch (err) { + console.error("Email send error:", err); + res.status(500).send(`❌ Email failed: ${err.message}`); + } +}); \ No newline at end of file diff --git a/create-a-container/views/request-account.html b/create-a-container/views/request-account.html new file mode 100644 index 00000000..2b6706db --- /dev/null +++ b/create-a-container/views/request-account.html @@ -0,0 +1,75 @@ + + + + + + Request Account + + + + + + + + + + \ No newline at end of file From 1efbb038a3d668d701550cd0a57fd677cd8b27a3 Mon Sep 17 00:00:00 2001 From: Carter Myers <206+cmyers@users.noreply.github.mieweb.com> Date: Fri, 10 Oct 2025 11:55:01 -0700 Subject: [PATCH 3/4] Add AI container host selection and signup button Enhanced create-container-new.sh to support multiple AI container hosts (Phoenix and Fort Wayne) with dynamic host selection and improved remote command handling. Updated index.html to include a 'Request an Account' button for user signup, linking to the account request page. --- container-creation/create-container-new.sh | 109 ++++++++++++++++----- create-a-container/public/index.html | 9 ++ 2 files changed, 96 insertions(+), 22 deletions(-) diff --git a/container-creation/create-container-new.sh b/container-creation/create-container-new.sh index 62ec9e63..7890ede7 100644 --- a/container-creation/create-container-new.sh +++ b/container-creation/create-container-new.sh @@ -1,6 +1,6 @@ #!/bin/bash # Script to create the pct container, run register container, and migrate container accordingly. -# Last Modified by August 27th, 2025 by Carter Myers +# Last Modified by October 3rd, 2025 by Carter Myers # ----------------------------------------------------- BOLD='\033[1m' @@ -68,13 +68,39 @@ VERSIONS_DICT=$(echo "${21}" | base64 -d) AI_CONTAINER="${22}" # new argument from HTML form echo "PROJECT ROOT: \"$PROJECT_ROOT\"" +echo "AI_CONTAINER: \"$AI_CONTAINER\"" + +# === Determine target PVE host based on AI_CONTAINER === +# PHOENIX -> 10.15.0.6 (existing AI host) +# FORTWAYNE -> 10.250.0.2 (new WireGuard-connected host) +# N -> local execution (no SSH proxy) +case "${AI_CONTAINER^^}" in + PHOENIX) + TARGET_PVE_HOST="10.15.0.6" + ;; + FORTWAYNE) + TARGET_PVE_HOST="10.250.0.2" + ;; + N|"" ) + TARGET_PVE_HOST="" + ;; + *) + echo "Invalid AI_CONTAINER value: $AI_CONTAINER" + exit 1 + ;; +esac + +# Helper: returns true if we're using a remote PVE host (PHOENIX or FORTWAYNE) +is_remote_pve() { + [[ -n "$TARGET_PVE_HOST" ]] +} -# === Wrapper for pct exec (and optionally pct commands for AI) === +# === Wrapper for pct exec (and optionally pct commands for remote PVE) === run_pct_exec() { local ctid="$1" shift - if [ "${AI_CONTAINER^^}" == "Y" ]; then - ssh root@10.15.0.6 "pct exec $ctid -- $*" + if is_remote_pve; then + ssh root@"$TARGET_PVE_HOST" "pct exec $ctid -- $*" else pct exec "$ctid" -- "$@" fi @@ -82,25 +108,25 @@ run_pct_exec() { run_pct() { # $@ = full pct command, e.g., clone, set, start, etc. - if [ "${AI_CONTAINER^^}" == "Y" ]; then - ssh root@10.15.0.6 "pct $*" + if is_remote_pve; then + ssh root@"$TARGET_PVE_HOST" "pct $*" else pct "$@" fi } run_pveum() { - # Wrapper for pveum commands in AI case - if [ "${AI_CONTAINER^^}" == "Y" ]; then - ssh root@10.15.0.6 "pveum $*" + # Wrapper for pveum commands in remote case + if is_remote_pve; then + ssh root@"$TARGET_PVE_HOST" "pveum $*" else pveum "$@" fi } run_pvesh() { - if [ "${AI_CONTAINER^^}" == "Y" ]; then - ssh root@10.15.0.6 "pvesh $*" + if is_remote_pve; then + ssh root@"$TARGET_PVE_HOST" "pvesh $*" else pvesh "$@" fi @@ -110,19 +136,21 @@ run_pct_push() { local ctid="$1" local src="$2" local dest="$3" - if [ "${AI_CONTAINER^^}" == "Y" ]; then - ssh root@10.15.0.6 "pct push $ctid $src $dest" + if is_remote_pve; then + ssh root@"$TARGET_PVE_HOST" "pct push $ctid $src $dest" else pct push "$ctid" "$src" "$dest" fi } -# === Template Selection === -if [ "${AI_CONTAINER^^}" == "Y" ]; then - echo "⏳ AI container requested. Using debian12-ai-template (CTID 150) on 10.15.0.6..." +# === Template Selection & Clone === +if [[ "${AI_CONTAINER^^}" == "PHOENIX" ]]; then + echo "⏳ Phoenix AI container requested. Using template CTID 163..." CTID_TEMPLATE="163" + # Request cluster nextid from the target (remote if configured, else local via run_pvesh) CONTAINER_ID=$(run_pvesh get /cluster/nextid) + echo "DEBUG: Cloning on TARGET_PVE_HOST=${TARGET_PVE_HOST:-local} CTID_TEMPLATE=${CTID_TEMPLATE} -> CONTAINER_ID=${CONTAINER_ID}" run_pct clone $CTID_TEMPLATE $CONTAINER_ID \ --hostname $CONTAINER_NAME \ --full true @@ -135,11 +163,33 @@ if [ "${AI_CONTAINER^^}" == "Y" ]; then run_pct start $CONTAINER_ID run_pveum aclmod /vms/$CONTAINER_ID --user "$PROXMOX_USERNAME@pve" --role PVEVMUser + +elif [[ "${AI_CONTAINER^^}" == "FORTWAYNE" ]]; then + echo "⏳ Fort Wayne AI container requested. Using template CTID 103 on 10.250.0.2..." + CTID_TEMPLATE="103" + # allocate nextid directly on Fort Wayne + CONTAINER_ID=$(ssh root@10.250.0.2 pvesh get /cluster/nextid) + + echo "DEBUG: Cloning on Fort Wayne (10.250.0.2) CTID_TEMPLATE=${CTID_TEMPLATE} -> CONTAINER_ID=${CONTAINER_ID}" + ssh root@10.250.0.2 pct clone $CTID_TEMPLATE $CONTAINER_ID \ + --hostname $CONTAINER_NAME \ + --full true + + ssh root@10.250.0.2 pct set $CONTAINER_ID \ + --tags "$PROXMOX_USERNAME" \ + --tags "$LINUX_DISTRO" \ + --tags "AI" \ + --onboot 1 + + ssh root@10.250.0.2 pct start $CONTAINER_ID + ssh root@10.250.0.2 pveum aclmod /vms/$CONTAINER_ID --user "$PROXMOX_USERNAME@pve" --role PVEVMUser + else REPO_BASE_NAME=$(basename -s .git "$PROJECT_REPOSITORY") REPO_BASE_NAME_WITH_OWNER=$(echo "$PROJECT_REPOSITORY" | cut -d'/' -f4) TEMPLATE_NAME="template-$REPO_BASE_NAME-$REPO_BASE_NAME_WITH_OWNER" + # Search local and other known PVE (keeps original approach; will find local or remote templates depending on your environment) CTID_TEMPLATE=$( { pct list; ssh root@10.15.0.5 'pct list'; } | awk -v name="$TEMPLATE_NAME" '$3 == name {print $1}') case "${LINUX_DISTRO^^}" in @@ -154,9 +204,10 @@ else esac fi + # For non-AI containers, allocate next ID locally and clone once here CONTAINER_ID=$(pvesh get /cluster/nextid) - echo "⏳ Cloning Container..." + echo "⏳ Cloning Container (non-AI)... CTID_TEMPLATE=${CTID_TEMPLATE} -> CONTAINER_ID=${CONTAINER_ID}" run_pct clone $CTID_TEMPLATE $CONTAINER_ID \ --hostname $CONTAINER_NAME \ --full true @@ -176,8 +227,16 @@ fi if [ -f "/var/lib/vz/snippets/container-public-keys/$PUB_FILE" ]; then echo "⏳ Appending Public Key..." run_pct_exec $CONTAINER_ID touch ~/.ssh/authorized_keys > /dev/null 2>&1 - run_pct_exec $CONTAINER_ID bash -c "cat > ~/.ssh/authorized_keys" < /var/lib/vz/snippets/container-public-keys/$PUB_FILE > /dev/null 2>&1 - rm -rf /var/lib/vz/snippets/container-public-keys/$PUB_FILE > /dev/null 2>&1 + # Use a here-doc to reliably feed the pubkey to the remote pct exec + if is_remote_pve; then + # copy key file to remote PVE host temporarily then pct push it in case pct exec over ssh doesn't accept stdin redirection + scp "/var/lib/vz/snippets/container-public-keys/$PUB_FILE" root@"$TARGET_PVE_HOST":/tmp/"$PUB_FILE" > /dev/null 2>&1 || true + ssh root@"$TARGET_PVE_HOST" "pct push $CONTAINER_ID /tmp/$PUB_FILE /root/.ssh/authorized_keys >/dev/null 2>&1 || (pct exec $CONTAINER_ID -- bash -lc 'cat > ~/.ssh/authorized_keys' < /tmp/$PUB_FILE)" + ssh root@"$TARGET_PVE_HOST" "rm -f /tmp/$PUB_FILE" >/dev/null 2>&1 || true + else + run_pct_exec $CONTAINER_ID bash -c "cat > ~/.ssh/authorized_keys" < /var/lib/vz/snippets/container-public-keys/$PUB_FILE > /dev/null 2>&1 + rm -rf /var/lib/vz/snippets/container-public-keys/$PUB_FILE > /dev/null 2>&1 + fi fi ROOT_PSWD=$(tr -dc 'A-Za-z0-9' Container Creation Login +
+