I am trying to automate a build process using Jenkins and Packer together. I have everything working up through Packer rebooting the host after a successful install. Once it installs (verify through VNC) it goes for reboot and then instead of booting to the newly installed host, it boots back into the iso installer. I have tried telling the preseed to remove the CD and asked ChatGPT for numerous solutions and everything cause it to either break the build or continues to boot to the iso after install.
My repo isn’t public, but here is my packer file:
packer {
required_plugins {
qemu = {
version = ">= 1.0.0"
source = "github.com/hashicorp/qemu"
}
}
}
variable "hostname" { type = string }
variable "tailscale_key" { type = string }
variable "root_password" { type = string }
variable "output_format" { type = string }
variable "iso_url" { type = string }
variable "iso_checksum" { type = string }
variable "addressing_type" {
type = string
default = "dhcp"
}
variable "static_ip" {
type = string
default = ""
}
variable "static_subnet" {
type = string
default = ""
}
variable "static_gateway" {
type = string
default = ""
}
variable "deploy_key_path" {
type = string
default = ""
}
source "qemu" "vm_image" {
iso_url = var.iso_url
iso_checksum = var.iso_checksum
vm_name = "${var.hostname}"
output_directory = "output"
format = "qcow2"
disk_size = "50000M"
cpus = 2
memory = 4096
# Enhanced VM configuration
accelerator = "kvm"
disk_interface = "virtio"
net_device = "virtio-net"
headless = false
use_default_display = true
# VNC Configuration - consistent with display setting
vnc_port_min = 5999
vnc_port_max = 5999
vnc_bind_address = "0.0.0.0"
# Improved SSH configuration
ssh_username = "root"
ssh_password = "changeme"
ssh_timeout = "20m"
ssh_port = 22
# QEMU binary and args
qemu_binary = "qemu-system-x86_64"
qemuargs = [
["-m", "4096M"],
["-smp", "2"],
["-boot", "order=d"],
["-drive", "file=output/${var.hostname},if=virtio,cache=writeback,discard=ignore,format=qcow2"],
["-drive", "file=${var.iso_url},media=cdrom,readonly=on"],
["-netdev", "user,id=user.0,hostfwd=tcp::{{ .SSHHostPort }}-:22"],
["-device", "virtio-net,netdev=user.0"],
["-machine", "accel=kvm"],
["-cpu", "host"],
["-serial", "stdio"]
]
# Boot configuration - simplified
boot_wait = "15s"
boot_command = [
# Wait for boot menu - press tab to edit boot options
"<esc><wait5>",
# Append installer options
"/install.amd/vmlinuz vga=788 auto=true priority=critical ",
"url=http://{{ .HTTPIP }}:{{ .HTTPPort }}/preseed.cfg ",
"initrd=/install.amd/initrd.gz --- quiet ",
"hostname=${var.hostname} ",
"domain= ",
"interface=auto ",
"netcfg/get_domain= ",
"netcfg/get_hostname=${var.hostname} ",
"<enter><wait>"
]
# Debug output for HTTP server
http_directory = "http"
http_port_min = 8000
http_port_max = 8000
}
build {
sources = ["source.qemu.vm_image"]
# Start with a single provisioner for most setup tasks
provisioner "shell" {
inline = [
"hostnamectl set-hostname ${var.hostname}",
"apt update -y",
"apt install -y curl wget git qemu-guest-agent golang",
"systemctl enable qemu-guest-agent",
"curl -fsSL https://tailscale.com/install.sh | sh",
"tailscale up --authkey=${var.tailscale_key} &>/dev/null & disown",
"apt-get clean",
"rm -rf /var/lib/apt/lists/*",
"history -c",
"echo 'Provisioning complete.'"
]
}
provisioner "file" {
source = var.deploy_key_path
destination = "/tmp/deploy_key.pem"
}
provisioner "shell" {
inline = [
"chmod 600 /tmp/deploy_key.pem",
"mkdir -p ~/.ssh",
"ssh-keyscan github.com >> ~/.ssh/known_hosts",
"GIT_SSH_COMMAND='ssh -i /tmp/deploy_key.pem -o StrictHostKeyChecking=no' git clone git@github.com:[redacted]/pttools.git /opt/pttools",
"rm -f /tmp/deploy_key.pem"
]
}
provisioner "shell" {
inline = [
"echo 'Configuring network interfaces...'",
<<EOF
if [ "${var.addressing_type}" = "static" ]; then
cat <<EOT > /etc/network/interfaces.d/eth0
auto eth0
iface eth0 inet static
address ${var.static_ip}
netmask ${var.static_subnet}
gateway ${var.static_gateway}
EOT
else
cat <<EOT > /etc/network/interfaces.d/eth0
auto eth0
iface eth0 inet dhcp
EOT
fi
EOF
,
"echo 'Final network configuration complete.'"
]
}
# Final provisioner - change the password as the last step
provisioner "shell" {
inline = [
"echo 'Changing root password to the specified value...'",
"echo 'root:${var.root_password}' | chpasswd",
"echo 'Password successfully changed.'"
]
}
# Post-processing to convert to desired format
post-processors {
post-processor "shell-local" {
# This will run regardless of the output format
inline = [
"echo 'Build completed successfully. Initial format: qcow2'",
"ls -la output/",
]
}
# VHD format conversion
post-processor "shell-local" {
only = ["qemu.vm_image"]
inline = [
"if [ \"${var.output_format}\" = \"vhd\" ]; then",
" echo 'Converting qcow2 to vhd format...'",
" qemu-img convert -f qcow2 -O vpc output/${var.hostname} output/${var.hostname}.vhd",
" rm output/${var.hostname}",
" echo 'Conversion to VHD completed'",
"fi"
]
}
# VMDK format conversion
post-processor "shell-local" {
only = ["qemu.vm_image"]
inline = [
"if [ \"${var.output_format}\" = \"vmdk\" ]; then",
" echo 'Converting qcow2 to vmdk format...'",
" qemu-img convert -f qcow2 -O vmdk output/${var.hostname} output/${var.hostname}.vmdk",
" rm output/${var.hostname}",
" echo 'Conversion to VMDK completed'",
"fi"
]
}
# OVA format conversion (with OVF creation)
post-processor "shell-local" {
only = ["qemu.vm_image"]
inline = [
"if [ \"${var.output_format}\" = \"ova\" ]; then",
" echo 'Converting qcow2 to vmdk (intermediate step for OVA)...'",
" qemu-img convert -f qcow2 -O vmdk output/${var.hostname} output/${var.hostname}.vmdk",
" echo 'Creating simple OVF descriptor...'",
" cat > output/${var.hostname}.ovf << 'EOT'",
"<Envelope xmlns=\"http://schemas.dmtf.org/ovf/envelope/1\">",
" <References>",
" <File ovf:href=\"${var.hostname}.vmdk\" ovf:id=\"file1\" xmlns:ovf=\"http://schemas.dmtf.org/ovf/envelope/1\"/>",
" </References>",
" <DiskSection>",
" <Info>Virtual disk information</Info>",
" <Disk ovf:capacity=\"50000\" ovf:capacityAllocationUnits=\"byte * 2^20\" ovf:diskId=\"vmdisk1\" ovf:fileRef=\"file1\" xmlns:ovf=\"http://schemas.dmtf.org/ovf/envelope/1\"/>",
" </DiskSection>",
" <VirtualSystem ovf:id=\"vm\" xmlns:ovf=\"http://schemas.dmtf.org/ovf/envelope/1\">",
" <Info>A virtual machine</Info>",
" <Name>${var.hostname}</Name>",
" </VirtualSystem>",
"</Envelope>",
"EOT",
" echo 'Creating OVA package...'",
" cd output && tar -cf ${var.hostname}.ova ${var.hostname}.ovf ${var.hostname}.vmdk",
" rm ${var.hostname} ${var.hostname}.vmdk ${var.hostname}.ovf",
" echo 'Conversion to OVA completed'",
"fi"
]
}
# QCOW2 format (rename for consistency)
post-processor "shell-local" {
only = ["qemu.vm_image"]
inline = [
"if [ \"${var.output_format}\" = \"qcow2\" ]; then",
" echo 'Keeping qcow2 format, renaming file...'",
" mv output/${var.hostname} output/${var.hostname}.qcow2",
" echo 'File renamed to .qcow2 extension'",
"fi"
]
}
}
}
Preseed:
### Localization
d-i debian-installer/language string en
d-i debian-installer/country string US
d-i debian-installer/locale string en_US.UTF-8
d-i console-setup/ask_detect boolean false
d-i keyboard-configuration/xkb-keymap select us
### Clock and Timezone
d-i clock-setup/utc boolean true
d-i time/zone string America/Chicago
### Network (DHCP)
d-i netcfg/choose_interface select auto
d-i netcfg/get_hostname string localhost
d-i netcfg/get_domain string unassigned-domain
### Mirror settings
d-i mirror/country string enter information manually
d-i mirror/http/hostname string http.kali.org
d-i mirror/http/directory string /kali
d-i mirror/http/proxy string
### Disk Partitioning (auto, single partition, no LVM/crypto)
d-i partman-auto/method string regular
d-i partman-auto-lvm/guided_size string max
d-i partman-auto/choose_recipe select atomic # "Atomic" = all files in one partition​:contentReference[oaicite:5]{index=5}
d-i partman-md/confirm boolean true
d-i partman-partitioning/confirm_write_new_label boolean true
d-i partman/choose_partition select finish
d-i partman/confirm boolean true
d-i partman/confirm_nooverwrite boolean true
### Package Selection (Minimal headless install + SSH)
tasksel tasksel/first multiselect standard, kali-linux-headless, openssh-server # Base system "standard" utilities
# Disable automatic upgrades during installation
d-i pkgsel/upgrade select none
d-i pkgsel/update-policy select none
d-i pkgsel/updatedb boolean false
d-i pkgsel/include string openssh-server
d-i pkgsel/install-language-support boolean false
### User Accounts (Root only, no normal user)
d-i passwd/root-login boolean true # Enable root account
d-i passwd/make-user boolean false # Do not create a regular user
d-i passwd/root-password password changeme
d-i passwd/root-password-again password changeme
### The installer will warn about weak passwords. If you are sure you know
### what you're doing and want to override it, uncomment this.
d-i user-setup/allow-password-weak boolean true
d-i user-setup/encrypt-home boolean false
### Boot Loader
d-i grub-installer/only_debian boolean true # Install GRUB to MBR (if only OS)
d-i grub-installer/with_other_os boolean false # Don't search for other OSes
d-i grub-installer/bootdev string /dev/vda
# Force grub installation without asking and prevent media prompts
d-i grub-installer/skip boolean false
d-i grub-installer/force-efi-extra-removable boolean true
# Prevent the installer from ejecting the CD
d-i cdrom-detect/eject boolean false
# Prevent installer from prompting for additional media
d-i apt-setup/cdrom/set-first boolean false
d-i apt-setup/cdrom/set-next boolean false
d-i apt-setup/cdrom/set-failed boolean false
d-i cdrom-detect/eject boolean true
### Avoid that last message about the install being complete
d-i finish-install/reboot_in_progress note
### Enable SSH and set boot device to hard disk
d-i preseed/late_command string \
in-target systemctl enable ssh.service; \
sed -i 's/GRUB_DEFAULT=0/GRUB_DEFAULT=saved/' /target/etc/default/grub; \
in-target update-grub; \
in-target grub-set-default 0; \
eject;
# After installation, make sure to boot from hard disk and not CD-ROM
d-i debian-installer/exit/always_halt boolean false
d-i debian-installer/exit/halt boolean false
d-i debian-installer/exit/poweroff boolean false
Any help would be greatly appreciated as I am no professional when it comes to automating VM build like this and still learning. I patched together what I have by reviewing some guides and asking GPT for assistance when I didn’t understand stuff.
The goal is to have a pipeline that can build up-to-date systems with the tools we require (ill be adding more provisioning steps once it is working to add tools and configure the system better) and then be able to convert it to several output types depending on our needs.