diff --git a/docs/vm/APP_DEPLOYER_VM.md b/docs/vm/APP_DEPLOYER_VM.md index 641a5003..703159f1 100644 --- a/docs/vm/APP_DEPLOYER_VM.md +++ b/docs/vm/APP_DEPLOYER_VM.md @@ -4,7 +4,7 @@ Deploy LXC applications inside a full Virtual Machine instead of an LXC containe ## Overview -The App Deployer VM bridges the gap between CT install scripts (`install/*.sh`) and VM infrastructure. It leverages the existing install scripts — originally designed for LXC containers — and runs them inside a full VM via a first-boot systemd service. +The App Deployer VM bridges the gap between CT install scripts (`install/*.sh`) and VM infrastructure. It leverages the existing install scripts — originally designed for LXC containers — and runs them **live during image build** via `virt-customize --run`. ### Supported Operating Systems @@ -32,7 +32,13 @@ APP_SELECT=yamtrack bash -c "$(curl -fsSL https://git.community-scripts.org/comm ### Update the application later (inside the VM) ```bash -bash /opt/community-scripts/update-app.sh +bash -c "$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/ct/.sh)" +``` + +For example, to update Yamtrack: + +```bash +bash -c "$(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/ct/yamtrack.sh)" ``` ## How It Works @@ -51,25 +57,14 @@ bash /opt/community-scripts/update-app.sh │ - Install base packages │ │ - Inject install.func │ │ - Inject tools.func │ -│ - Inject install script │ -│ - Create first-boot service │ -│ - Inject update mechanism │ +│ - Run install script LIVE │ +│ (virt-customize --run) │ +│ - Configure hostname & SSH │ │ 6. Create VM (qm create) │ │ 7. Import customized disk │ │ 8. Start VM │ -└────────────┬────────────────────────┘ - │ First Boot -┌────────────▼────────────────────────┐ -│ VM (Debian/Ubuntu) │ │ │ -│ app-install.service (oneshot): │ -│ 1. Wait for network │ -│ 2. Set environment variables │ -│ (FUNCTIONS_FILE_PATH, etc.) │ -│ 3. Run install/-install.sh │ -│ 4. Mark as installed │ -│ │ -│ → Application running in VM! │ +│ → Application pre-installed! │ └─────────────────────────────────────┘ ``` @@ -79,17 +74,17 @@ bash /opt/community-scripts/update-app.sh ┌─────────────────────────────────────┐ │ Inside the VM (SSH or console) │ │ │ -│ bash /opt/community-scripts/ │ -│ update-app.sh │ +│ bash -c "$(curl -fsSL │ +│ $COMMUNITY_SCRIPTS_URL/ │ +│ ct/.sh)" │ │ │ -│ → Downloads ct/.sh │ │ → start() detects no pveversion │ │ → Shows update/settings menu │ │ → Runs update_script() │ └─────────────────────────────────────┘ ``` -The update mechanism reuses the existing CT script logic. Since `pveversion` is not available inside the VM, the `start()` function automatically enters the update/settings mode — the same path it takes inside LXC containers. +The update mechanism reuses the existing CT script logic. Since `pveversion` is not available inside the VM, the `start()` function automatically enters the update/settings mode — exactly the same as running updates in LXC containers. ## Architecture @@ -106,13 +101,13 @@ The update mechanism reuses the existing CT script logic. Since `pveversion` is 1. **Install scripts run unmodified** — The same `install/*.sh` scripts that work in LXC containers work inside VMs. The environment (`FUNCTIONS_FILE_PATH`, exports) is replicated identically. -2. **Image customization via `virt-customize`** — All files are injected into the qcow2 image before the VM boots. No SSH or guest agent required during setup. +2. **Image customization via `virt-customize`** — All dependencies are installed and the app install script runs live inside the qcow2 image during build. No SSH or guest agent required during setup. -3. **First-boot systemd service** — The install script runs automatically on first boot. Progress can be monitored via `/var/log/app-install.log`. +3. **Live installation** — The install script runs during image build (not on first boot), so the application is ready immediately when the VM starts. -4. **Update via CT script** — The existing CT script's `start()` → `update_script()` flow works in VMs without modification. +4. **Update via CT script URL** — Run the same `bash -c "$(curl ...ct/.sh)"` command inside the VM, just like in an LXC container. -### Environment Variables (set during first-boot) +### Environment Variables (set during image build) | Variable | Description | | ----------------------- | ---------------------------------- | @@ -131,43 +126,33 @@ The update mechanism reuses the existing CT script logic. Since `pveversion` is ``` /opt/community-scripts/ ├── install.func # Function library -├── tools.func # Helper functions -├── install/ -│ └── -install.sh # Application install script -├── ct/ -│ └── (downloaded on update) # CT script for updates -└── update-app.sh # Update wrapper script +└── tools.func # Helper functions ``` ## Limitations - **Alpine-based apps**: Currently only Debian/Ubuntu VMs are supported. Alpine install scripts are not compatible. - **LXC-specific features**: Some CT features (FUSE, TUN, GPU passthrough) are configured differently in VMs. -- **First-boot timing**: The app installation happens after the VM boots, so the application is not immediately available (monitor `/var/log/app-install.log`). - **`cleanup_lxc`**: This function works fine in VMs (it only cleans package caches), but the name is LXC-centric. ## Troubleshooting -### Check installation progress +### Check build log + +If the installation fails during image build, check the log on the Proxmox host: ```bash -# From inside the VM -tail -f /var/log/app-install.log - -# From the Proxmox host (via guest agent) -qm guest exec -- cat /var/log/app-install.log +cat /tmp/vm-app-install.log ``` ### Re-run installation -```bash -# Remove the installed marker and reboot -rm /root/.app-installed -systemctl start app-install.service -``` +Re-build the VM from scratch — since the app is installed during image build, there is no in-VM reinstall mechanism. Simply delete the VM and run the deployer again. -### Check if installation completed +### Verify installation worked + +After the VM boots, SSH in and check if the application service is running: ```bash -test -f /root/.app-installed && echo "Installed" || echo "Not yet installed" +systemctl status ``` diff --git a/misc/vm-app.func b/misc/vm-app.func index e065fbef..800300d3 100644 --- a/misc/vm-app.func +++ b/misc/vm-app.func @@ -14,9 +14,8 @@ # # How it works: # 1. Creates a VM with a cloud image (Debian 12/13, Ubuntu 22.04/24.04) -# 2. Uses virt-customize to inject install scripts and dependencies -# 3. Creates a first-boot systemd service that runs the install script -# 4. For updates: the CT script can be run directly inside the VM +# 2. Uses virt-customize to inject dependencies and run the install script LIVE +# 3. For updates: run the CT script URL directly inside the VM # # Key Functions: # select_app() - Interactive app selection from install/*.sh @@ -346,13 +345,11 @@ ensure_virt_customize() { # Main image customization function. Uses virt-customize to: # 1. Install base packages (qemu-guest-agent, curl, sudo, etc.) # 2. Inject install.func and tools.func as files -# 3. Inject the application install script -# 4. Create a first-boot systemd service -# 5. Inject the update mechanism -# 6. Configure hostname, SSH, auto-login +# 3. Run the application install script LIVE inside the image +# 4. Configure hostname, SSH, auto-login # # Parameters: None (uses global variables) -# Sets: WORK_FILE (path to the customized image), APP_PREINSTALLED flag +# Sets: WORK_FILE (path to the customized image) # ------------------------------------------------------------------------------ customize_vm_image() { local app="${APP_INSTALL_SCRIPT}" @@ -362,85 +359,41 @@ customize_vm_image() { export LIBGUESTFS_BACKEND_SETTINGS=dns=8.8.8.8,1.1.1.1 - APP_PREINSTALLED="no" - # --- Step 1: Install base packages --- msg_info "Installing base packages in image" if virt-customize -a "$WORK_FILE" --install qemu-guest-agent,curl,sudo,ca-certificates,gnupg2,jq,mc >/dev/null 2>&1; then msg_ok "Installed base packages" else - msg_warn "Base packages will be installed on first boot" + msg_error "Failed to install base packages in image" + exit 1 fi - # --- Step 2: Create directory structure --- - virt-customize -q -a "$WORK_FILE" \ - --mkdir /opt/community-scripts \ - --mkdir /opt/community-scripts/install \ - --mkdir /opt/community-scripts/ct >/dev/null 2>&1 - - # --- Step 3: Inject function libraries --- + # --- Step 2: Inject function libraries --- msg_info "Injecting function libraries into image" - # Download install.func and tools.func content - local install_func_content tools_func_content - install_func_content=$(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/install.func") - tools_func_content=$(curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/tools.func") - - # Write function libraries to temp files for injection local tmp_install_func tmp_tools_func tmp_install_func=$(mktemp) tmp_tools_func=$(mktemp) - echo "$install_func_content" >"$tmp_install_func" - echo "$tools_func_content" >"$tmp_tools_func" + curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/install.func" >"$tmp_install_func" + curl -fsSL "$COMMUNITY_SCRIPTS_URL/misc/tools.func" >"$tmp_tools_func" virt-customize -q -a "$WORK_FILE" \ + --mkdir /opt/community-scripts \ --upload "$tmp_install_func:/opt/community-scripts/install.func" \ --upload "$tmp_tools_func:/opt/community-scripts/tools.func" >/dev/null 2>&1 rm -f "$tmp_install_func" "$tmp_tools_func" msg_ok "Injected function libraries" - # --- Step 4: Inject application install script --- - msg_info "Injecting ${APP_NAME} install script" + # --- Step 3: Create and run install wrapper LIVE --- + msg_info "Installing ${APP_NAME} in image (this may take several minutes)" - local install_script_content - install_script_content=$(curl -fsSL "$COMMUNITY_SCRIPTS_URL/install/${app}-install.sh") - - local tmp_install_script - tmp_install_script=$(mktemp) - echo "$install_script_content" >"$tmp_install_script" - - virt-customize -q -a "$WORK_FILE" \ - --upload "$tmp_install_script:/opt/community-scripts/install/${app}-install.sh" \ - --chmod 0755:/opt/community-scripts/install/${app}-install.sh >/dev/null 2>&1 - - rm -f "$tmp_install_script" - msg_ok "Injected ${APP_NAME} install script" - - # --- Step 5: Create first-boot wrapper script --- - msg_info "Creating first-boot installation service" - - local wrapper_script - wrapper_script=$(mktemp) - cat >"$wrapper_script" <"$tmp_wrapper" < /var/log/app-install.log 2>&1 -echo "[\$(date)] Starting ${APP_NAME} installation" - -# Wait for network connectivity -echo "[\$(date)] Waiting for network..." -for i in {1..60}; do - if ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1 || ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1; then - echo "[\$(date)] Network is up" - break - fi - sleep 2 -done - # Set environment variables (same as build_container provides) export FUNCTIONS_FILE_PATH="\$(cat /opt/community-scripts/install.func)" export APPLICATION="${APP_NAME}" @@ -453,81 +406,22 @@ export PCT_OSVERSION="${OS_VERSION}" export COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL}" export DEPLOY_TARGET="vm" -# Run the install script -echo "[\$(date)] Running install script..." -bash /opt/community-scripts/install/${app}-install.sh - -# Mark installation as complete -touch /root/.app-installed -echo "[\$(date)] ${APP_NAME} installation completed successfully" +# Download and run the install script +curl -fsSL "${COMMUNITY_SCRIPTS_URL}/install/${app}-install.sh" | bash WRAPPER_EOF - virt-customize -q -a "$WORK_FILE" \ - --upload "$wrapper_script:/root/app-install-wrapper.sh" \ - --chmod 0755:/root/app-install-wrapper.sh >/dev/null 2>&1 + # virt-customize --run executes the script with network inside the image + if virt-customize -a "$WORK_FILE" --run "$tmp_wrapper" 2>&1 | tee /tmp/vm-app-install.log; then + msg_ok "Installed ${APP_NAME}" + else + msg_error "${APP_NAME} installation failed! Check /tmp/vm-app-install.log" + rm -f "$tmp_wrapper" + exit 1 + fi - rm -f "$wrapper_script" + rm -f "$tmp_wrapper" - # --- Step 6: Create systemd first-boot service --- - local service_file - service_file=$(mktemp) - cat >"$service_file" <<'SERVICE_EOF' -[Unit] -Description=Install Application on First Boot (ProxmoxVED App Deployer) -After=network-online.target -Wants=network-online.target -ConditionPathExists=!/root/.app-installed - -[Service] -Type=oneshot -ExecStart=/root/app-install-wrapper.sh -RemainAfterExit=yes -TimeoutStartSec=1800 -StandardOutput=journal+console -StandardError=journal+console - -[Install] -WantedBy=multi-user.target -SERVICE_EOF - - virt-customize -q -a "$WORK_FILE" \ - --upload "$service_file:/etc/systemd/system/app-install.service" >/dev/null 2>&1 - virt-customize -q -a "$WORK_FILE" \ - --run-command 'systemctl enable app-install.service' >/dev/null 2>&1 - - rm -f "$service_file" - msg_ok "Created first-boot installation service" - - # --- Step 7: Inject update mechanism --- - msg_info "Injecting update mechanism" - - local update_wrapper - update_wrapper=$(mktemp) - cat >"$update_wrapper" </dev/null 2>&1 - - rm -f "$update_wrapper" - msg_ok "Injected update mechanism" - - # --- Step 8: Configure hostname and SSH --- + # --- Step 4: Configure hostname and SSH --- msg_info "Finalizing image (hostname, SSH config)" virt-customize -q -a "$WORK_FILE" --hostname "${HN}" >/dev/null 2>&1 @@ -555,7 +449,7 @@ EOF' >/dev/null 2>&1 || true msg_ok "Finalized image" - # --- Step 9: Resize disk --- + # --- Step 5: Resize disk --- msg_info "Resizing disk image to ${DISK_SIZE}" qemu-img resize "$WORK_FILE" "${DISK_SIZE}" >/dev/null 2>&1 msg_ok "Resized disk image" @@ -668,23 +562,20 @@ wait_for_vm_ip() { # Displays the final summary with VM details and access information. # ------------------------------------------------------------------------------ show_app_vm_summary() { + local app="${APP_INSTALL_SCRIPT}" echo "" echo -e "${INFO}${BOLD}${GN}${APP_NAME} VM Configuration Summary:${CL}" echo -e "${TAB}${DGN}VM ID: ${BGN}${VMID}${CL}" echo -e "${TAB}${DGN}Hostname: ${BGN}${HN}${CL}" echo -e "${TAB}${DGN}OS: ${BGN}${OS_DISPLAY}${CL}" - echo -e "${TAB}${DGN}Application: ${BGN}${APP_NAME}${CL}" + echo -e "${TAB}${DGN}Application: ${BGN}${APP_NAME} (pre-installed)${CL}" echo -e "${TAB}${DGN}CPU Cores: ${BGN}${CORE_COUNT}${CL}" echo -e "${TAB}${DGN}RAM: ${BGN}${RAM_SIZE} MiB${CL}" echo -e "${TAB}${DGN}Disk: ${BGN}${DISK_SIZE}${CL}" [ -n "${VM_IP:-}" ] && echo -e "${TAB}${DGN}IP Address: ${BGN}${VM_IP}${CL}" echo "" - echo -e "${TAB}${DGN}${APP_NAME}: ${BGN}Installing on first boot${CL}" - echo -e "${TAB}${YW} The application will be installed automatically after the VM boots.${CL}" - echo -e "${TAB}${YW} This may take several minutes depending on the application.${CL}" - echo -e "${TAB}${YW} Monitor progress: ${BL}cat /var/log/app-install.log${CL}" - echo "" - echo -e "${TAB}${DGN}Update later: ${BGN}bash /opt/community-scripts/update-app.sh${CL}" + echo -e "${TAB}${DGN}Update ${APP_NAME} inside the VM:${CL}" + echo -e "${TAB}${BGN} bash -c \"\$(curl -fsSL ${COMMUNITY_SCRIPTS_URL}/ct/${app}.sh)\"${CL}" echo "" if [ "$USE_CLOUD_INIT" = "yes" ] && declare -f display_cloud_init_info >/dev/null 2>&1; then