Andrew Mercer
on this page

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.