mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-10-08 08:26:25 +00:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dabe45606 | ||
|
|
3b768aaeb9 | ||
|
|
008e2d90be | ||
|
|
fa8b74a00b | ||
|
|
1dfe79f968 | ||
|
|
111a281b3e | ||
|
|
b755ec70d5 | ||
|
|
beff70719f | ||
|
|
cec9f13da1 | ||
|
|
ea59019670 | ||
|
|
13bfd8aa15 | ||
|
|
175bbe9da7 |
@@ -559,6 +559,28 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
</details>
|
||||
|
||||
## 2026-10-07
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- Fedora VM ([#17747](https://github.com/community-scripts/ProxmoxVE/pull/17747))
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- paperclip: run as a dedicated non-root user so Claude Code can skip permissions [@samvandenbossche](https://github.com/samvandenbossche) ([#17707](https://github.com/community-scripts/ProxmoxVE/pull/17707))
|
||||
- Thingsboard: update Java version from 17 to 25 [@CerberusStyle](https://github.com/CerberusStyle) ([#17733](https://github.com/community-scripts/ProxmoxVE/pull/17733))
|
||||
|
||||
- #### 💥 Breaking Changes
|
||||
|
||||
- OpenCloud: bump to v8.1.0, rebuild search index on upgrade [@SimKaiLong](https://github.com/SimKaiLong) ([#17710](https://github.com/community-scripts/ProxmoxVE/pull/17710))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- Fix Fedora and BLS cloud image console setup [@MickLesk](https://github.com/MickLesk) ([core#116](https://github.com/community-scripts/core/pull/116))
|
||||
- Keep setup_nodejs alive when the caller's directory is gone [@MickLesk](https://github.com/MickLesk) ([core#114](https://github.com/community-scripts/core/pull/114))
|
||||
|
||||
## 2026-10-06
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
+30
-1
@@ -31,8 +31,9 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
RELEASE="v7.4.0"
|
||||
RELEASE="v8.1.0"
|
||||
if check_for_gh_release "OpenCloud" "opencloud-eu/opencloud" "${RELEASE}" "each release is tested individually before the version is updated. Please do not open issues for this"; then
|
||||
OLD_VERSION="$(cat ~/.opencloud 2>/dev/null)"
|
||||
msg_info "Stopping services"
|
||||
systemctl stop opencloud opencloud-wopi
|
||||
msg_ok "Stopped services"
|
||||
@@ -70,6 +71,34 @@ function update_script() {
|
||||
msg_info "Starting services"
|
||||
systemctl start opencloud opencloud-wopi
|
||||
msg_ok "Started services"
|
||||
|
||||
if [[ -z "$OLD_VERSION" || "${OLD_VERSION#v}" =~ ^[0-7]\. ]]; then
|
||||
# The index CLI cancels the rebuild when interrupted, so it runs as its own unit and the
|
||||
# update only waits for it. Restart covers a search service that is still starting.
|
||||
msg_info "Rebuilding search index (safe to interrupt, it continues in the background)"
|
||||
REINDEX_START="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
systemctl reset-failed opencloud-reindex &>/dev/null || true
|
||||
$STD systemd-run --unit=opencloud-reindex --uid=opencloud --gid=opencloud \
|
||||
-p EnvironmentFile=/etc/opencloud/opencloud.env -p Restart=on-failure -p RestartSec=15 \
|
||||
-p StartLimitIntervalSec=900 -p StartLimitBurst=20 \
|
||||
/usr/bin/opencloud search index --all-spaces --force-rescan --insecure
|
||||
while [[ "$(systemctl show -p ActiveState --value opencloud-reindex)" =~ ^(active|activating)$ ]]; do
|
||||
sleep 10
|
||||
done
|
||||
if [[ "$(systemctl show -p ActiveState --value opencloud-reindex)" == "failed" ]]; then
|
||||
msg_ok "Stopped waiting for the search index rebuild"
|
||||
msg_warn "The rebuild did not complete, see: journalctl -u opencloud-reindex"
|
||||
msg_warn "Retry with: systemctl reset-failed opencloud-reindex; systemd-run --unit=opencloud-reindex \
|
||||
--uid=opencloud --gid=opencloud -p EnvironmentFile=/etc/opencloud/opencloud.env \
|
||||
/usr/bin/opencloud search index --all-spaces --force-rescan --insecure"
|
||||
else
|
||||
msg_ok "Rebuilt search index"
|
||||
if journalctl -u opencloud-reindex --since "$REINDEX_START" --no-pager | grep -qE '/[0-9]+ ERROR'; then
|
||||
msg_warn "Some spaces could not be indexed, see: journalctl -u opencloud-reindex"
|
||||
fi
|
||||
msg_warn "Once search finds older files, remove the old index: rm -rf /var/lib/opencloud/search/bleve"
|
||||
fi
|
||||
fi
|
||||
msg_ok "Updated successfully"
|
||||
fi
|
||||
exit
|
||||
|
||||
+29
-1
@@ -15,6 +15,8 @@ var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_arm64="${var_arm64:-yes}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
export var_paperclip_user="${var_paperclip_user:-}"
|
||||
export var_paperclip_pass="${var_paperclip_pass:-}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
@@ -59,9 +61,35 @@ function update_script() {
|
||||
@openai/codex@latest
|
||||
msg_ok "Updated Agent CLIs"
|
||||
|
||||
# Claude Code refuses --dangerously-skip-permissions as root; migrate existing installs to a dedicated user
|
||||
PAPERCLIP_USER=$(sed -n 's/^User=//p' /etc/systemd/system/paperclip.service)
|
||||
if [[ -z "$PAPERCLIP_USER" || "$PAPERCLIP_USER" == "root" ]]; then
|
||||
PAPERCLIP_USER="${var_paperclip_user:-paperclip}"
|
||||
if [[ "$PAPERCLIP_USER" == "root" || ! "$PAPERCLIP_USER" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]]; then
|
||||
msg_error "Invalid var_paperclip_user '${PAPERCLIP_USER}' (must be a non-root lowercase Linux username)"
|
||||
exit 1
|
||||
fi
|
||||
PAPERCLIP_USER_HOME="/home/${PAPERCLIP_USER}"
|
||||
id -u "$PAPERCLIP_USER" &>/dev/null || useradd -m -d "$PAPERCLIP_USER_HOME" -s /bin/bash "$PAPERCLIP_USER"
|
||||
passwd -S "$PAPERCLIP_USER" 2>/dev/null | grep -q " P " || passwd -l "$PAPERCLIP_USER" &>/dev/null
|
||||
mkdir -p "${PAPERCLIP_USER_HOME}/.claude" "${PAPERCLIP_USER_HOME}/.codex"
|
||||
[[ -d /root/.claude ]] && cp -a /root/.claude/. "${PAPERCLIP_USER_HOME}/.claude/"
|
||||
[[ -d /root/.codex ]] && cp -a /root/.codex/. "${PAPERCLIP_USER_HOME}/.codex/"
|
||||
sed -i \
|
||||
-e "s|^User=.*|User=${PAPERCLIP_USER}\nGroup=${PAPERCLIP_USER}|" \
|
||||
-e "s|^Environment=HOME=.*|Environment=HOME=${PAPERCLIP_USER_HOME}|" \
|
||||
-e "s|^Environment=CODEX_HOME=.*|Environment=CODEX_HOME=${PAPERCLIP_USER_HOME}/.codex|" \
|
||||
-e "s|/root/.local/bin|${PAPERCLIP_USER_HOME}/.local/bin|" \
|
||||
/etc/systemd/system/paperclip.service
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
PAPERCLIP_USER_HOME=$(getent passwd "$PAPERCLIP_USER" | cut -d: -f6)
|
||||
chmod 600 /opt/paperclip-ai/.env
|
||||
chown -R "${PAPERCLIP_USER}:${PAPERCLIP_USER}" /opt/paperclip-ai /opt/paperclip-data "$PAPERCLIP_USER_HOME"
|
||||
|
||||
msg_info "Running Database Migrations"
|
||||
set -a && source /opt/paperclip-ai/.env && set +a
|
||||
$STD pnpm db:migrate
|
||||
$STD runuser -u "$PAPERCLIP_USER" -- env HOME="$PAPERCLIP_USER_HOME" bash -c 'cd /opt/paperclip-ai && pnpm db:migrate'
|
||||
msg_ok "Ran Database Migrations"
|
||||
|
||||
msg_info "Starting Service"
|
||||
|
||||
@@ -30,6 +30,7 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
JAVA_VERSION="25" setup_java
|
||||
if check_for_gh_release "thingsboard" "thingsboard/thingsboard"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop thingsboard
|
||||
|
||||
@@ -64,7 +64,7 @@ $STD sudo -u cool coolconfig set-admin-password --user=admin --password="$COOLPA
|
||||
echo "$COOLPASS" >~/.coolpass
|
||||
msg_ok "Installed Collabora Online"
|
||||
|
||||
fetch_and_deploy_gh_release "OpenCloud" "opencloud-eu/opencloud" "singlefile" "v7.4.0" "/usr/bin" "opencloud-*-linux-$(arch_resolve)"
|
||||
fetch_and_deploy_gh_release "OpenCloud" "opencloud-eu/opencloud" "singlefile" "v8.1.0" "/usr/bin" "opencloud-*-linux-$(arch_resolve)"
|
||||
mv /usr/bin/OpenCloud /usr/bin/opencloud
|
||||
|
||||
msg_info "Configuring OpenCloud"
|
||||
|
||||
@@ -46,8 +46,21 @@ msg_info "Configuring Paperclip"
|
||||
PAPERCLIP_HOME="/opt/paperclip-data"
|
||||
PAPERCLIP_CONFIG="${PAPERCLIP_HOME}/instances/default/config.json"
|
||||
|
||||
mkdir -p /opt/paperclip-data
|
||||
mkdir -p /root/.claude /root/.codex
|
||||
PAPERCLIP_USER="${var_paperclip_user:-paperclip}"
|
||||
# Claude Code refuses --dangerously-skip-permissions as root, so run as a dedicated user
|
||||
if [[ "$PAPERCLIP_USER" == "root" || ! "$PAPERCLIP_USER" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]]; then
|
||||
msg_error "Invalid var_paperclip_user '${PAPERCLIP_USER}' (must be a non-root lowercase Linux username)"
|
||||
exit 1
|
||||
fi
|
||||
PAPERCLIP_USER_HOME="/home/${PAPERCLIP_USER}"
|
||||
id -u "$PAPERCLIP_USER" &>/dev/null || useradd -m -d "$PAPERCLIP_USER_HOME" -s /bin/bash "$PAPERCLIP_USER"
|
||||
if [[ -n "${var_paperclip_pass:-}" ]]; then
|
||||
printf '%s:%s\n' "$PAPERCLIP_USER" "$var_paperclip_pass" | chpasswd
|
||||
else
|
||||
passwd -l "$PAPERCLIP_USER" &>/dev/null
|
||||
fi
|
||||
unset var_paperclip_pass
|
||||
mkdir -p /opt/paperclip-data "${PAPERCLIP_USER_HOME}/.claude" "${PAPERCLIP_USER_HOME}/.codex"
|
||||
BETTER_AUTH_SECRET=$(openssl rand -hex 32)
|
||||
cat <<EOF >/opt/paperclip-ai/.env
|
||||
DATABASE_URL=postgresql://${PG_DB_USER}:${PG_DB_PASS}@127.0.0.1:5432/${PG_DB_NAME}
|
||||
@@ -62,11 +75,13 @@ PAPERCLIP_DEPLOYMENT_EXPOSURE=private
|
||||
PAPERCLIP_PUBLIC_URL=http://${LOCAL_IP}:3100
|
||||
BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
|
||||
EOF
|
||||
chmod 600 /opt/paperclip-ai/.env
|
||||
chown -R "${PAPERCLIP_USER}:${PAPERCLIP_USER}" /opt/paperclip-ai /opt/paperclip-data "$PAPERCLIP_USER_HOME"
|
||||
msg_ok "Configured Paperclip"
|
||||
|
||||
msg_info "Running Database Migrations"
|
||||
set -a && source /opt/paperclip-ai/.env && set +a
|
||||
$STD pnpm db:migrate
|
||||
$STD runuser -u "$PAPERCLIP_USER" -- env HOME="$PAPERCLIP_USER_HOME" pnpm db:migrate
|
||||
msg_ok "Ran Database Migrations"
|
||||
|
||||
msg_info "Bootstrapping Paperclip"
|
||||
@@ -77,7 +92,8 @@ for PAPERCLIP_ONBOARD_CMD in \
|
||||
"pnpm paperclipai onboard --yes --bind lan" \
|
||||
"pnpm paperclipai onboard --yes"; do
|
||||
rm -f "$PAPERCLIP_ONBOARD_LOG"
|
||||
setsid env \
|
||||
setsid runuser -u "$PAPERCLIP_USER" -- env \
|
||||
HOME="$PAPERCLIP_USER_HOME" \
|
||||
PAPERCLIP_HOME="$PAPERCLIP_HOME" \
|
||||
PAPERCLIP_CONFIG="$PAPERCLIP_CONFIG" \
|
||||
bash -c 'cd /opt/paperclip-ai && exec "$@"' _ $PAPERCLIP_ONBOARD_CMD \
|
||||
@@ -109,7 +125,8 @@ if [[ ! -f "$PAPERCLIP_CONFIG" ]]; then
|
||||
fi
|
||||
|
||||
if grep -q 'authenticated' $PAPERCLIP_CONFIG; then
|
||||
pnpm paperclipai auth bootstrap-ceo >"$PAPERCLIP_BOOTSTRAP_LOG" 2>&1 || true
|
||||
chown "${PAPERCLIP_USER}:${PAPERCLIP_USER}" /opt/paperclip-ai
|
||||
runuser -u "$PAPERCLIP_USER" -- env HOME="$PAPERCLIP_USER_HOME" bash -c 'cd /opt/paperclip-ai && pnpm paperclipai auth bootstrap-ceo' >"$PAPERCLIP_BOOTSTRAP_LOG" 2>&1 || true
|
||||
PAPERCLIP_INVITE_URL=$(awk -F'Invite URL: ' '/Invite URL:/ {print $2; exit}' "$PAPERCLIP_BOOTSTRAP_LOG")
|
||||
PAPERCLIP_INVITE_EXPIRY=$(awk -F'Expires: ' '/Expires:/ {print $2; exit}' "$PAPERCLIP_BOOTSTRAP_LOG")
|
||||
if [[ -n "$PAPERCLIP_INVITE_URL" ]]; then
|
||||
@@ -131,6 +148,7 @@ else
|
||||
fi
|
||||
rm -f "$PAPERCLIP_ONBOARD_LOG" "$PAPERCLIP_BOOTSTRAP_LOG"
|
||||
msg_ok "Bootstrapped Paperclip"
|
||||
echo -e "${INFO}${YW} Authenticate Claude Code and Codex as the service user: ${BGN}su - ${PAPERCLIP_USER}${CL}"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/paperclip.service
|
||||
@@ -141,12 +159,13 @@ Requires=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
User=${PAPERCLIP_USER}
|
||||
Group=${PAPERCLIP_USER}
|
||||
WorkingDirectory=/opt/paperclip-ai
|
||||
EnvironmentFile=/opt/paperclip-ai/.env
|
||||
Environment=HOME=/root
|
||||
Environment=CODEX_HOME=/root/.codex
|
||||
Environment=PATH=/root/.local/bin:/usr/local/bin:/usr/bin:/bin
|
||||
Environment=HOME=${PAPERCLIP_USER_HOME}
|
||||
Environment=CODEX_HOME=${PAPERCLIP_USER_HOME}/.codex
|
||||
Environment=PATH=${PAPERCLIP_USER_HOME}/.local/bin:/usr/local/bin:/usr/bin:/bin
|
||||
Environment=DISABLE_AUTOUPDATER=1
|
||||
ExecStart=/usr/bin/env pnpm paperclipai run
|
||||
Restart=on-failure
|
||||
|
||||
@@ -20,7 +20,7 @@ $STD apt install -y \
|
||||
fonts-dejavu-core
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
JAVA_VERSION="17" setup_java
|
||||
JAVA_VERSION="25" setup_java
|
||||
PG_VERSION="16" setup_postgresql
|
||||
PG_DB_NAME="thingsboard_db" PG_DB_USER="thingsboard" setup_postgresql_db
|
||||
fetch_and_deploy_gh_release "thingsboard" "thingsboard/thingsboard" "binary" "latest" "/tmp" "thingsboard-*.deb"
|
||||
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://fedoraproject.org/cloud/
|
||||
|
||||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main}"
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/pve/vm-core.func")
|
||||
load_functions
|
||||
|
||||
APP="Fedora"
|
||||
APP_TYPE="vm"
|
||||
NSAPP="fedora-vm"
|
||||
var_os="fedora"
|
||||
var_version=" "
|
||||
GEN_MAC=02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:/g; s/.$//')
|
||||
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)"
|
||||
METHOD=""
|
||||
THIN="discard=on,ssd=1,"
|
||||
|
||||
header_info
|
||||
echo -e "\n Loading..."
|
||||
|
||||
set -e
|
||||
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||
trap cleanup EXIT
|
||||
trap 'post_update_to_api "failed" "130"' SIGINT
|
||||
trap 'post_update_to_api "failed" "143"' SIGTERM
|
||||
trap 'post_update_to_api "failed" "129"; exit 129' SIGHUP
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
pushd "$TEMP_DIR" >/dev/null
|
||||
|
||||
vm_preflight
|
||||
|
||||
# Fedora Cloud Base sets no password and has no console login, so the only
|
||||
# question is which credentials.
|
||||
CLOUDINIT_REQUIRED=1
|
||||
vm_prompt_cloud_init "fedora"
|
||||
|
||||
function default_settings() {
|
||||
VMID=$(get_valid_nextid)
|
||||
vm_apply_machine_type "q35"
|
||||
DISK_SIZE="20G"
|
||||
DISK_CACHE=""
|
||||
HN="fedora"
|
||||
CPU_TYPE=" -cpu host"
|
||||
CORE_COUNT="2"
|
||||
RAM_SIZE="2048"
|
||||
BRG="vmbr0"
|
||||
MAC="$GEN_MAC"
|
||||
VLAN=""
|
||||
MTU=""
|
||||
START_VM="yes"
|
||||
METHOD="default"
|
||||
echo -e "${CLOUD}${BOLD}${DGN}Cloud-Init: ${BGN}${USE_CLOUD_INIT}${CL}"
|
||||
vm_echo_default_settings
|
||||
}
|
||||
|
||||
function advanced_settings() {
|
||||
METHOD="advanced"
|
||||
vm_prompt_vmid "${VMID:-$(get_valid_nextid)}"
|
||||
vm_prompt_machine_type "q35"
|
||||
vm_prompt_disk_size "20G" "Set Disk Size in GiB"
|
||||
vm_prompt_disk_cache "none"
|
||||
vm_prompt_hostname "fedora"
|
||||
vm_prompt_cpu_model "host"
|
||||
vm_prompt_cpu_cores "2"
|
||||
vm_prompt_ram "2048"
|
||||
vm_prompt_bridge "vmbr0"
|
||||
vm_prompt_mac "$GEN_MAC"
|
||||
vm_prompt_vlan
|
||||
vm_prompt_mtu
|
||||
vm_prompt_verbose "no"
|
||||
vm_prompt_start_vm "yes"
|
||||
|
||||
if vm_confirm_advanced_settings "Ready to create a Fedora VM?"; then
|
||||
echo -e "${CREATING}${BOLD}${DGN}Creating a Fedora VM using the above advanced settings${CL}"
|
||||
else
|
||||
header_info
|
||||
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
|
||||
advanced_settings
|
||||
fi
|
||||
}
|
||||
|
||||
vm_start_script "Use Default Settings?\n\nDefaults:\n• 2 CPU Cores\n• 2 GB RAM\n• 20 GB Disk\n• Cloud-Init enabled" 14 58
|
||||
post_to_api_vm
|
||||
|
||||
vm_select_storage "$HN"
|
||||
|
||||
if ! command -v virt-customize &>/dev/null; then
|
||||
msg_info "Installing libguestfs-tools"
|
||||
$STD apt-get update
|
||||
$STD apt-get install -y libguestfs-tools
|
||||
msg_ok "Installed libguestfs-tools"
|
||||
fi
|
||||
|
||||
msg_info "Retrieving the URL for the Fedora Cloud Base image"
|
||||
MIRROR="https://dl.fedoraproject.org/pub/fedora/linux/releases"
|
||||
FEDORA_RELEASE=$(curl -fsSL "$MIRROR/" 2>/dev/null | grep -oP 'href="\K[0-9]+(?=/")' | sort -rn | head -1)
|
||||
[[ -z "$FEDORA_RELEASE" ]] && FEDORA_RELEASE="44"
|
||||
|
||||
FEDORA_ARCH="$(vm_arch_resolve x86_64 aarch64)"
|
||||
IMAGE_DIR="${MIRROR}/${FEDORA_RELEASE}/Cloud/${FEDORA_ARCH}/images"
|
||||
FILE=$(curl -fsSL "${IMAGE_DIR}/" 2>/dev/null | grep -oP 'href="\KFedora-Cloud-Base-Generic-[^"]+\.'"${FEDORA_ARCH}"'\.qcow2(?=")' | sort -V | tail -1)
|
||||
if [[ -z "$FILE" ]]; then
|
||||
msg_error "Could not determine the current Fedora Cloud image"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
msg_ok "Fedora ${CL}${BL}${FEDORA_RELEASE}${CL} ${GN}(${FILE})"
|
||||
|
||||
URL="${IMAGE_DIR}/${FILE}"
|
||||
CACHE_FILE="$(vm_image_cache_path "$URL")"
|
||||
vm_fetch_image "$URL" "$CACHE_FILE" --cache --min-bytes $((100 * 1024 * 1024)) || exit 115
|
||||
|
||||
WORK_FILE=$(mktemp --suffix=.qcow2)
|
||||
cp "$CACHE_FILE" "$WORK_FILE"
|
||||
popd >/dev/null
|
||||
rm -rf "$TEMP_DIR"
|
||||
vm_prepare_cloud_image "$WORK_FILE" "$HN" || true
|
||||
|
||||
msg_info "Customizing ${FILE}"
|
||||
CUSTOMIZE_FAILURES_BEFORE=${#_VM_PREPARE_FAILED[@]}
|
||||
_vm_customize "Fedora serial console" "$WORK_FILE" --run-command "systemctl enable serial-getty@ttyS0.service"
|
||||
_vm_customize "Fedora SELinux relabel" "$WORK_FILE" --selinux-relabel
|
||||
if ((${#_VM_PREPARE_FAILED[@]} == CUSTOMIZE_FAILURES_BEFORE)); then
|
||||
msg_ok "Customized image"
|
||||
else
|
||||
msg_warn "Fedora image customization incomplete; see warnings above"
|
||||
fi
|
||||
|
||||
STORAGE_TYPE=$(pvesm status -storage "$STORAGE" | awk 'NR>1 {print $2}')
|
||||
vm_apply_storage_layout "$STORAGE_TYPE"
|
||||
vm_define_disk_references 3
|
||||
|
||||
if [[ "$DISK_IMPORT_FORMAT" == "raw" ]]; then
|
||||
msg_info "Converting image to raw format"
|
||||
RAW_FILE=$(mktemp --suffix=.raw)
|
||||
qemu-img convert -f qcow2 -O raw "$WORK_FILE" "$RAW_FILE"
|
||||
rm -f "$WORK_FILE"
|
||||
WORK_FILE="$RAW_FILE"
|
||||
msg_ok "Converted image to raw format"
|
||||
fi
|
||||
|
||||
msg_info "Creating a Fedora VM"
|
||||
qm create "$VMID" -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf${CPU_TYPE} -cores "$CORE_COUNT" -memory "$RAM_SIZE" \
|
||||
-name "$HN" -tags community-script -net0 virtio,bridge="$BRG",macaddr="$MAC""$VLAN""$MTU" -onboot 1 -ostype l26 -scsihw virtio-scsi-pci
|
||||
vm_alloc_efi_disk "$DISK0"
|
||||
pvesm alloc "$STORAGE" "$VMID" "$DISK2" 4M 1>&/dev/null
|
||||
qm importdisk "$VMID" "$WORK_FILE" "$STORAGE" -format "$DISK_IMPORT_FORMAT" 1>&/dev/null
|
||||
qm set "$VMID" \
|
||||
-efidisk0 "${DISK0_REF}${FORMAT:-}" \
|
||||
-scsi0 "${DISK1_REF}",${DISK_CACHE}${THIN}size="${DISK_SIZE}" \
|
||||
-tpmstate0 "${DISK2_REF}",version=v2.0 \
|
||||
-boot order=scsi0 \
|
||||
-serial0 socket >/dev/null
|
||||
set_description
|
||||
rm -f "$WORK_FILE"
|
||||
msg_ok "Created a Fedora VM ${CL}${BL}(${HN})"
|
||||
|
||||
vm_resize_disk
|
||||
|
||||
vm_provision "$VMID" || true
|
||||
|
||||
if [ "$START_VM" == "yes" ]; then
|
||||
msg_info "Starting Fedora VM"
|
||||
$STD qm start "$VMID"
|
||||
msg_ok "Started Fedora VM"
|
||||
fi
|
||||
|
||||
post_update_to_api "done" "none"
|
||||
|
||||
echo -e "\n${INFO}${BOLD}${GN}Fedora 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}Release: ${BGN}Fedora ${FEDORA_RELEASE} (${FEDORA_ARCH})${CL}"
|
||||
if [ -n "${CLOUDINIT_CRED_FILE:-}" ]; then
|
||||
echo -e "${TAB}${DGN}Cloud-Init credentials: ${BGN}${CLOUDINIT_CRED_FILE}${CL}"
|
||||
fi
|
||||
|
||||
msg_ok "Completed successfully!\n"
|
||||
+3
-3
@@ -149,8 +149,8 @@ fi
|
||||
|
||||
FULL_URL="https://download.truenas.com/${SELECTED_ISO#/}"
|
||||
ISO_NAME=$(basename "$FULL_URL")
|
||||
CACHE_DIR="/var/lib/vz/template/iso"
|
||||
CACHE_FILE="$CACHE_DIR/$ISO_NAME"
|
||||
vm_select_iso_storage "$ISO_NAME" "$HN"
|
||||
CACHE_FILE="$ISO_PATH"
|
||||
|
||||
msg_info "Retrieving the ISO for the TrueNAS Disk Image"
|
||||
MIN_ISO_BYTES=$((500 * 1024 * 1024))
|
||||
@@ -162,7 +162,7 @@ qm create "$VMID"${MACHINE} -bios ovmf -agent enabled=1 -tablet 0 -localtime 1${
|
||||
-cores "$CORE_COUNT" -memory "$RAM_SIZE" -balloon 0 -name "$HN" -tags community-script \
|
||||
-net0 "virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU" -onboot 1 -ostype l26 \
|
||||
-efidisk0 $STORAGE:1,efitype=4m,pre-enrolled-keys=0 -sata0 ${STORAGE}:${DISK_SIZE%G},ssd=1 \
|
||||
-scsihw virtio-scsi-single -cdrom local:iso/$ISO_NAME -boot order='sata0;ide2' -vga virtio >/dev/null
|
||||
-scsihw virtio-scsi-single -cdrom "$ISO_VOLUME" -boot order='sata0;ide2' -vga virtio >/dev/null
|
||||
msg_ok "Created VM shell"
|
||||
|
||||
if [ "$IMPORT_DISKS" == "yes" ]; then
|
||||
|
||||
+3
-4
@@ -96,9 +96,8 @@ URL="https://download.umbrel.com/release/${UMBREL_RELEASE}/umbrelos-amd64-usb-in
|
||||
# The upstream file name is the same for every release, so the version goes into
|
||||
# the cached name -- otherwise the cache serves 1.7.4 to someone asking for 2.0.
|
||||
ISO_NAME="umbrelos-${UMBREL_RELEASE}-amd64-usb-installer.iso"
|
||||
CACHE_DIR="/var/lib/vz/template/iso"
|
||||
CACHE_FILE="${CACHE_DIR}/${ISO_NAME}"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
vm_select_iso_storage "$ISO_NAME" "$HN"
|
||||
CACHE_FILE="$ISO_PATH"
|
||||
msg_ok "${CL}${BL}${URL}${CL}"
|
||||
|
||||
# download.umbrel.com answers 307 for any name at all, so a redirect proves
|
||||
@@ -114,7 +113,7 @@ qm create "$VMID"${MACHINE} -bios ovmf -agent enabled=1 -tablet 0 -localtime 1 $
|
||||
-net0 "virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU" -onboot 1 -ostype l26 -scsihw virtio-scsi-pci \
|
||||
-efidisk0 "${STORAGE}:1,efitype=4m,pre-enrolled-keys=0" \
|
||||
-scsi0 "${STORAGE}:${DISK_SIZE%G},${DISK_CACHE:-}${THIN%,}" \
|
||||
-cdrom "local:iso/${ISO_NAME}" -boot order='scsi0;ide2' >/dev/null
|
||||
-cdrom "$ISO_VOLUME" -boot order='scsi0;ide2' >/dev/null
|
||||
|
||||
set_description
|
||||
msg_ok "Created a Umbrel OS VM ${CL}${BL}(${HN})"
|
||||
|
||||
@@ -0,0 +1,495 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
|
||||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main}"
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_CORE_URL:-https://raw.githubusercontent.com/community-scripts/core/main}/pve/vm-core.func")
|
||||
load_functions
|
||||
|
||||
GEN_MAC=02:$(openssl rand -hex 5 | awk '{print toupper($0)}' | sed 's/\(..\)/\1:/g; s/.$//')
|
||||
RANDOM_UUID="$(cat /proc/sys/kernel/random/uuid)"
|
||||
METHOD=""
|
||||
APP="UniFi OS Server"
|
||||
APP_TYPE="vm"
|
||||
NSAPP="unifi-os-server-vm"
|
||||
var_os="-"
|
||||
var_version="-"
|
||||
CLOUDINIT_REQUIRED=1
|
||||
OS_TYPE=""
|
||||
OS_VERSION=""
|
||||
OS_CODENAME=""
|
||||
OS_DISPLAY=""
|
||||
|
||||
HA=$(echo "\033[1;34m")
|
||||
|
||||
THIN="discard=on,ssd=1,"
|
||||
|
||||
header_info
|
||||
echo -e "\n Loading..."
|
||||
|
||||
set -Eeuo pipefail
|
||||
trap 'error_handler $LINENO "$BASH_COMMAND"' ERR
|
||||
trap cleanup EXIT
|
||||
trap 'post_update_to_api "failed" "INTERRUPTED"' SIGINT
|
||||
trap 'post_update_to_api "failed" "TERMINATED"' SIGTERM
|
||||
|
||||
vm_require_arch amd64
|
||||
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
pushd $TEMP_DIR >/dev/null
|
||||
|
||||
function select_os() {
|
||||
if [[ -n "${1:-}" ]]; then
|
||||
OS_CHOICE="$1"
|
||||
elif [[ "${VM_UNATTENDED:-0}" == "1" ]]; then
|
||||
OS_CHOICE="${VM_OS_VERSION:-debian13}"
|
||||
elif ! OS_CHOICE=$(whiptail --backtitle "Proxmox VE Helper Scripts" --title "SELECT OS" --radiolist \
|
||||
"Choose Operating System for UniFi OS VM" 12 68 2 \
|
||||
"debian13" "Debian 13 (Trixie) - Latest" ON \
|
||||
"ubuntu2404" "Ubuntu 24.04 LTS (Noble)" OFF \
|
||||
3>&1 1>&2 2>&3); then
|
||||
exit_script
|
||||
fi
|
||||
|
||||
case $OS_CHOICE in
|
||||
debian13)
|
||||
OS_TYPE="debian"
|
||||
OS_VERSION="13"
|
||||
OS_CODENAME="trixie"
|
||||
OS_DISPLAY="Debian 13 (Trixie)"
|
||||
;;
|
||||
ubuntu2404)
|
||||
OS_TYPE="ubuntu"
|
||||
OS_VERSION="24.04"
|
||||
OS_CODENAME="noble"
|
||||
OS_DISPLAY="Ubuntu 24.04 LTS"
|
||||
;;
|
||||
*)
|
||||
msg_error "Unsupported OS '${OS_CHOICE}' (expected debian13 or ubuntu2404)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function get_image_url() {
|
||||
local arch
|
||||
arch=$(dpkg --print-architecture)
|
||||
case $OS_TYPE in
|
||||
debian)
|
||||
# Always use Cloud-Init variant for UniFi OS
|
||||
echo "https://cloud.debian.org/images/cloud/${OS_CODENAME}/latest/debian-${OS_VERSION}-generic-${arch}.qcow2"
|
||||
;;
|
||||
ubuntu)
|
||||
# Ubuntu only has cloudimg variant (always with Cloud-Init support)
|
||||
echo "https://cloud-images.ubuntu.com/${OS_CODENAME}/current/${OS_CODENAME}-server-cloudimg-${arch}.img"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function default_settings() {
|
||||
vm_apply_machine_type "q35"
|
||||
select_os "${VM_OS_VERSION:-debian13}"
|
||||
|
||||
# Set defaults for other settings
|
||||
VMID=$(get_valid_nextid)
|
||||
DISK_CACHE=""
|
||||
DISK_SIZE="32G"
|
||||
HN="unifi-server-os"
|
||||
CPU_TYPE=" -cpu host"
|
||||
CORE_COUNT="2"
|
||||
RAM_SIZE="6144"
|
||||
BRG="vmbr0"
|
||||
MAC="$GEN_MAC"
|
||||
VLAN=""
|
||||
MTU=""
|
||||
START_VM="yes"
|
||||
METHOD="default"
|
||||
vm_echo_default_settings
|
||||
}
|
||||
|
||||
function advanced_settings() {
|
||||
METHOD="advanced"
|
||||
select_os
|
||||
vm_prompt_vmid "${VMID:-$(get_valid_nextid)}"
|
||||
vm_prompt_machine_type "q35"
|
||||
vm_prompt_disk_size "32G"
|
||||
vm_prompt_disk_cache "none"
|
||||
vm_prompt_hostname "unifi-server-os"
|
||||
vm_prompt_cpu_model "host"
|
||||
vm_prompt_cpu_cores "2"
|
||||
vm_prompt_ram "6144"
|
||||
vm_prompt_bridge "vmbr0"
|
||||
vm_prompt_mac "$GEN_MAC"
|
||||
vm_prompt_vlan
|
||||
vm_prompt_mtu
|
||||
vm_prompt_verbose "no"
|
||||
vm_prompt_start_vm "yes"
|
||||
|
||||
if vm_confirm_advanced_settings "Ready to create a UniFi OS Server VM?"; then
|
||||
echo -e "${CREATING}${BOLD}${DGN}Creating a UniFi OS Server VM using the above advanced settings${CL}"
|
||||
else
|
||||
header_info
|
||||
echo -e "${ADVANCED}${BOLD}${RD}Using Advanced Settings${CL}"
|
||||
advanced_settings
|
||||
fi
|
||||
}
|
||||
|
||||
vm_preflight
|
||||
|
||||
if [[ "${VM_UNATTENDED:-0}" == "1" ]]; then
|
||||
CLOUDINIT_PASSWORD="${CLOUDINIT_PASSWORD:-${VM_ROOT_PASSWORD:-}}"
|
||||
fi
|
||||
vm_prompt_cloud_init "root"
|
||||
if [[ "${USE_CLOUD_INIT:-no}" != "yes" ]]; then
|
||||
msg_error "UniFi OS Server requires Cloud-Init"
|
||||
exit 1
|
||||
fi
|
||||
if ! declare -f setup_cloud_init >/dev/null; then
|
||||
msg_error "Required Cloud-Init helpers are unavailable"
|
||||
exit 1
|
||||
fi
|
||||
if [[ "${VM_UNATTENDED:-0}" == "1" && -n "${VM_SSH_KEYS:-}" && -z "${CLOUDINIT_SSH_KEYS:-}" ]]; then
|
||||
if [[ -f "$VM_SSH_KEYS" ]]; then
|
||||
cp "$VM_SSH_KEYS" "$TEMP_DIR/ssh-keys-input"
|
||||
else
|
||||
printf '%s\n' "$VM_SSH_KEYS" >"$TEMP_DIR/ssh-keys-input"
|
||||
fi
|
||||
CLOUDINIT_SSH_KEYS="$TEMP_DIR/ssh-keys.pub"
|
||||
_ci_ssh_extract_keys_from_file "$TEMP_DIR/ssh-keys-input" >"$CLOUDINIT_SSH_KEYS"
|
||||
if [[ ! -s "$CLOUDINIT_SSH_KEYS" ]] || ! ssh-keygen -lf "$CLOUDINIT_SSH_KEYS" >/dev/null; then
|
||||
msg_error "VM_SSH_KEYS must contain valid SSH public keys or name a public-key file"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
vm_start_script "Use Default Settings?\n\nDefaults:\n• 2 CPU Cores\n• 6 GB RAM\n• 32 GB Disk\n• Cloud-Init enabled" 14 58
|
||||
post_to_api_vm
|
||||
|
||||
msg_info "Checking system resources"
|
||||
SYSTEM_RAM_GB=$(grep MemTotal /proc/meminfo | awk '{printf "%.0f", $2 / 1024 / 1024}')
|
||||
SYSTEM_SWAP_GB=$(grep SwapTotal /proc/meminfo | awk '{printf "%.0f", $2 / 1024 / 1024}')
|
||||
SYSTEM_FREE_DISK_GB=$(df -BG / | awk 'NR==2 {print $4}' | sed 's/G//')
|
||||
if [[ ${SYSTEM_RAM_GB} -lt 4 ]]; then
|
||||
msg_error "Warning: Less than 4GB RAM detected (${SYSTEM_RAM_GB}GB). Install may be slow."
|
||||
sleep 3
|
||||
fi
|
||||
if [[ ${SYSTEM_FREE_DISK_GB} -lt 10 ]]; then
|
||||
msg_error "Warning: Less than 10GB free disk detected. Install may fail."
|
||||
sleep 3
|
||||
fi
|
||||
msg_ok "System resources: ${SYSTEM_RAM_GB}GB RAM, ${SYSTEM_FREE_DISK_GB}GB free disk"
|
||||
|
||||
if command -v ufw &>/dev/null; then
|
||||
if ufw status verbose | grep -q "Status: active"; then
|
||||
msg_info "Setting up firewall rules for UniFi OS Server ports"
|
||||
ufw allow 11443/tcp 2>/dev/null
|
||||
ufw allow 8080/tcp 2>/dev/null
|
||||
ufw allow 3478/tcp 2>/dev/null
|
||||
ufw allow 3478/udp 2>/dev/null
|
||||
msg_ok "Firewall rules configured"
|
||||
fi
|
||||
fi
|
||||
|
||||
vm_select_storage "$HN"
|
||||
|
||||
# Fetch latest UniFi OS Server version and download URL
|
||||
msg_info "Fetching latest UniFi OS Server version"
|
||||
|
||||
# Install jq if not available
|
||||
if ! command -v jq &>/dev/null; then
|
||||
msg_info "Installing jq for JSON parsing"
|
||||
$STD apt-get update
|
||||
$STD apt-get install -y jq
|
||||
fi
|
||||
|
||||
# Download firmware list from Ubiquiti API
|
||||
API_URL="https://fw-update.ui.com/api/firmware-latest"
|
||||
TEMP_JSON=$(mktemp)
|
||||
|
||||
if ! curl -fsSL "$API_URL" -o "$TEMP_JSON"; then
|
||||
rm -f "$TEMP_JSON"
|
||||
msg_error "Failed to fetch data from Ubiquiti API"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse JSON to find latest unifi-os-server linux-x64 version
|
||||
LATEST=$(jq -r '
|
||||
._embedded.firmware
|
||||
| map(select(.product == "unifi-os-server"))
|
||||
| map(select(.platform == "linux-x64"))
|
||||
| sort_by(.version_major, .version_minor, .version_patch)
|
||||
| last
|
||||
' "$TEMP_JSON")
|
||||
|
||||
UOS_VERSION=$(echo "$LATEST" | jq -r '.version' | sed 's/^v//')
|
||||
UOS_URL=$(echo "$LATEST" | jq -r '._links.data.href')
|
||||
|
||||
# Cleanup temp file
|
||||
rm -f "$TEMP_JSON"
|
||||
|
||||
if [[ -z "$UOS_URL" || "$UOS_URL" == "null" || -z "$UOS_VERSION" || "$UOS_VERSION" == "null" ]]; then
|
||||
msg_error "Failed to parse UniFi OS Server version or download URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
UOS_INSTALLER="unifi-os-server-${UOS_VERSION}.bin"
|
||||
msg_ok "Found UniFi OS Server ${UOS_VERSION}"
|
||||
|
||||
# --- Download Cloud Image ---
|
||||
msg_info "Downloading ${OS_DISPLAY} Cloud Image"
|
||||
URL=$(get_image_url)
|
||||
sleep 2
|
||||
msg_ok "${CL}${BL}${URL}${CL}"
|
||||
CACHE_FILE="$(vm_image_cache_path "$URL")"
|
||||
vm_fetch_image "$URL" "$CACHE_FILE" --cache --min-bytes $((100 * 1024 * 1024)) || exit 115
|
||||
FILE="$(basename "$CACHE_FILE")"
|
||||
# Work on a copy: virt-customize below rewrites the image,
|
||||
# which would poison the cache for every later VM.
|
||||
cp -f "$CACHE_FILE" "$FILE"
|
||||
|
||||
# Resize the imported disk with vm_resize_disk; Cloud-Init grows the actual root
|
||||
# filesystem before the first-boot installer runs, without another offline copy.
|
||||
|
||||
# --- Download UniFi OS installer on the host ---
|
||||
msg_info "Downloading UniFi OS Server ${UOS_VERSION} installer"
|
||||
curl -fsSL "${UOS_URL}" -o "unifi-os-server.bin"
|
||||
chmod +x "unifi-os-server.bin"
|
||||
msg_ok "Downloaded UniFi OS Server installer"
|
||||
|
||||
# --- Pre-install packages and setup first-boot installer via virt-customize ---
|
||||
msg_info "Customizing disk image (installing packages, staging installer)"
|
||||
|
||||
# Create the first-boot installer script
|
||||
FIRSTBOOT_SCRIPT=$(mktemp)
|
||||
cat >"$FIRSTBOOT_SCRIPT" <<'FBEOF'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
LOG="/var/log/unifi-os-install.log"
|
||||
exec > >(tee -a "$LOG") 2>&1
|
||||
echo "[$(date)] Starting UniFi OS Server first-boot setup..."
|
||||
trap 'echo "[$(date)] ERROR: First-boot setup failed at line $LINENO"' ERR
|
||||
if ! systemctl start qemu-guest-agent; then
|
||||
echo "[$(date)] WARNING: Preinstalled guest agent could not start; retrying after package installation"
|
||||
fi
|
||||
|
||||
# Sync clock before apt (fresh VMs have clock skew that breaks GPG signature validation)
|
||||
echo "[$(date)] Syncing system clock..."
|
||||
if ! timedatectl set-ntp true; then
|
||||
echo "[$(date)] WARNING: Could not enable NTP; checking existing clock synchronization"
|
||||
fi
|
||||
# Try NTP first
|
||||
for attempt in {1..6}; do
|
||||
if timedatectl show -p NTPSynchronized --value 2>/dev/null | grep -q "yes"; then
|
||||
echo "[$(date)] Clock synchronized via NTP"
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
# Fallback: sync from HTTP header if NTP didn't work
|
||||
if ! timedatectl show -p NTPSynchronized --value 2>/dev/null | grep -q "yes"; then
|
||||
if HTTP_DATE=$(curl -fsSI --max-time 10 https://deb.debian.org | sed -n 's/^[Dd]ate: //p' | tr -d '\r') &&
|
||||
[ -n "$HTTP_DATE" ] && date -s "$HTTP_DATE" >/dev/null; then
|
||||
echo "[$(date)] Clock synchronized via HTTP"
|
||||
else
|
||||
echo "[$(date)] WARNING: Clock synchronization unavailable"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install required packages
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
echo "[$(date)] Installing packages..."
|
||||
for attempt in {1..3}; do
|
||||
if apt-get update -qq 2>&1; then
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "[$(date)] apt-get update failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
echo "[$(date)] apt-get update failed (attempt $attempt/3), retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
for attempt in {1..3}; do
|
||||
if apt-get install -y -qq qemu-guest-agent podman uidmap slirp4netns curl wget; then
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "[$(date)] apt-get install failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
echo "[$(date)] apt-get install failed (attempt $attempt/3), retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
systemctl enable --now qemu-guest-agent
|
||||
echo "[$(date)] Packages installed"
|
||||
|
||||
# Setup swap (2GB)
|
||||
if [ ! -f /swapfile ]; then
|
||||
fallocate -l 2G /swapfile
|
||||
chmod 600 /swapfile
|
||||
mkswap /swapfile
|
||||
swapon /swapfile
|
||||
echo '/swapfile none swap sw 0 0' >> /etc/fstab
|
||||
echo "[$(date)] Swap file created"
|
||||
fi
|
||||
|
||||
# Run UniFi OS installer
|
||||
if [ -f /opt/unifi-os-server.bin ]; then
|
||||
cd /opt
|
||||
./unifi-os-server.bin <<<'y'
|
||||
rm -f /opt/unifi-os-server.bin
|
||||
echo "[$(date)] UniFi OS Server installed successfully"
|
||||
else
|
||||
echo "[$(date)] ERROR: /opt/unifi-os-server.bin not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Disable this service after successful run
|
||||
systemctl disable unifi-os-firstboot.service
|
||||
echo "[$(date)] First-boot setup complete"
|
||||
FBEOF
|
||||
|
||||
# Create the systemd service unit file
|
||||
FIRSTBOOT_SVC=$(mktemp)
|
||||
cat >"$FIRSTBOOT_SVC" <<'SVCEOF'
|
||||
[Unit]
|
||||
Description=UniFi OS Server First Boot Installer
|
||||
After=network-online.target cloud-final.service qemu-guest-agent.service
|
||||
Wants=network-online.target cloud-final.service qemu-guest-agent.service
|
||||
ConditionPathExists=/opt/unifi-os-server.bin
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/opt/unifi-os-firstboot.sh
|
||||
RemainAfterExit=yes
|
||||
StandardOutput=journal+console
|
||||
|
||||
[Install]
|
||||
# cloud-final runs after multi-user.target; using that target here would cycle.
|
||||
WantedBy=cloud-init.target
|
||||
SVCEOF
|
||||
|
||||
vm_prepare_cloud_image "$FILE" "$HN"
|
||||
|
||||
virt-customize -a "${FILE}" \
|
||||
--upload "unifi-os-server.bin:/opt/unifi-os-server.bin" \
|
||||
--chmod 0755:/opt/unifi-os-server.bin \
|
||||
--upload "$FIRSTBOOT_SCRIPT:/opt/unifi-os-firstboot.sh" \
|
||||
--chmod 0755:/opt/unifi-os-firstboot.sh \
|
||||
--upload "$FIRSTBOOT_SVC:/etc/systemd/system/unifi-os-firstboot.service" \
|
||||
--run-command "systemctl enable unifi-os-firstboot.service" \
|
||||
--run-command "systemctl enable ssh" \
|
||||
2>&1 | while read -r line; do echo -ne "${BFR}${TAB}${YW}${HOLD}${line}${HOLD}"; done
|
||||
|
||||
rm -f "$FIRSTBOOT_SCRIPT" "$FIRSTBOOT_SVC" "unifi-os-server.bin"
|
||||
msg_ok "Disk image customized (UniFi OS ${UOS_VERSION} staged for first-boot install)"
|
||||
|
||||
msg_info "Creating UniFi OS VM"
|
||||
qm create "$VMID" -agent 1${MACHINE} -tablet 0 -localtime 1 -bios ovmf \
|
||||
${CPU_TYPE} -cores "$CORE_COUNT" -memory "$RAM_SIZE" \
|
||||
-name "$HN" -tags community-script \
|
||||
-net0 virtio,bridge="$BRG",macaddr="$MAC""$VLAN""$MTU" \
|
||||
-onboot 1 -ostype l26 -scsihw virtio-scsi-pci
|
||||
|
||||
IMPORT_OUT="$(qm importdisk "$VMID" "$FILE" "$STORAGE" --format "$DISK_IMPORT_FORMAT" 2>&1)"
|
||||
DISK_REF="$(printf '%s\n' "$IMPORT_OUT" | sed -n "s/.*successfully imported disk '\([^']\+\)'.*/\1/p")"
|
||||
|
||||
if [[ -z "$DISK_REF" ]]; then
|
||||
DISK_REF="$(pvesm list "$STORAGE" | awk -v id="$VMID" '$1 ~ ("vm-"id"-disk-") {print $1}' | sort | tail -n1)"
|
||||
fi
|
||||
if [[ -z "$DISK_REF" ]]; then
|
||||
msg_error "Unable to determine imported UniFi OS VM disk reference"
|
||||
printf '%s\n' "$IMPORT_OUT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
qm set "$VMID" \
|
||||
-efidisk0 "${STORAGE}:0,efitype=4m" \
|
||||
-scsi0 "${DISK_REF},${DISK_CACHE}size=${DISK_SIZE}" \
|
||||
-boot order=scsi0 -serial0 socket >/dev/null
|
||||
vm_resize_disk
|
||||
qm set "$VMID" --agent enabled=1 >/dev/null
|
||||
|
||||
vm_mark_created
|
||||
vm_provision "$VMID"
|
||||
# Core currently suppresses SSH-key write errors; keep them fatal for this VM.
|
||||
if [[ -n "${CLOUDINIT_SSH_KEYS:-}" ]]; then
|
||||
qm set "$VMID" --sshkeys "$CLOUDINIT_SSH_KEYS" >/dev/null
|
||||
fi
|
||||
|
||||
set_description
|
||||
|
||||
msg_ok "Created a UniFi OS VM ${CL}${BL}(${HN})"
|
||||
msg_info "Operating System: ${OS_DISPLAY}"
|
||||
msg_info "Cloud-Init: ${USE_CLOUD_INIT}"
|
||||
|
||||
VM_IP=""
|
||||
UNIFI_READY=""
|
||||
if [ "$START_VM" == "yes" ]; then
|
||||
msg_info "Starting UniFi OS VM"
|
||||
$STD qm start $VMID
|
||||
msg_ok "Started UniFi OS VM"
|
||||
|
||||
msg_info "Waiting for VM IP via the preinstalled guest agent"
|
||||
if VM_IP=$(get_vm_ip "$VMID" 360); then
|
||||
if GUEST_INTERFACES=$(qm guest cmd "$VMID" network-get-interfaces 2>"$TEMP_DIR/guest-agent.log") &&
|
||||
VM_IP=$(jq -er --arg mac "$MAC" '
|
||||
[.[] | select((.["hardware-address"] // "" | ascii_downcase) == ($mac | ascii_downcase))
|
||||
| .["ip-addresses"][]? | select(.["ip-address-type"] == "ipv4")
|
||||
| .["ip-address"] | select(startswith("127.") or startswith("169.254.") | not)]
|
||||
| first // empty
|
||||
' <<<"$GUEST_INTERFACES" 2>"$TEMP_DIR/guest-agent.log"); then
|
||||
msg_ok "Guest agent responding — VM IP: ${VM_IP}"
|
||||
else
|
||||
VM_IP=""
|
||||
msg_warn "Guest agent did not report a usable IPv4 address for the VM network interface (${MAC})"
|
||||
if [[ -s "$TEMP_DIR/guest-agent.log" ]]; then
|
||||
msg_warn "Guest agent query: $(tail -n 1 "$TEMP_DIR/guest-agent.log")"
|
||||
fi
|
||||
msg_warn "Check DHCP/static IP settings and ip -4 addr in the VM console"
|
||||
fi
|
||||
else
|
||||
msg_warn "VM started, but no IP was reported by the guest agent"
|
||||
if ! qm guest cmd "$VMID" network-get-interfaces >/dev/null 2>"$TEMP_DIR/guest-agent.log"; then
|
||||
msg_warn "Guest agent query failed: $(tail -n 1 "$TEMP_DIR/guest-agent.log")"
|
||||
fi
|
||||
msg_warn "Check the VM console: ip -4 addr; systemctl status qemu-guest-agent"
|
||||
fi
|
||||
|
||||
# Wait for UniFi OS to be ready on port 11443
|
||||
if [ -n "$VM_IP" ]; then
|
||||
msg_info "Waiting for UniFi OS to start on https://${VM_IP}:11443 (may take several minutes)"
|
||||
for i in {1..60}; do
|
||||
if curl -fsSk --max-time 3 "https://${VM_IP}:11443" -o /dev/null &>/dev/null; then
|
||||
UNIFI_READY="yes"
|
||||
break
|
||||
fi
|
||||
printf "\r${TAB}${YW}${HOLD}Waiting for UniFi OS to start on https://${VM_IP}:11443 (may take several minutes) [%ds]${HOLD}" "$((i * 5))"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [ -n "$UNIFI_READY" ]; then
|
||||
msg_ok "UniFi OS is up at https://${VM_IP}:11443"
|
||||
else
|
||||
msg_warn "UniFi OS is not ready; first-boot installation may still be running or may have failed"
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
msg_info "Start VM ${VMID} to run the UniFi OS first-boot installation"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${TAB}${GATEWAY}${BOLD}${GN}UniFi OS Server VM created!${CL}"
|
||||
echo -e "${TAB}${INFO}Web interface (after installation): https://${VM_IP:-<VM-IP>}:11443"
|
||||
echo -e "${TAB}${INFO}Console login: ${CLOUDINIT_USER:-root}"
|
||||
echo -e "${TAB}${INFO}Cloud-Init credentials: ${CLOUDINIT_CRED_FILE}"
|
||||
if [[ "$UNIFI_READY" != "yes" ]]; then
|
||||
echo -e "${TAB}${INFO}In the VM: journalctl -u cloud-final -u unifi-os-firstboot.service"
|
||||
echo -e "${TAB}${INFO}Install log: /var/log/unifi-os-install.log"
|
||||
fi
|
||||
echo ""
|
||||
post_update_to_api "done" "none"
|
||||
msg_ok "VM provisioning completed.\n"
|
||||
Reference in New Issue
Block a user