diff --git a/container-creation/create-container.sh b/container-creation/create-container.sh index 583be02a..6a844782 100644 --- a/container-creation/create-container.sh +++ b/container-creation/create-container.sh @@ -1,106 +1,213 @@ #!/bin/bash # Script to create the pct container, run register container, and migrate container accordingly. -# Last Modified by June 30th, 2025 by Maxwell Klema +# Last Modified by August 5th, 2025 by Maxwell Klema +# ----------------------------------------------------- + +BOLD='\033[1m' +BLUE='\033[34m' +MAGENTA='\033[35m' +GREEN='\033[32m' +RESET='\033[0m' + +# Run cleanup commands in case script is interrupted + +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 +} + +# Echo Container Details +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="${CONTAINER_NAME,,}" + CONTAINER_NAME="$1" -CONTAINER_PASSWORD="$2" +GH_ACTION="$2" HTTP_PORT="$3" PROXMOX_USERNAME="$4" +USERNAME_ONLY="${PROXMOX_USERNAME%@*}" PUB_FILE="$5" PROTOCOL_FILE="$6" -NEXT_ID=$(pvesh get /cluster/nextid) #Get the next available LXC ID - -# Run cleanup commands in case script is interrupted -function cleanup() -{ - BOLD='\033[1m' - RESET='\033[0m' - - echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" - echo "⚠️ Script was abruptly exited. Running cleanup tasks." - echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" - pct unlock 114 - if [ -f "/var/lib/vz/snippets/container-public-keys/$PUB_FILE" ]; then - rm -rf /var/lib/vz/snippets/container-public-keys/$PUB_FILE - fi - if [ -f "/var/lib/vz/snippets/container-port-maps/$PROTOCOL_FILE" ]; then - rm -rf /var/lib/vz/snippets/container-port-maps/$PROTOCOL_FILE - fi - exit 1 -} +# 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}" + +# Pick the correct template to clone ===== + +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" +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 no template ID was provided, assign a default based on distro + +if [ -z "$CTID_TEMPLATE" ]; then + case "${LINUX_DISTRO^^}" in + DEBIAN) CTID_TEMPLATE="160" ;; + ROCKY) CTID_TEMPLATE="138" ;; + esac +fi +# Create the Container Clone ==== -# Create the Container Clone +if [ "${GH_ACTION^^}" != "Y" ]; then + CONTAINER_ID=$(pvesh get /cluster/nextid) #Get the next available LXC ID -echo "⏳ Cloning Container..." -pct clone 114 $NEXT_ID \ - --hostname $CONTAINER_NAME \ - --full true \ + echo "⏳ Cloning Container..." + pct clone $CTID_TEMPLATE $CONTAINER_ID \ + --hostname $CONTAINER_NAME \ + --full true > /dev/null 2>&1 -# Set Container Options + # Set Container Options -echo "⏳ Setting Container Properties.." -pct set $NEXT_ID \ - --tags "$PROXMOX_USERNAME" \ - --onboot 1 \ + echo "⏳ Setting Container Properties..." + pct set $CONTAINER_ID \ + --tags "$PROXMOX_USERNAME" \ + --tags "$LINUX_DISTRO" \ + --tags "LDAP" \ + --onboot 1 > /dev/null 2>&1 -pct start $NEXT_ID -pveum aclmod /vms/$NEXT_ID --user "$PROXMOX_USERNAME@pve" --role PVEVMUser -#pct delete $NEXT_ID + pct start $CONTAINER_ID > /dev/null 2>&1 + pveum aclmod /vms/$CONTAINER_ID --user "$PROXMOX_USERNAME@pve" --role PVEVMUser > /dev/null 2>&1 -# Get the Container IP Address and install some packages + # Get the Container IP Address and install some packages -echo "⏳ Waiting for DHCP to allocate IP address to container..." -sleep 10 + echo "⏳ Waiting for DHCP to allocate IP address to container..." + sleep 5 +else + CONTAINER_ID=$( { pct list; ssh root@10.15.0.5 'pct list'; } | awk -v name="$CONTAINER_NAME" '$3 == name {print $1}') +fi -CONTAINER_IP=$(pct exec $NEXT_ID -- hostname -I | awk '{print $1}') -pct exec $NEXT_ID -- apt-get upgrade -pct exec $NEXT_ID -- apt install -y sudo -pct exec $NEXT_ID -- apt install -y git if [ -f "/var/lib/vz/snippets/container-public-keys/$PUB_FILE" ]; then - pct exec $NEXT_ID -- touch ~/.ssh/authorized_keys - pct exec $NEXT_ID -- bash -c "cat > ~/.ssh/authorized_keys"< /var/lib/vz/snippets/container-public-keys/$PUB_FILE - rm -rf /var/lib/vz/snippets/container-public-keys/$PUB_FILE + echo "⏳ Appending Public Key..." + pct exec $CONTAINER_ID -- touch ~/.ssh/authorized_keys > /dev/null 2>&1 + 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 -# Set password inside the container +CONTAINER_IP="" +attempts=0 +max_attempts=10 -pct exec $NEXT_ID -- bash -c "echo 'root:$CONTAINER_PASSWORD' | chpasswd" +while [[ -z "$CONTAINER_IP" && $attempts -lt $max_attempts ]]; do + CONTAINER_IP=$(pct exec "$CONTAINER_ID" -- hostname -I | awk '{print $1}') + [[ -z "$CONTAINER_IP" ]] && sleep 2 && ((attempts++)) +done -# Run Contianer Provision Script to add container to port_map.json +if [[ -z "$CONTAINER_IP" ]]; then + echo "❌ Timed out waiting for container to get an IP address." + exit 1 +fi -if [ -f "/var/lib/vz/snippets/container-port-maps/$PROTOCOL_FILE" ]; then - echo "CONTAINS PROTOCOL FILE" - /var/lib/vz/snippets/register-container-test.sh $NEXT_ID $HTTP_PORT /var/lib/vz/snippets/container-port-maps/$PROTOCOL_FILE - rm -rf /var/lib/vz/snippets/container-port-maps/$PROTOCOL_FILE -else - /var/lib/vz/snippets/register-container-test.sh $NEXT_ID $HTTP_PORT +# Set up SSSD to communicate with LDAP server ==== +echo "⏳ Configuring LDAP connection via SSSD..." +source /var/lib/vz/snippets/helper-scripts/configureLDAP.sh + +# Attempt to Automatically Deploy Project Inside Container + +if [ "${DEPLOY_ON_START^^}" == "Y" ]; then + source /var/lib/vz/snippets/helper-scripts/deployOnStart.sh + + #cleanup + 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 -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) +# Create Log File ==== -# Migrate to pve2 if Container ID is even +pct exec $CONTAINER_ID -- bash -c "cd /root && touch container-updates.log" -if (( $NEXT_ID % 2 == 0 )); then - pct stop $NEXT_ID - pct migrate $NEXT_ID intern-phxdc-pve2 --target-storage containers-pve2 --online - ssh root@10.15.0.5 "pct start $NEXT_ID" +# Run Contianer Provision Script to add container to port_map.json +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 -# Echo Container Details -# Define friendly, high-contrast colors -BOLD='\033[1m' -BLUE='\033[34m' -MAGENTA='\033[35m' -GREEN='\033[32m' -RESET='\033[0m' +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 -e "📦 ${BLUE}Container ID :${RESET} $NEXT_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 root@$CONTAINER_NAME.opensource.mieweb.org" -echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" \ No newline at end of file +# Output container details and start services if necessary ===== + +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) + +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" +) + +# Safely quote each argument for the shell +QUOTED_CMD=$(printf ' %q' "${CMD[@]}") + +tmux new-session -d -s "$CONTAINER_NAME" "$QUOTED_CMD" +exit 0 \ No newline at end of file diff --git a/create-a-container/container-creator.service b/create-a-container/container-creator.service new file mode 100644 index 00000000..ece707d6 --- /dev/null +++ b/create-a-container/container-creator.service @@ -0,0 +1,14 @@ +[Unit] +Description=Container Creator Node.js App +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/container-creator +ExecStart=/usr/bin/node /opt/container-creator/server.js +Restart=on-failure +Environment=NODE_ENV=production + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/create-a-container/create-container-wrapper.sh b/create-a-container/create-container-wrapper.sh new file mode 100644 index 00000000..24ea26bc --- /dev/null +++ b/create-a-container/create-container-wrapper.sh @@ -0,0 +1,209 @@ +#!/bin/bash +# Wrapper for non-interactive container creation +# Reads all inputs from environment variables and validates them +# Exits with error messages if invalid/missing + +set -euo pipefail + +GH_ACTION="${GH_ACTION:-}" + +RESET="\033[0m" +BOLD="\033[1m" +MAGENTA='\033[35m' + +outputError() { + echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + echo -e "${BOLD}${MAGENTA}❌ Script Failed. Exiting... ${RESET}" + echo -e "$1" + echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + exit 1 +} + +echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" +echo -e "${BOLD}${MAGENTA}📦 MIE Container Creation Script (Wrapper)${RESET}" +echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + +# Required variables, fail if not set or empty +: "${PROXMOX_USERNAME:?Environment variable PROXMOX_USERNAME is required}" +: "${PROXMOX_PASSWORD:?Environment variable PROXMOX_PASSWORD is required}" +: "${CONTAINER_NAME:?Environment variable CONTAINER_NAME is required}" +: "${LINUX_DISTRIBUTION:?Environment variable LINUX_DISTRIBUTION is required}" +: "${HTTP_PORT:?Environment variable HTTP_PORT is required}" +: "${DEPLOY_ON_START:=n}" # default to "n" if not set + +# Convert container name and linux distribution to lowercase +CONTAINER_NAME="${CONTAINER_NAME,,}" +LINUX_DISTRIBUTION="${LINUX_DISTRIBUTION,,}" +DEPLOY_ON_START="${DEPLOY_ON_START,,}" + +# 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 + outputError "Invalid Proxmox Credentials." +fi + +echo "🎉 Proxmox user '$PROXMOX_USERNAME' authenticated." + +# Validate container name: alphanumeric + dash only +if ! [[ "$CONTAINER_NAME" =~ ^[a-z0-9-]+$ ]]; then + outputError "Invalid container name: Only lowercase letters, numbers, and dashes are allowed." +fi + +# Check if hostname already exists remotely +HOST_NAME_EXISTS=$(ssh root@10.15.20.69 "node /etc/nginx/checkHostnameRunner.js checkHostnameExists ${CONTAINER_NAME}") +if [ "$HOST_NAME_EXISTS" == "true" ]; then + outputError "Container hostname '$CONTAINER_NAME' already exists." +fi +echo "✅ Container name '$CONTAINER_NAME' is available." + +# Validate Linux distribution choice +if [[ "$LINUX_DISTRIBUTION" != "debian" && "$LINUX_DISTRIBUTION" != "rocky" ]]; then + outputError "Linux distribution must be 'debian' or 'rocky'." +fi + +# Validate HTTP_PORT: integer between 80 and 60000 +if ! [[ "$HTTP_PORT" =~ ^[0-9]+$ ]] || [ "$HTTP_PORT" -lt 80 ] || [ "$HTTP_PORT" -gt 60000 ]; then + outputError "HTTP_PORT must be a number between 80 and 60000." +fi + +echo "✅ HTTP port set to $HTTP_PORT." + +# Public key optional +if [ -n "${PUBLIC_KEY-}" ]; then + # Validate public key format (simple check) + if echo "$PUBLIC_KEY" | ssh-keygen -l -f - &>/dev/null; then + AUTHORIZED_KEYS="/root/.ssh/authorized_keys" + echo "$PUBLIC_KEY" > "$AUTHORIZED_KEYS" + systemctl restart ssh + echo "$PUBLIC_KEY" > "/root/bin/ssh/temp_pubs/key_$(shuf -i 100000-999999 -n1).pub" + sudo /root/bin/ssh/publicKeyAppendJumpHost.sh "$PUBLIC_KEY" + echo "🔐 Public key added." + else + outputError "Invalid PUBLIC_KEY format." + fi +else + echo "ℹ️ No public key provided." +fi + +# Protocol list handling (optional) +PROTOCOL_BASE_FILE="protocol_list_$(shuf -i 100000-999999 -n 1).txt" +PROTOCOL_FILE="/root/bin/protocols/$PROTOCOL_BASE_FILE" +touch "$PROTOCOL_FILE" + +# --- Logic for named protocols from a list (existing) --- +if [[ "${USE_OTHER_PROTOCOLS-}" == "y" || "${USE_OTHER_PROTOCOLS-}" == "Y" ]]; then + if [ -z "${OTHER_PROTOCOLS_LIST-}" ]; then + outputError "USE_OTHER_PROTOCOLS is yes but OTHER_PROTOCOLS_LIST is empty." + fi + IFS=',' read -ra PROTOCOLS <<< "$OTHER_PROTOCOLS_LIST" + for PROTOCOL_NAME in "${PROTOCOLS[@]}"; do + PROTOCOL_NAME=$(echo "$PROTOCOL_NAME" | tr '[:lower:]' '[:upper:]') + FOUND=0 + while read -r line; do + PROTOCOL_ABBRV=$(echo "$line" | awk '{print $1}') + if [[ "$PROTOCOL_ABBRV" == "$PROTOCOL_NAME" ]]; then + echo "$line" >> "$PROTOCOL_FILE" + echo " ^|^e Protocol $PROTOCOL_NAME added." + FOUND=1 + break + fi + done < "/root/bin/protocols/master_protocol_list.txt" + if [ "$FOUND" -eq 0 ]; then + echo " ^}^l Protocol $PROTOCOL_NAME not found, skipping." + fi + done +fi + +# --- START: Added logic for single custom port --- +# Check if the OTHER_PORT variable is set and not empty +if [ -n "${OTHER_PORT-}" ]; then + # Validate that it's an integer + if [[ "$OTHER_PORT" =~ ^[0-9]+$ ]]; then + echo "TCP $OTHER_PORT" >> "$PROTOCOL_FILE" + echo "UDP $OTHER_PORT" >> "$PROTOCOL_FILE" + echo " ^|^e Custom port $OTHER_PORT (TCP/UDP) added." + else + echo " ^}^l Invalid custom port specified: $OTHER_PORT. Must be an integer. Skipping." + fi +fi + +# Deploy on start must be y or n +if [[ "$DEPLOY_ON_START" != "y" && "$DEPLOY_ON_START" != "n" ]]; then + outputError "DEPLOY_ON_START must be 'y' or 'n'." +fi + +if [ "$DEPLOY_ON_START" == "y" ]; then + source /root/bin/deploy-application.sh +fi + +# Send files to hypervisor (public keys, protocols, env vars, services) +send_file_to_hypervisor() { + local LOCAL_FILE="$1" + local REMOTE_FOLDER="$2" + if [ "$REMOTE_FOLDER" != "container-env-vars" ]; then + if [ -s "$LOCAL_FILE" ]; then + sftp root@10.15.0.4 < /dev/null +put $LOCAL_FILE /var/lib/vz/snippets/$REMOTE_FOLDER/ +EOF + fi + else + if [ -d "$LOCAL_FILE" ]; then + sftp root@10.15.0.4 < /dev/null +put -r $LOCAL_FILE /var/lib/vz/snippets/$REMOTE_FOLDER/ +EOF + fi + fi +} + +# Example paths, set or export these in environment if used +send_file_to_hypervisor "/root/bin/ssh/temp_pubs/key_*.pub" "container-public-keys" +send_file_to_hypervisor "$PROTOCOL_FILE" "container-port-maps" +send_file_to_hypervisor "${ENV_FOLDER_PATH:-}" "container-env-vars" +send_file_to_hypervisor "${TEMP_SERVICES_FILE_PATH:-}" "container-services" + +echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" +echo -e "${BOLD}${MAGENTA}🚀 Starting Container Creation...${RESET}" +echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + +# Safely get the basename of the temporary public key file. +KEY_BASENAME="" +# The 'find' command is safer than 'ls' for script usage. +KEY_FILE=$(find /root/bin/ssh/temp_pubs -type f -name "*.pub" | head -n1) + +if [[ -n "$KEY_FILE" ]]; then + KEY_BASENAME=$(basename "$KEY_FILE") +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 \ + '$CONTAINER_NAME' \ + '$GH_ACTION' \ + '$HTTP_PORT' \ + '$PROXMOX_USERNAME' \ + '$KEY_BASENAME' \ + '$PROTOCOL_BASE_FILE' \ + '$DEPLOY_ON_START' \ + '${PROJECT_REPOSITORY:-}' \ + '${PROJECT_BRANCH:-}' \ + '${PROJECT_ROOT:-}' \ + '${INSTALL_COMMAND:-}' \ + '${BUILD_COMMAND:-}' \ + '${START_COMMAND:-}' \ + '${RUNTIME_LANGUAGE:-}' \ + '${ENV_FOLDER:-}' \ + '${SERVICES_FILE:-}' \ + '$LINUX_DISTRIBUTION' \ + '${MULTI_COMPONENT:-}' \ + '${ROOT_START_COMMAND:-}' \ +\"" + +# Clean up temp files +rm -f "$PROTOCOL_FILE" +rm -f /root/bin/ssh/temp_pubs/key_*.pub +rm -f "${TEMP_SERVICES_FILE_PATH:-}" +rm -rf "${ENV_FOLDER_PATH:-}" + +# Unset sensitive variables +unset PUBLIC_KEY + +echo "✅ Container creation wrapper script finished successfully." \ No newline at end of file diff --git a/create-a-container/public/index.html b/create-a-container/public/index.html new file mode 100644 index 00000000..63d37ff1 --- /dev/null +++ b/create-a-container/public/index.html @@ -0,0 +1,66 @@ + + + + + MIE Container Creation Login + + + + + + + + \ No newline at end of file diff --git a/create-a-container/public/logo.png b/create-a-container/public/logo.png new file mode 100644 index 00000000..3429aeaa Binary files /dev/null and b/create-a-container/public/logo.png differ diff --git a/create-a-container/public/style.css b/create-a-container/public/style.css new file mode 100644 index 00000000..61739d12 --- /dev/null +++ b/create-a-container/public/style.css @@ -0,0 +1,164 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(to right, #2c3e50, #3498db); + display: flex; + align-items: flex-start; /* Changed from center to flex-start for top alignment */ + justify-content: center; + height: 100vh; + margin: 0; + padding-top: 2rem; /* Added padding to give space from the top */ + overflow-y: auto; /* Allow scrolling if content is too long */ +} + +.main-content-wrapper { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 2rem; + width: 100%; + max-width: 900px; /* Increased max-width for two columns */ + padding: 0 1rem; +} + +.login-container { + background-color: white; + padding: 2rem 3rem; + border-radius: 12px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); + max-width: 400px; /* New: limit width */ + width: 100%; /* Still responsive, but maxed out */ + text-align: center; +} + +/* Wider wrapper for output view */ +.output-container { + background-color: white; + padding: 2rem 3rem; + border-radius: 12px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); + max-width: 800px; /* Wider than login-container */ + width: 100%; + text-align: left; /* Align output text to the right */ + font-family: monospace; /* Keep terminal feel */ + white-space: pre-wrap; /* Preserve formatting */ + overflow-y: auto; /* Scroll if log is long */ + max-height: 80vh; /* Prevent giant box from pushing content off screen */ +} + +.output-container .status-message { + text-align: left; /* Status aligns with log text */ +} + +.container-list-box { + background-color: white; + padding: 2rem; + border-radius: 12px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); + width: 100%; + flex: 1; + text-align: left; +} + +.container-list-box h3 { + text-align: center; + margin-top: 0; + color: #2c3e50; + border-bottom: 2px solid #f0f0f0; + padding-bottom: 1rem; +} + +.container-list-box ul { + list-style-type: none; + padding: 0; + margin: 0; +} + +.container-list-box li { + background-color: #f9f9f9; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 1rem; + margin-bottom: 1rem; +} + +.container-list-box li h4 { + margin: 0 0 0.5rem 0; + color: #3498db; +} + +.container-list-box li p { + margin: 0.25rem 0; + font-size: 0.95em; +} + +.container-list-box li ul { + margin-top: 0.5rem; + padding-left: 1rem; + font-size: 0.9em; +} + +.container-list-box li ul li { + padding: 0; + margin-bottom: 0.2rem; + border: none; + background: none; +} + + +.logo { + max-width: 180px; + margin: 0 auto 1.5rem auto; + display: block; +} + +h2 { + margin-bottom: 1.5rem; + color: #2c3e50; +} + +label { + display: block; + margin-bottom: 0.5rem; + font-weight: 600; + text-align: left; +} + +input[type="text"], +input[type="password"], +input[type="number"], +textarea, +select { + width: 100%; + padding: 0.75rem; + margin-bottom: 1.2rem; + border: 1px solid #ccc; + border-radius: 6px; + font-size: 16px; + box-sizing: border-box; +} + +input[type="submit"] { + width: 100%; + padding: 0.8rem; + background-color: #3498db; + border: none; + border-radius: 6px; + color: white; + font-weight: bold; + cursor: pointer; +} + +input[type="submit"]:hover { + background-color: #2980b9; +} + +.footer { + margin-top: 1.2rem; + font-size: 0.9em; + color: #777; +} + +select:focus { + border-color: #007bff; + outline: none; +} \ No newline at end of file diff --git a/create-a-container/server.js b/create-a-container/server.js new file mode 100644 index 00000000..5b97cc62 --- /dev/null +++ b/create-a-container/server.js @@ -0,0 +1,162 @@ +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 app = express(); + +// 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')); +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 +})); + +// --- Route Handlers --- + +// Serves the main container creation form, protected by login +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')); +}); + +// 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" }); + } + }); +}); + +// ✨ UPDATED: API endpoint to get user's containers +app.get('/api/my-containers', (req, res) => { + if (!req.session.user) { + return res.status(401).json({ error: "Unauthorized" }); + } + // 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." }); + } + }); +}); + +// Kicks off the container creation script as a background job +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: '' }; + + 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}` }); +}); + +// 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."); + } + 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(); + }); +}); + +// --- Server Initialization --- +const PORT = 3000; +app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`)); \ No newline at end of file diff --git a/create-a-container/views/form.html b/create-a-container/views/form.html new file mode 100644 index 00000000..63274dc9 --- /dev/null +++ b/create-a-container/views/form.html @@ -0,0 +1,121 @@ + + + + + MIE Container Creation + + + + +
+ + +
+

Your Active Containers

+
    +
+
+
+ + + + + \ No newline at end of file diff --git a/create-a-container/views/logo.png b/create-a-container/views/logo.png new file mode 100644 index 00000000..3429aeaa Binary files /dev/null and b/create-a-container/views/logo.png differ diff --git a/create-a-container/views/status.html b/create-a-container/views/status.html new file mode 100644 index 00000000..db3c5262 --- /dev/null +++ b/create-a-container/views/status.html @@ -0,0 +1,57 @@ + + + + + Container Creation Status + + + + + + +
+ +

Container Creation Log

+

+        
Status: Running...
+
+ + + + \ No newline at end of file