Provisioning a Rocky 9 KVM Instance with Cobbler¶
Architecture¶
┌─────────────────┐ ┌─────────────────────────────────────────┐
│ Admin Machine │ │ Cobbler Server │
│ │─SSH────▶│ cobblerd :25151 (localhost only) │
│ ansible/ │ │ httpd :80 (kickstart/repos) │
│ cobbler CLI │ │ tftpd :69 (PXE boot files) │
└─────────────────┘ │ dhcpd :67 (IP assignment) │
└──────────────┬──────────────────────────┘
│ PXE/TFTP/HTTP
▼
┌─────────────────────────────────────────┐
│ KVM Hypervisor │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Rocky 9 VM │ │
│ │ 1. BIOS/UEFI → PXE boot │ │
│ │ 2. Gets IP from cobbler DHCP │ │
│ │ 3. Downloads pxelinux.0 │ │
│ │ 4. Downloads kickstart │ │
│ │ 5. Installs Rocky 9 │ │
│ │ 6. Reboots to local disk │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
The KVM VM needs to be on the same network as Cobbler (or a routed network where DHCP/TFTP can reach it). The hypervisor itself never talks to cobblerd — only the VM does, and only over standard PXE protocols.
Prerequisites¶
On the Cobbler server¶
# Verify cobbler is running and synced
sudo cobbler version
sudo cobbler check
sudo systemctl status cobblerd dhcpd tftp httpd
# Verify TFTP is serving files
ls /var/lib/tftpboot/
On the KVM hypervisor¶
# Install required tools
sudo dnf install -y qemu-kvm libvirt virt-install libvirt-client
# Start libvirt
sudo systemctl enable --now libvirtd
# Verify KVM is available
sudo virsh version
sudo virt-host-validate
Step 1 — Import a Rocky 9 Distro into Cobbler¶
# On the Cobbler server
# Download Rocky 9 ISO (if not already present)
sudo wget -P /var/lib/libvirt/images/ \
https://dl.rockylinux.org/pub/rocky/9/isos/x86_64/Rocky-9-latest-x86_64-minimal.iso
# Mount the ISO
sudo mkdir -p /mnt/cobbler/rocky9
sudo mount -o loop,ro \
/var/lib/libvirt/images/Rocky-9-latest-x86_64-minimal.iso \
/mnt/cobbler/rocky9
# Import into Cobbler
sudo cobbler import \
--name=rocky9 \
--arch=x86_64 \
--path=/mnt/cobbler/rocky9
# Verify distro and profile were created
sudo cobbler distro list
sudo cobbler profile list
Step 2 — Create a Kickstart Template¶
sudo tee /var/lib/cobbler/templates/rocky9-kvm.ks << 'ENDOFFILE'
#version=ROCKY9
url --url=http://$next_server_v4/cblr/links/$distro_name
lang en_US.UTF-8
keyboard us
timezone UTC --utc
rootpw --iscrypted $default_password_crypted
bootloader --location=mbr --boot-drive=vda --append="console=tty0 console=ttyS0,115200"
zerombr
clearpart --all --initlabel --drives=vda
autopart --type=lvm
selinux --enforcing
firewall --enabled --ssh
firstboot --disable
reboot
%packages --ignoremissing
@^minimal-environment
curl
rsync
tmux
%end
%post --log=/root/ks-post.log
#!/bin/bash
# Create admin user
useradd -m -G wheel admin
echo "%wheel ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/wheel
chmod 0440 /etc/sudoers.d/wheel
# Add SSH key
mkdir -p /home/admin/.ssh
chmod 700 /home/admin/.ssh
cat >> /home/admin/.ssh/authorized_keys << 'SSHKEY'
$ssh_key
SSHKEY
chmod 600 /home/admin/.ssh/authorized_keys
chown -R admin:admin /home/admin/.ssh
# Set hostname
hostnamectl set-hostname $hostname
# Update system
dnf update -y
dnf clean all
%end
ENDOFFILE
Step 3 — Create a Cobbler Profile¶
sudo cobbler profile add \
--name=rocky9-kvm \
--distro=rocky9-x86_64 \
--autoinstall=/var/lib/cobbler/templates/rocky9-kvm.ks \
--kernel-options="console=tty0 console=ttyS0,115200" \
--autoinstall-meta="ssh_key=your-ssh-public-key-here"
Step 4 — Register the VM in Cobbler Before Creating It¶
You need the MAC address before the VM exists. Either pre-generate one or let libvirt generate it and use it in Cobbler.
# Generate a random MAC address
MAC=$(python3 -c "
import random
mac = [0x52, 0x54, 0x00,
random.randint(0x00, 0xff),
random.randint(0x00, 0xff),
random.randint(0x00, 0xff)]
print(':'.join(map(lambda x: '%02x' % x, mac)))
")
echo "MAC: $MAC"
# Register in Cobbler
sudo cobbler system add \
--name=rocky9-vm01 \
--profile=rocky9-kvm \
--hostname=rocky9-vm01.example.com \
--mac=$MAC \
--ip-address=192.168.2.51 \
--netmask=255.255.255.0 \
--gateway=192.168.2.1 \
--interface=enp1s0 \
--static=true \
--netboot-enabled=true \
--autoinstall-meta="ssh_key=your-ssh-public-key hostname=rocky9-vm01.example.com"
# Apply changes
sudo cobbler sync
Step 5 — Create the KVM VM with the Same MAC¶
# On the KVM hypervisor
# Use the exact MAC you registered in Cobbler
MAC="52:54:00:xx:xx:xx" # replace with your generated MAC
virt-install \
--name=rocky9-vm01 \
--ram=2048 \
--vcpus=2 \
--disk path=/var/lib/libvirt/images/rocky9-vm01.qcow2,size=20,format=qcow2 \
--network network=default,mac=$MAC \
--pxe \
--os-variant=rocky9 \
--boot network,hd \
--graphics none \
--console pty,target_type=serial \
--noautoconsole
# Watch the install progress
sudo virsh console rocky9-vm01
# (Ctrl+] to exit console)
Step 6 — Monitor the Installation¶
# On Cobbler server — watch active installs
sudo cobbler status
# Watch DHCP leases
sudo tail -f /var/log/messages | grep dhcp
# Watch kickstart being served
sudo tail -f /var/log/httpd/access_log | grep rocky9-vm01
# Watch cobbler log
sudo tail -f /var/log/cobbler/cobbler.log
Step 7 — Post-Install: Disable PXE Boot¶
After installation completes the VM reboots. If pxe_just_once is enabled
in your Cobbler settings (it is), Cobbler automatically disables netboot after
the first successful boot. If not, disable it manually:
sudo cobbler system edit \
--name=rocky9-vm01 \
--netboot-enabled=false
sudo cobbler sync
Automating with Ansible¶
Put this in a playbook to fully automate VM provisioning:
---
- name: Provision Rocky 9 KVM VM via Cobbler
hosts: localhost
vars:
vm_name: rocky9-vm01
vm_ip: 192.168.2.51
vm_mac: "52:54:00:ab:cd:ef"
vm_hostname: rocky9-vm01.example.com
cobbler_host: cobbler.umoswg.online
hypervisor_host: kvm.umoswg.online
tasks:
- name: Register system in Cobbler
delegate_to: "{{ cobbler_host }}"
become: true
ansible.builtin.command:
cmd: >
cobbler system add
--name={{ vm_name }}
--profile=rocky9-kvm
--hostname={{ vm_hostname }}
--mac={{ vm_mac }}
--ip-address={{ vm_ip }}
--netmask=255.255.255.0
--gateway=192.168.2.1
--interface=enp1s0
--static=true
--netboot-enabled=true
register: cobbler_add
failed_when:
- cobbler_add.rc != 0
- "'already exists' not in cobbler_add.stderr"
- name: Sync Cobbler
delegate_to: "{{ cobbler_host }}"
become: true
ansible.builtin.command: cobbler sync
- name: Create VM on hypervisor
delegate_to: "{{ hypervisor_host }}"
become: true
ansible.builtin.command:
cmd: >
virt-install
--name={{ vm_name }}
--ram=2048
--vcpus=2
--disk path=/var/lib/libvirt/images/{{ vm_name }}.qcow2,size=20,format=qcow2
--network network=default,mac={{ vm_mac }}
--pxe
--os-variant=rocky9
--boot network,hd
--graphics none
--console pty,target_type=serial
--noautoconsole
creates: /var/lib/libvirt/images/{{ vm_name }}.qcow2
- name: Wait for VM to finish installing and become reachable
ansible.builtin.wait_for:
host: "{{ vm_ip }}"
port: 22
timeout: 900
delay: 30
- name: Disable PXE boot after install
delegate_to: "{{ cobbler_host }}"
become: true
ansible.builtin.command: >
cobbler system edit
--name={{ vm_name }}
--netboot-enabled=false
- name: Final Cobbler sync
delegate_to: "{{ cobbler_host }}"
become: true
ansible.builtin.command: cobbler sync
Troubleshooting¶
VM not getting a DHCP lease¶
# Check dhcpd is running and config is correct
sudo systemctl status dhcpd
sudo cat /etc/dhcp/dhcpd.conf
# Verify MAC is registered correctly
sudo cobbler system report --name=rocky9-vm01 | grep mac
VM gets IP but doesn't PXE boot¶
# Check TFTP is serving files
sudo systemctl status tftp
ls /var/lib/tftpboot/pxelinux.cfg/
# File should exist named after MAC in hex: 01-52-54-00-ab-cd-ef
VM PXE boots but kickstart fails¶
# Check kickstart is being served
curl http://192.168.2.12/cblr/svc/op/autoinstall/system/rocky9-vm01
# Validate kickstart syntax
sudo ksvalidator /var/lib/cobbler/templates/rocky9-kvm.ks
VM installs but reboots back into PXE¶
# pxe_just_once is not working — disable netboot manually
sudo cobbler system edit --name=rocky9-vm01 --netboot-enabled=false
sudo cobbler sync
Network Requirements¶
| Port | Protocol | Direction | Purpose |
|---|---|---|---|
| 67 | UDP | Cobbler → VM | DHCP lease |
| 68 | UDP | VM → Cobbler | DHCP request |
| 69 | UDP | Cobbler → VM | TFTP (PXE files) |
| 80 | TCP | VM → Cobbler | Kickstart + packages |
| 25151 | TCP | localhost only | Cobbler XMLRPC API |
The VM and Cobbler must be on the same L2 network segment for DHCP/TFTP to
work without a DHCP relay agent. If they're on different subnets, configure
ip helper-address (Cisco) or dhcp-relay on the router pointing at the
Cobbler server IP.
Cobbler is not meant to run on a hypervisor — it's designed to run on a dedicated bare-metal or VM server that sits on the provisioning network. It's a network boot infrastructure service, not a hypervisor management tool.
The relationship between Cobbler and hypervisors looks like this:
┌─────────────────┐
│ Cobbler Server │ ← dedicated VM or bare metal, one per site/network
│ (your setup) │ runs: dhcpd, tftpd, httpd, cobblerd
└────────┬────────┘
│ PXE/DHCP/TFTP/HTTP (network protocols)
▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Hypervisor 1 │ │ Hypervisor 2 │ │ Bare Metal 1 │
│ KVM host │ │ KVM host │ │ physical server │
│ │ │ │ │ │
│ ┌───────────┐ │ │ ┌───────────┐ │ │ │
│ │ VM (PXE) │ │ │ │ VM (PXE) │ │ │ │
│ └───────────┘ │ │ └───────────┘ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Cobbler handles any machine that PXE boots on its network regardless of whether it's a VM on hypervisor 1, hypervisor 2, or a physical server. It doesn't know or care what the hardware is — it just sees a MAC address making a DHCP request and serves the appropriate kickstart.
Multiple hypervisors are totally fine and are the normal use case. You register each VM in Cobbler with its MAC before booting it, sync, then boot the VM — Cobbler serves it. The hypervisors themselves are just network participants.
The one limitation is the L2 network boundary. Cobbler's DHCP and TFTP only work on the same network segment by default. If your hypervisors are on different subnets you have two options:
One Cobbler per network segment:
Site A network → Cobbler A
Site B network → Cobbler B
Or a single Cobbler with DHCP relay on the routers:
Hypervisor subnet → router ip helper-address → Cobbler IP
For your setup with a single lab network (192.168.2.0/24), one Cobbler handles everything on that segment regardless of how many hypervisors or physical machines you have. Where Cobbler ends and something else begins is when you start asking "what hypervisor should this VM run on" or "how much RAM should it get" — that's where something like Foreman+Katello or OpenStack takes over and calls Cobbler as a sub-component to handle the PXE/kickstart piece.