Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a5c60d4
test: resolve fallback templates from the linuxcontainers catalog
MickLesk Sep 4, 2026
9c22b37
Find the images a distro actually publishes, not just the default var…
MickLesk Sep 7, 2026
6729e36
Fix script slug generation for descriptions
MickLesk Sep 7, 2026
aead014
test: resolve fallback templates from the linuxcontainers catalog
MickLesk Sep 4, 2026
ac005d5
Find the images a distro actually publishes, not just the default var…
MickLesk Sep 7, 2026
0fc1f96
Only offer another template version when there is one
MickLesk Sep 7, 2026
f68f896
Merge branch 'test/linuxcontainers-full-catalog' of https://github.co…
MickLesk Sep 7, 2026
9592069
Track detected HW accel vendor in setup
MickLesk Sep 7, 2026
074f3ce
Reach the image catalog on every architecture, not just arm64
MickLesk Sep 7, 2026
39a6ad4
Merge remote-tracking branch 'origin/main' into test/linuxcontainers-…
MickLesk Sep 25, 2026
18d830e
Translate the releases Proxmox and the image catalog disagree on
MickLesk Sep 25, 2026
c1098e9
Create Amazon Linux containers and keep the MOTD working without host…
MickLesk Sep 25, 2026
3b5af6d
Give the patched template a release PVE actually accepts
MickLesk Sep 25, 2026
5638f0b
Merge remote-tracking branch 'origin/main' into test/linuxcontainers-…
Oct 6, 2026
4ae0ccd
Install dependencies with the container's own package manager
MickLesk Sep 24, 2026
121ec84
Merge remote-tracking branch 'origin/main' into test/linuxcontainers-…
MickLesk Oct 6, 2026
8ae4fa1
Close the linuxcontainers.org lookup message
MickLesk Oct 6, 2026
74635f5
Stop early when the image has no build for this architecture
MickLesk Oct 6, 2026
3a2cda3
Set up Arch Linux ARM containers
MickLesk Oct 6, 2026
9f05d66
Autoremove before dnf clean all
MickLesk Oct 6, 2026
f6c5b82
Drop the second MOTD on Incus
MickLesk Oct 6, 2026
b4e5e67
Stop leaked message blocks from spinning over the install
MickLesk Oct 6, 2026
c7b3113
Merge remote-tracking branch 'origin/main' into test/linuxcontainers-…
MickLesk Oct 6, 2026
98e8d7a
Survive the Kali, Oracle and Amazon Linux images on the update path
MickLesk Oct 9, 2026
33cabbd
Clean dnf, zypper and pacman containers on the Proxmox update path too
MickLesk Oct 9, 2026
787a011
Pass the terminal type to the tty1 autologin getty
MickLesk Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion core/core.func
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ ensure_profile_loaded() {
# false conditional makes `source` return that status, which as the final
# command of an && list is not exempt from errexit -- that took whole updates
# with it.
# RHEL's lang.sh reads LC_ALL unguarded; under nounset that ends the shell
# before `|| true` gets a say.
local nounset=0
[[ $- == *u* ]] && nounset=1
set +u
if [[ -d /etc/profile.d ]]; then
for script in /etc/profile.d/*.sh; do
[[ -r "$script" ]] || continue
Expand All @@ -207,6 +212,7 @@ ensure_profile_loaded() {
if [[ -r "${HOME:-/root}/.profile" ]]; then
source "${HOME:-/root}/.profile" >/dev/null 2>&1 || true
fi
((nounset)) && set -u

# Also ensure /usr/local/bin is in PATH (common install location)
if [[ ":$PATH:" != *":/usr/local/bin:"* ]]; then
Expand Down Expand Up @@ -2269,7 +2275,14 @@ cleanup_lxc() {
# problem worth reporting.
$STD apk cache clean 2>/dev/null || true
rm -rf /var/cache/apk/*
else
elif command -v dnf &>/dev/null; then
$STD dnf -y autoremove 2>/dev/null || msg_warn "dnf autoremove failed (non-critical)"
$STD dnf clean all 2>/dev/null || msg_warn "dnf clean failed (non-critical)"
elif command -v zypper &>/dev/null; then
$STD zypper clean 2>/dev/null || msg_warn "zypper clean failed (non-critical)"
elif command -v pacman &>/dev/null; then
$STD pacman -Sc --noconfirm 2>/dev/null || msg_warn "pacman clean failed (non-critical)"
elif command -v apt &>/dev/null; then
$STD apt -y autoremove 2>/dev/null || msg_warn "apt autoremove failed (non-critical)"
$STD apt -y autoclean 2>/dev/null || msg_warn "apt autoclean failed (non-critical)"
$STD apt -y clean 2>/dev/null || msg_warn "apt clean failed (non-critical)"
Expand Down
71 changes: 37 additions & 34 deletions incus/backend.func
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,18 @@ _incus_restart_ct() {
[[ "$(incus_instance_field "${CT_NAME}" s)" == "RUNNING" ]]
}

# An alias resolves whether or not the image exists for this host's
# architecture; the launch then fails with "architecture isn't supported".
_incus_image_fits_host() {
local arch
arch=$(incus image info "$1" 2>/dev/null | awk '/^Architecture:/ {print $2; exit}' || true)
[[ -n "$arch" ]] || return 2
[[ "$arch" == "$(uname -m)" ]]
}

_incus_resolve_launch_image() {
# Tries images:OS/VER (+ /cloud, codename alias, local cache). Sets INCUS_LAUNCH_IMAGE.
# Returns 2 when the image exists, but not for this architecture.
local os="${IMAGE_OS:-debian}" ver="${IMAGE_VERSION:-13}" alias=""
os="${os,,}"
alias="$(incus_get_image_alias "$os" "$ver" 2>/dev/null || true)"
Expand All @@ -129,28 +139,34 @@ _incus_resolve_launch_image() {
for cand in "${candidates[@]}"; do
if [[ -n "$local_aliases" ]] &&
grep -qxF "${cand#images:}" <<<"$local_aliases" &&
incus image info "${cand#images:}" &>/dev/null; then
_incus_image_fits_host "${cand#images:}"; then
INCUS_LAUNCH_IMAGE="${cand#images:}"
return 0
fi
done

local fit other_arch=0
for cand in "${candidates[@]}"; do
if incus image info "$cand" &>/dev/null; then
fit=0
_incus_image_fits_host "$cand" || fit=$?
if ((fit == 0)); then
INCUS_LAUNCH_IMAGE="$cand"
return 0
fi
((fit == 1)) && other_arch=1
done

# Not on the image server under any name we know: take its newest version.
local newest
newest=$(incus image alias list images: "${os}/" -f csv 2>/dev/null | cut -d, -f1 |
awk -F/ 'NF == 2 && $2 ~ /^[0-9][0-9.]*$/ {print $2}' | sort -uV | tail -n1 || true)
if [[ -n "$newest" ]] && incus image info "images:${os}/${newest}" &>/dev/null; then
msg_warn "${os} ${ver} is not on the image server; using ${os} ${newest}"
INCUS_LAUNCH_IMAGE="images:${os}/${newest}"
return 0
fi
for newest in $(incus image alias list images: "${os}/" -f csv 2>/dev/null | cut -d, -f1 |
awk -F/ 'NF == 2 && $2 ~ /^[0-9][0-9.]*$/ {print $2}' | sort -urV || true); do
if _incus_image_fits_host "images:${os}/${newest}"; then
msg_warn "${os} ${ver} is not on the image server; using ${os} ${newest}"
INCUS_LAUNCH_IMAGE="images:${os}/${newest}"
return 0
fi
done
((other_arch)) && return 2

# Last resort: try remote launch of primary (incus may pull on demand)
INCUS_LAUNCH_IMAGE="images:${os}/${ver}"
Expand Down Expand Up @@ -365,7 +381,12 @@ incus_create_lxc_container() {

msg_info "Resolving container image"
local image_name=""
if _incus_resolve_launch_image; then
local resolve_rc=0
_incus_resolve_launch_image || resolve_rc=$?
if ((resolve_rc == 2)); then
msg_error "${IMAGE_OS} ${IMAGE_VERSION} has no $(uname -m) image on the image server"
exit 106
elif ((resolve_rc == 0)); then
image_name="${INCUS_LAUNCH_IMAGE}"
msg_ok "Image ${BL}${image_name}${CL}"
else
Expand Down Expand Up @@ -430,12 +451,13 @@ incus_create_lxc_container() {

if ! incus launch "${image_name}" "${CT_NAME}" "${launch_args[@]}" >>"$LOGFILE" 2>&1; then
# Retry alternate candidates once if primary pull failed
local retry_img="" launched=0
local retry_img="" launched=0 tried=" ${image_name} "
for retry_img in "images:${IMAGE_OS}/${IMAGE_VERSION}/cloud" \
"images:${IMAGE_OS}/$(incus_get_image_alias "${IMAGE_OS}" "${IMAGE_VERSION}" 2>/dev/null || true)" \
"images:${IMAGE_OS}/$(incus_get_image_alias "${IMAGE_OS}" "${IMAGE_VERSION}" 2>/dev/null || true)/cloud"; do
[[ -z "$retry_img" || "$retry_img" == "$image_name" || "$retry_img" == */ ]] && continue
msg_warn "Launch failed with ${image_name} — retrying ${retry_img}"
[[ -z "$retry_img" || "$retry_img" == */ || "$tried" == *" ${retry_img} "* ]] && continue
tried+="${retry_img} "
msg_warn "Launch failed, retrying with ${retry_img}"
if incus launch "${retry_img}" "${CT_NAME}" "${launch_args[@]}" >>"$LOGFILE" 2>&1; then
image_name="$retry_img"
launched=1
Expand Down Expand Up @@ -1026,26 +1048,6 @@ EOF"
msg_ok "Customized LXC Container"
}

incus_setup_motd_pve_style() {
msg_info "Setting up MOTD"
local motd_file
motd_file=$(mktemp)
cat >"$motd_file" <<MOTDEOF
#!/bin/bash
# Community Scripts MOTD
if [[ -f /etc/os-release ]]; then . /etc/os-release; fi
echo ''
echo ' 🚀 ${APP} LXC'
echo " 🖥️ OS: \${PRETTY_NAME:-Linux}"
echo " 📡 IP: \$(hostname -I 2>/dev/null | awk '{print \$1}')"
echo ''
MOTDEOF
incus file push "$motd_file" "${CT_NAME}/etc/profile.d/99-community-scripts-motd.sh" >>"${LOGFILE:-$INCUS_BUILD_LOG}" 2>&1
incus_ct_exec chmod +x /etc/profile.d/99-community-scripts-motd.sh >>"${LOGFILE:-$INCUS_BUILD_LOG}" 2>&1
rm -f "$motd_file"
msg_ok "MOTD configured"
}

_incus_push_functions_and_run_install() {
# Sets INCUS_LAST_INSTALL_EXIT (do not capture stdout — msg_* writes there).
INCUS_LAST_INSTALL_EXIT=0
Expand All @@ -1066,6 +1068,8 @@ INNEREOF" >>"${LOGFILE:-$INCUS_BUILD_LOG}" 2>&1 || { msg_error "Failed to push f
# prompts - the spinner kept redrawing over them, which is why Immich's
# machine-learning menu could not be read and the keystroke landed in the
# spinner line. A static line, and the script owns the terminal from here.
# Emptied first, or msg_custom would resume a block the host left open.
_MSG_BLOCKS=()
msg_custom "🚀" "${GN}" "Starting application installation"
local _install_script _run_env lxc_exit=0 install_exit_code=0
# Checked here: inside $(...) below, stdout is the capture pipe, never the terminal.
Expand Down Expand Up @@ -1489,7 +1493,6 @@ incus_build_container() {
incus_fix_debian13_root_ownership
incus_customize_container
install_ssh_keys_into_ct
incus_setup_motd_pve_style
incus_run_install_script_with_recovery
}

Expand Down
2 changes: 1 addition & 1 deletion incus/build.func
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ incus_variables() {
NSAPP=$(echo "${APP,,}" | tr -d ' ')
var_install="${NSAPP}-install"
INTEGER='^[0-9]+([.][0-9]+)?$'
HOST_NAME=$(hostname)
HOST_NAME=$(hostname 2>/dev/null || uname -n)
PVEHOST_NAME="$HOST_NAME"
DIAGNOSTICS="yes"
METHOD="default"
Expand Down
7 changes: 6 additions & 1 deletion incus/core.func
Original file line number Diff line number Diff line change
Expand Up @@ -835,11 +835,16 @@ incus_ensure_profile_loaded() {
[[ -n "${_INCUS_PROFILE_LOADED:-}" ]] && return
command -v incusd &>/dev/null && return

local nounset=0
[[ $- == *u* ]] && nounset=1
set +u
if [[ -d /etc/profile.d ]]; then
for script in /etc/profile.d/*.sh; do
[[ -r "$script" ]] && source "$script"
[[ -r "$script" ]] || continue
source "$script" >/dev/null 2>&1 || true
done
fi
((nounset)) && set -u

if [[ ":$PATH:" != *":/usr/local/bin:"* ]]; then
export PATH="/usr/local/bin:$PATH"
Expand Down
91 changes: 75 additions & 16 deletions incus/tools.func
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,17 @@ detect_os() {
case "$OS_TYPE" in
debian | ubuntu | devuan) OS_FAMILY="debian"; PKG_MANAGER="apt" ;;
alpine) OS_FAMILY="alpine"; PKG_MANAGER="apk" ;;
fedora | rocky | rockylinux | alma | almalinux | centos | rhel | openeuler) OS_FAMILY="rhel"; PKG_MANAGER="dnf" ;;
fedora | rocky | rockylinux | alma | almalinux | centos | rhel | openeuler | oracle | ol | amzn | amazonlinux) OS_FAMILY="rhel"; PKG_MANAGER="dnf" ;;
opensuse* | sles) OS_TYPE="opensuse"; OS_FAMILY="suse"; PKG_MANAGER="zypper" ;;
arch | archlinux) OS_TYPE="arch"; OS_FAMILY="arch"; PKG_MANAGER="pacman" ;;
gentoo) OS_FAMILY="gentoo"; PKG_MANAGER="emerge" ;;
*) OS_FAMILY="unknown"; PKG_MANAGER="apt" ;;
*)
OS_FAMILY="unknown"
PKG_MANAGER="apt"
for pm in apt-get dnf apk zypper pacman; do
command -v "$pm" &>/dev/null && PKG_MANAGER="${pm%-get}" && break
done
;;
esac

if command -v systemctl &>/dev/null && [[ -d /run/systemd/system ]]; then
Expand Down Expand Up @@ -196,20 +202,70 @@ network_check() {

update_os() {
msg_info "Updating Container OS"
apt_update_safe
$STD apt upgrade -y
pkg_update
pkg_upgrade
msg_ok "Updated Container OS"
}

# install.func parity (safe Incus subset)
pkg_update() { $STD apt update; }
pkg_upgrade() { $STD apt upgrade -y; }
pkg_install() { $STD apt install -y "$@"; }
pkg_remove() { $STD apt remove -y "$@"; }
pkg_update() {
case "${PKG_MANAGER:-apt}" in
apt) apt_update_safe ;;
apk) $STD apk update ;;
dnf) $STD dnf -y makecache ;;
zypper) $STD zypper -n refresh ;;
pacman) $STD pacman -Sy --noconfirm ;;
*) : ;;
esac
}
pkg_upgrade() {
case "${PKG_MANAGER:-apt}" in
apt) $STD apt upgrade -y ;;
apk) $STD apk upgrade ;;
dnf) $STD dnf -y upgrade ;;
zypper) $STD zypper -n update ;;
pacman) $STD pacman -Su --noconfirm ;;
*) : ;;
esac
}
pkg_install() {
case "${PKG_MANAGER:-apt}" in
apt) $STD apt install -y "$@" ;;
apk) $STD apk add "$@" ;;
dnf) $STD dnf -y install "$@" ;;
zypper) $STD zypper -n install "$@" ;;
pacman) $STD pacman -S --noconfirm --needed "$@" ;;
emerge) $STD emerge "$@" ;;
*) msg_error "pkg_install: unsupported package manager '${PKG_MANAGER:-}'"; return 1 ;;
esac
}
pkg_remove() {
case "${PKG_MANAGER:-apt}" in
apt) $STD apt remove -y "$@" ;;
apk) $STD apk del "$@" ;;
dnf) $STD dnf -y remove "$@" ;;
zypper) $STD zypper -n remove "$@" ;;
pacman) $STD pacman -Rs --noconfirm "$@" ;;
emerge) $STD emerge --deselect "$@" ;;
*) : ;;
esac
}
pkg_clean() {
$STD apt -y autoremove 2>/dev/null || true
$STD apt -y autoclean 2>/dev/null || true
$STD apt -y clean 2>/dev/null || true
case "${PKG_MANAGER:-apt}" in
apt)
$STD apt -y autoremove 2>/dev/null || true
$STD apt -y autoclean 2>/dev/null || true
$STD apt -y clean 2>/dev/null || true
;;
apk) $STD apk cache clean 2>/dev/null || true ;;
dnf)
$STD dnf -y autoremove 2>/dev/null || true
$STD dnf clean all 2>/dev/null || true
;;
zypper) $STD zypper clean 2>/dev/null || true ;;
pacman) $STD pacman -Sc --noconfirm 2>/dev/null || true ;;
*) : ;;
esac
}

svc_enable() { systemctl enable -q "$@"; }
Expand Down Expand Up @@ -273,7 +329,7 @@ motd_ssh() {
local ip
ip=$(get_lxc_ip)
cat <<EOF >/etc/motd
🚀 Incus Container: $(hostname)
🚀 Incus Container: $(uname -n)
🖥️ OS: $(grep ^PRETTY_NAME /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"')
📡 IP Address: ${ip}
EOF
Expand All @@ -293,10 +349,13 @@ EOF
}

cleanup_lxc() {
$STD apt -y autoremove 2>/dev/null || true
$STD apt -y autoclean 2>/dev/null || true
rm -f /tmp/incus-functions 2>/dev/null || true
rm -f /tmp/incus-funcs.sh 2>/dev/null || true
if declare -f pkg_clean >/dev/null; then
pkg_clean
else
$STD apt -y autoremove 2>/dev/null || true
$STD apt -y autoclean 2>/dev/null || true
fi
rm -f /tmp/incus-functions /tmp/incus-funcs.sh 2>/dev/null || true
}

# check_container_storage / check_container_resources:
Expand Down
42 changes: 40 additions & 2 deletions lib/system.func
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,41 @@ ensure_dependencies() {
return 0
fi

# dnf/zypper/pacman/emerge: no cheap name-to-binary map, so go by command and
# let the manager skip whatever is already there.
local family=""
declare -F _cs_os_family >/dev/null 2>&1 && family="$(_cs_os_family)"
case "$family" in
rhel | suse | arch | gentoo)
local -a pm=()
case "$family" in
rhel)
if command -v dnf >/dev/null 2>&1; then pm=(dnf install -y); else pm=(yum install -y); fi
;;
suse) pm=(zypper --non-interactive install --no-recommends) ;;
arch) pm=(pacman -S --noconfirm --needed) ;;
gentoo) pm=(emerge --quiet --noreplace) ;;
esac

for dep in "${deps[@]}"; do
command -v "$dep" >/dev/null 2>&1 || missing+=("$dep")
done
((${#missing[@]})) || return 0

$STD "${pm[@]}" "${missing[@]}" || {
local failed=()
for pkg in "${missing[@]}"; do
$STD "${pm[@]}" "$pkg" 2>/dev/null || failed+=("$pkg")
done
if ((${#failed[@]})); then
msg_error "Failed to install dependencies: ${failed[*]}"
return 1
fi
}
return 0
;;
esac

# Debian/Ubuntu: Fast batch check using dpkg-query
local installed_pkgs
installed_pkgs=$(dpkg-query -W -f='${Package}\n' 2>/dev/null | sort -u)
Expand Down Expand Up @@ -2102,8 +2137,11 @@ verify_gpg_fingerprint() {
create_self_signed_cert() {
local APP_NAME="${1:-${APPLICATION}}"
local EXTRA_SAN="${2:-}"
local HOSTNAME="$(hostname -f)"
local IP="$(hostname -I | awk '{print $1}')"
# RHEL-family minimal images ship no hostname binary, which left CN and SAN empty.
local HOSTNAME IP
HOSTNAME="$(hostname -f 2>/dev/null || uname -n)"
IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
[[ -n "$IP" ]] || IP="$(ip -4 route get 1 2>/dev/null | sed -n 's/.* src \([0-9.]\+\).*/\1/p' | head -1)"
local APP_NAME_LC=$(echo "${APP_NAME,,}" | tr -d ' ')
local CERT_DIR="/etc/ssl/${APP_NAME_LC}"
local CERT_KEY="${CERT_DIR}/${APP_NAME_LC}.key"
Expand Down
Loading
Loading