Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
81 changes: 66 additions & 15 deletions incus/vm-core.func
Original file line number Diff line number Diff line change
Expand Up @@ -1487,46 +1487,97 @@ vm_expand_image() {
return 0
}

# Same code as in pve/vm-core.func, defined here too because an Incus host may
# never load that file. Every step used to end in `>/dev/null 2>&1 || true` and
# the caller reported success either way, so an image none of them reached
# looked exactly like one that worked.
declare -ga _VM_PREPARE_FAILED=()
_VM_PREPARE_COLLECTING=0

_vm_customize() {
local label="${1:?label}" image="${2:?image}"
shift 2
local log detail entry
log="$(mktemp)" || log=/dev/null

if virt-customize -q -a "$image" "$@" >"$log" 2>&1; then
[[ "$log" != /dev/null ]] && rm -f "$log"
return 0
fi

detail="$(grep -m1 -iE 'error|cannot|failed' "$log" 2>/dev/null |
sed -E 's/^[^:]+: +//; s/[[:space:]]+$//' | cut -c1-140)"
[[ "$log" != /dev/null ]] && rm -f "$log"

# Always 0: these run as bare statements under `set -e`, and the failure is
# recorded rather than lost. Callers that care compare the array length.
entry="${label}${detail:+ - ${detail}}"
_VM_PREPARE_FAILED+=("$entry")
((_VM_PREPARE_COLLECTING)) || msg_warn "Image step failed: ${entry}"
return 0
}

vm_prepare_cloud_image() {
local image="${1:?image}" hostname="${2:-${HN:-vm}}"

if ! vm_ensure_virt_customize; then
msg_warn "Importing the image unmodified"
msg_warn "Importing the image unmodified - virt-customize is unavailable"
return 1
fi

_VM_PREPARE_FAILED=()
_VM_PREPARE_COLLECTING=1

msg_info "Preparing the image"
virt-customize -q -a "$image" --hostname "$hostname" >/dev/null 2>&1 || true
_vm_customize "hostname" "$image" --hostname "$hostname"
# A cloned machine-id gives every VM from this image the same DHCP lease.
virt-customize -q -a "$image" \
--run-command 'truncate -s 0 /etc/machine-id; rm -f /var/lib/dbus/machine-id' \
>/dev/null 2>&1 || true
virt-customize -q -a "$image" --run-command \
'sed -i "s/^#*PermitRootLogin.*/PermitRootLogin yes/;s/^#*PasswordAuthentication.*/PasswordAuthentication yes/" /etc/ssh/sshd_config' \
>/dev/null 2>&1 || true
_vm_customize "machine-id" "$image" \
--run-command 'truncate -s 0 /etc/machine-id; rm -f /var/lib/dbus/machine-id'
_vm_customize "sshd password login" "$image" --run-command \
'sed -i "s/^#*PermitRootLogin.*/PermitRootLogin yes/;s/^#*PasswordAuthentication.*/PasswordAuthentication yes/" /etc/ssh/sshd_config'

vm_enable_consoles "$image"
vm_install_guest_agent "$image" || true
msg_ok "Prepared the image"
_VM_PREPARE_COLLECTING=0

if ((${#_VM_PREPARE_FAILED[@]})); then
msg_error "Prepared the image - ${#_VM_PREPARE_FAILED[@]} step(s) failed"
local step
for step in "${_VM_PREPARE_FAILED[@]}"; do
msg_warn "$step"
done
else
msg_ok "Prepared the image"
fi
return 0
}

# `incus console` attaches to the first serial port, so a cloud image with no
# getty on ttyS0 gives a console that shows nothing. Same fix as on Proxmox.
vm_enable_consoles() {
local image="${1:?image}"
local image="${1:?image}" cloud_init="${2:-${USE_CLOUD_INIT:-yes}}"
command -v virt-customize >/dev/null 2>&1 || return 0

virt-customize -q -a "$image" \
--run-command 'systemctl enable getty@tty1.service serial-getty@ttyS0.service' \
>/dev/null 2>&1 || true
_vm_customize "console services" "$image" \
--run-command 'systemctl enable getty@tty1.service serial-getty@ttyS0.service'

# Without cloud-init nothing sets a password, and the nocloud images ship a
# locked root: the getty above then puts an unanswerable prompt on tty1.
# Those images get in through console autologin, so give tty1 the same
# treatment the serial console already has.
if [[ "$cloud_init" != "yes" ]]; then
_vm_customize "tty1 autologin" "$image" --run-command \
'mkdir -p /etc/systemd/system/getty@tty1.service.d
printf "[Service]\nExecStart=\nExecStart=-/sbin/agetty --autologin root --noclear %%I \$TERM\n" \
>/etc/systemd/system/getty@tty1.service.d/autologin.conf'
fi

virt-customize -q -a "$image" --run-command \
_vm_customize "grub console" "$image" --run-command \
'if [ -f /etc/default/grub ]; then
sed -i "s/console=ttyS0[^ \"]*/console=tty1 &/" /etc/default/grub
grep -q "console=tty1" /etc/default/grub || sed -i "s/\(GRUB_CMDLINE_LINUX_DEFAULT=\"\)/\1console=tty1 /" /etc/default/grub
command -v update-grub >/dev/null 2>&1 && update-grub
fi' >/dev/null 2>&1 || true
fi'
return 0
}

Expand Down
105 changes: 78 additions & 27 deletions pve/vm-core.func
Original file line number Diff line number Diff line change
Expand Up @@ -1938,50 +1938,101 @@ vm_prepare_cloud_image() {
local image="${1:?image}" hostname="${2:-${HN:-vm}}"

if ! vm_ensure_virt_customize; then
msg_warn "Importing the image unmodified"
msg_warn "Importing the image unmodified - virt-customize is unavailable"
return 1
fi

_VM_PREPARE_FAILED=()
_VM_PREPARE_COLLECTING=1

msg_info "Preparing the image"
virt-customize -q -a "$image" --hostname "$hostname" >/dev/null 2>&1 || true
_vm_customize "hostname" "$image" --hostname "$hostname"
# A cloned machine-id gives every VM from this image the same DHCP lease.
virt-customize -q -a "$image" \
--run-command 'truncate -s 0 /etc/machine-id; rm -f /var/lib/dbus/machine-id' \
>/dev/null 2>&1 || true
_vm_customize "machine-id" "$image" \
--run-command 'truncate -s 0 /etc/machine-id; rm -f /var/lib/dbus/machine-id'
# Cloud-Init sets a root password, which is useless if sshd refuses it.
virt-customize -q -a "$image" --run-command \
'sed -i "s/^#*PermitRootLogin.*/PermitRootLogin yes/;s/^#*PasswordAuthentication.*/PasswordAuthentication yes/" /etc/ssh/sshd_config' \
>/dev/null 2>&1 || true
_vm_customize "sshd password login" "$image" --run-command \
'sed -i "s/^#*PermitRootLogin.*/PermitRootLogin yes/;s/^#*PasswordAuthentication.*/PasswordAuthentication yes/" /etc/ssh/sshd_config'

vm_enable_consoles "$image"
vm_install_guest_agent "$image" || true
msg_ok "Prepared the image"
_VM_PREPARE_COLLECTING=0

if ((${#_VM_PREPARE_FAILED[@]})); then
msg_error "Prepared the image - ${#_VM_PREPARE_FAILED[@]} step(s) failed"
local step
for step in "${_VM_PREPARE_FAILED[@]}"; do
msg_warn "$step"
done
else
msg_ok "Prepared the image"
fi
return 0
}

# Cloud images send the kernel console to ttyS0 and run no getty on tty1, so
# Proxmox's console stays black -- the VM is fine, nothing is drawing on it.
# The nocloud image variants configure both consoles themselves, which is why
# only the cloud-init path showed this.
# Every virt-customize step used to end in `>/dev/null 2>&1 || true` and the
# caller reported success either way, so an image none of them reached looked
# exactly like one that worked. Failures are named instead: collected while
# vm_prepare_cloud_image holds a msg block, warned about directly otherwise.
declare -ga _VM_PREPARE_FAILED=()
_VM_PREPARE_COLLECTING=0

_vm_customize() {
local label="${1:?label}" image="${2:?image}"
shift 2
local log detail entry
log="$(mktemp)" || log=/dev/null

if virt-customize -q -a "$image" "$@" >"$log" 2>&1; then
[[ "$log" != /dev/null ]] && rm -f "$log"
return 0
fi

detail="$(grep -m1 -iE 'error|cannot|failed' "$log" 2>/dev/null |
sed -E 's/^[^:]+: +//; s/[[:space:]]+$//' | cut -c1-140)"
[[ "$log" != /dev/null ]] && rm -f "$log"

# Always 0: these run as bare statements under `set -e`, and the failure is
# recorded rather than lost. Callers that care compare the array length.
entry="${label}${detail:+ - ${detail}}"
_VM_PREPARE_FAILED+=("$entry")
((_VM_PREPARE_COLLECTING)) || msg_warn "Image step failed: ${entry}"
return 0
}

vm_enable_consoles() {
local image="${1:?image}"
local image="${1:?image}" cloud_init="${2:-${USE_CLOUD_INIT:-yes}}"
command -v virt-customize >/dev/null 2>&1 || return 0

# No autologin: cloud-init sets a password, so a prompt is the right thing.
virt-customize -q -a "$image" \
--run-command 'systemctl enable getty@tty1.service serial-getty@ttyS0.service' \
>/dev/null 2>&1 || true
# With cloud-init a password gets set, so a prompt is the right thing.
_vm_customize "console services" "$image" \
--run-command 'systemctl enable getty@tty1.service serial-getty@ttyS0.service'

# Without it nothing sets one, and the nocloud images ship a locked root: the
# getty above then puts an unanswerable prompt on tty1, which is exactly the
# login screen people hit in noVNC. Those images get in through console
# autologin, so give tty1 the same treatment the serial console already has.
if [[ "$cloud_init" != "yes" ]]; then
_vm_customize "tty1 autologin" "$image" --run-command \
'mkdir -p /etc/systemd/system/getty@tty1.service.d
printf "[Service]\nExecStart=\nExecStart=-/sbin/agetty --autologin root --noclear %%I \$TERM\n" \
>/etc/systemd/system/getty@tty1.service.d/autologin.conf'
fi

# A getty alone only gets you a login prompt after boot -- the kernel still
# talks to ttyS0 only, so the screen stays black until then. Listing tty1
# first and ttyS0 last keeps qm terminal as the primary console while the
# graphical one also shows the boot.
virt-customize -q -a "$image" --run-command \
_vm_customize "grub console" "$image" --run-command \
'if [ -f /etc/default/grub ]; then
sed -i "s/console=ttyS0[^ \"]*/console=tty1 &/" /etc/default/grub
grep -q "console=tty1" /etc/default/grub || sed -i "s/\(GRUB_CMDLINE_LINUX_DEFAULT=\"\)/\1console=tty1 /" /etc/default/grub
command -v update-grub >/dev/null 2>&1 && update-grub
fi' >/dev/null 2>&1 || true
fi'
return 0
}

Expand All @@ -1991,16 +2042,16 @@ vm_enable_consoles() {
# appliance, so this reports rather than fails.
vm_install_guest_agent() {
local image="${1:?image}"
local before=${#_VM_PREPARE_FAILED[@]}
command -v virt-customize >/dev/null 2>&1 || return 0

if virt-customize -q -a "$image" --install qemu-guest-agent >/dev/null 2>&1; then
virt-customize -q -a "$image" \
--run-command 'systemctl enable qemu-guest-agent.service' >/dev/null 2>&1 || true
return 0
fi
_vm_customize "qemu-guest-agent (VM shows no IP in Proxmox)" "$image" \
--install qemu-guest-agent
((${#_VM_PREPARE_FAILED[@]} > before)) && return 1

msg_warn "Could not install qemu-guest-agent -- the VM will show no IP in Proxmox"
return 1
_vm_customize "guest agent service" "$image" \
--run-command 'systemctl enable qemu-guest-agent.service'
return 0
}

# Disk first, CD second. An empty disk is not bootable so the installer still
Expand Down Expand Up @@ -2069,7 +2120,7 @@ vm_select_storage() {
if pvesm status -storage "$STORAGE" &>/dev/null; then
# The interactive path ends with this too. Skipping it here left
# DISK_IMPORT_FORMAT and friends unset for every preselected storage.
STORAGE_TYPE=$(pvesm status -storage "$STORAGE" | awk 'NR>1 {print $2}')
STORAGE_TYPE=$(pvesm status -storage "$STORAGE" 2>/dev/null | awk 'NR>1 {print $2}')
vm_apply_storage_layout "$STORAGE_TYPE"
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
return 0
Expand All @@ -2090,9 +2141,9 @@ vm_select_storage() {
msg_max_length=$((${#item} + offset))
fi
storage_menu+=("$tag" "$item" "OFF")
done < <(pvesm status -content images | awk 'NR>1')
done < <(pvesm status -content images 2>/dev/null | awk 'NR>1')

valid_storage=$(pvesm status -content images | awk 'NR>1')
valid_storage=$(pvesm status -content images 2>/dev/null | awk 'NR>1')
if [ -z "$valid_storage" ]; then
msg_error "Unable to detect a valid storage location."
exit 119 # no valid storage found
Expand Down Expand Up @@ -2123,7 +2174,7 @@ vm_select_storage() {
msg_ok "Using ${CL}${BL}$STORAGE${CL} ${GN}for Storage Location."
msg_ok "Virtual Machine ID is ${CL}${BL}$VMID${CL}."

STORAGE_TYPE=$(pvesm status -storage "$STORAGE" | awk 'NR>1 {print $2}')
STORAGE_TYPE=$(pvesm status -storage "$STORAGE" 2>/dev/null | awk 'NR>1 {print $2}')
vm_apply_storage_layout "$STORAGE_TYPE"
}

Expand Down
24 changes: 18 additions & 6 deletions vm/cloud-init.func
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,14 @@ function configure_cloudinit_ssh_keys() {
# ------------------------------------------------------------------------------
# _ci_msg - Internal message helper with fallback
# ------------------------------------------------------------------------------
function _ci_msg_info() { msg_info "$1" 2>/dev/null || echo "[INFO] $1"; }
function _ci_msg_ok() { msg_ok "$1" 2>/dev/null || echo "[OK] $1"; }
function _ci_msg_warn() { msg_warn "$1" 2>/dev/null || echo "[WARN] $1"; }
function _ci_msg_error() { msg_error "$1" 2>/dev/null || echo "[ERROR] $1"; }
# The fallback used to be `msg_x "$1" 2>/dev/null || echo ...`, which discarded
# every message msg_warn and msg_error write - both go to stderr - and never
# reached the echo, because those return 0. Errors in here were invisible, so a
# failure surfaced as nothing but the trap's line number.
function _ci_msg_info() { if declare -F msg_info >/dev/null; then msg_info "$1"; else echo "[INFO] $1"; fi; }
function _ci_msg_ok() { if declare -F msg_ok >/dev/null; then msg_ok "$1"; else echo "[OK] $1"; fi; }
function _ci_msg_warn() { if declare -F msg_warn >/dev/null; then msg_warn "$1"; else echo "[WARN] $1" >&2; fi; }
function _ci_msg_error() { if declare -F msg_error >/dev/null; then msg_error "$1"; else echo "[ERROR] $1" >&2; fi; }

# ------------------------------------------------------------------------------
# validate_ip_cidr - Validate IP address in CIDR format
Expand Down Expand Up @@ -578,13 +582,17 @@ function display_cloud_init_info() {
echo -e "${TAB:- }${DGN:-}User: ${BGN:-}${CLOUDINIT_USER:-root}${CL:-}"
echo -e "${TAB:- }${DGN:-}Password: ${BGN:-}${CLOUDINIT_PASSWORD}${CL:-}"
echo -e "${TAB:- }${DGN:-}Credentials: ${BL:-}${CLOUDINIT_CRED_FILE}${CL:-}"
echo -e "${TAB:- }${DGN:-}Console: ${BGN:-}xterm.js${CL:-}${DGN:-} - cloud images use the serial console, noVNC stays blank${CL:-}"
echo -e "${TAB:- }${DGN:-}Cloud-Init needs a minute on first boot before the login works${CL:-}"
echo -e "${TAB:- }${RD:-}⚠️ Delete credentials file after noting password!${CL:-}"
else
echo ""
echo "[INFO] Cloud-Init Configuration:"
echo " User: ${CLOUDINIT_USER:-root}"
echo " Password: ${CLOUDINIT_PASSWORD}"
echo " Credentials: ${CLOUDINIT_CRED_FILE}"
echo " Console: xterm.js - cloud images use the serial console, noVNC stays blank"
echo " Cloud-Init needs a minute on first boot before the login works"
echo " ⚠️ Delete credentials file after noting password!"
fi
fi
Expand Down Expand Up @@ -681,12 +689,16 @@ function setup_cloud_init_network_no_rename() {
fi

if [[ -z "$snippet_storage" ]]; then
_ci_msg_error "No active Proxmox storage supporting snippets was found"
_ci_msg_error "A static IP needs a storage with 'snippets' content, and none is active"
_ci_msg_warn "Enable Snippets under Datacenter > Storage on a directory storage, or set CLOUDINIT_SNIPPET_STORAGE"
return 1
fi

snippet_volid="${snippet_storage}:snippets/${snippet_name}"
snippet_path="$(pvesm path "$snippet_volid")"
if ! snippet_path="$(pvesm path "$snippet_volid" 2>/dev/null)" || [[ -z "$snippet_path" ]]; then
_ci_msg_error "Could not resolve a path for ${snippet_volid}"
return 1
fi

mkdir -p "$(dirname "$snippet_path")"

Expand Down
Loading