Ansible: Deploy VMs With PXE and Kickstart

Table of Contents

I’ve completed my basic workflow for for generating a PXE boot environment and deploying VMs. This builds on a few of my previous posts, so I will be omitting some steps.

See the relevant git commit here.

Overview

The workflow is a collection of roles. Currently, I’m calling them on an ad hoc basis, but they’re designed to ultimately be called from a single playbook.

local-repo

This is the role for configuring my local repository, which included a preparatory script on the container host. See the original post here.

I’ve since corrected my reposync cron job, adding a separate reposync.conf file.

tftp_server

Overview:

roles/tftp-server/
├── files
│   ├── boot
│   │   ├── centos7-1804-initrd.img
│   │   └── centos7-1804-vmlinuz
│   ├── chain.c32
│   ├── mboot.c32
│   ├── memdisk
│   ├── menu.c32
│   └── pxelinux.0
└── main.yml

main.yml

This file installs necessary packages, creates the directory structure, and copies files over. I could have sourced the files remotely, which is on the to do list.

---
- name: configure tftp server
  hosts: 'repo.lan.nathancurry.com'

  vars:
    packages: [ 'tftp-server' ]

  tasks:
  - name: Install packages
    yum:
      name: '{{ packages }}'
      state: 'present'
      update_cache: 'yes'

  - name: create folder
    file:
      path: /var/lib/tftpboot/boot
      state: directory
      mode: 0755

  - name: copy files
    copy:
      src: '{{ item }}'
      dest: /var/lib/tftpboot
      mode: 0755
    with_fileglob: files/*

  - name: copy boot images
    copy:
      src: '{{ item }}'
      dest: /var/lib/tftpboot/boot
      mode: 0755
    with_fileglob: files/boot/*

  - name: create folder
    file:
      path: /var/lib/tftpboot/pxelinux.cfg/hosts
      state: directory
      mode: 0755

Kickstart

This generates kickstart files (see here), as well as the associated PXE boot files.

I had previously cannibalized sergev/ansible-os-autoinstall for a previous project generating PXE files, and I cannibalized that previous project for this project.

Overview

Here is the full overview. I only cover what’s new below.

roles/kickstart/
├── tasks
│   └── main.yml            # updated
├── templates
│   ├── host_default.j2     # NEW
│   ├── kickstart.j2
│   └── pxe.j2              # NEW
└── vars
    └── main.yml            # updated

vars/main.yml

I added a few path variables. I didn’t have to do much, since I’m pulling a lot from hostvars.

repo_url: 'http://repo.lan.nathancurry.com/repo/centos7/base'
ks_url: 'http://repo.lan.nathancurry.com/ks'

tasks/main.yml

The PXE generation is essentially a one-liner template operation

  - name: Include secrets
    include_vars: ~/0/vault/secrets.yml

  - name: Generate unattended install
    template:
      src: templates/kickstart.j2
      dest: "{{ ks_dir }}/{{ hostvars[item].inventory_hostname_short }}.ks"
    with_items: "{{ groups['all'] | difference(groups['proxmox']) }}"

  - name: Generate boot files
    template:
      src: templates/pxe.j2
      dest: "{{ menu_dir }}/01-{{ hostvars[item]['mac_address']|regex_replace(':','-')|lower }}"
    with_items: "{{ groups['all'] }}"
    when: hostvars[item]['mac_address'] is defined

templates

I could have made this a little more modular, but I don’t have need for it now, and I always like to see if things work before I add a million variables:

###  pxe.j2  ###
{% if hostvars[item]['mac_address'] is defined %}
{% include "host_default.j2" %}
{% endif %}
LABEL {{ item }}
MENU LABEL {{ item }}
  ipappend 2
  kernel /boot/centos7-1804-vmlinuz
  append initrd=/boot/centos7-1804-initrd.img repo={{ repo_url }}/ ks={{ ks_url }}/{{ hostvars[item].inventory_hostname_short }}.ks

###  host_default.j2  ###

VM Deployment

This is currently part of my proxmox role, which needs some work. There are some promising projects on github which I will incorporate when I have some time.

vars/main.yml

I had filled out most of these before. A few of the variables passed to proxmox_kvm are different from those passed to proxmox, with the only real differences being the hard drive and network adapter.

tasks/deploy_vms.yml

This is likewise similar to the container deployment. The big difference is I enable the tftp server before deployment, and disable it after, to avoid errant catastrophe.

---
- name: 'deploy VMs'

  hosts: 'gold.lan.nathancurry.com'
  gather_facts: false

  # Set which host groups to deploy as containers and as VMs
  vars:
    deploy_vms: "{{ groups['ipaservers'] }}"

  handlers:
  - name: 'sleep'
    pause:
      seconds: 10

  tasks:
  - name: Load relevant secrets
    include_vars: "~/0/vault/proxmox.yml"
    no_log: false

  - name: 'include vars'
    include_vars: '../vars/main.yml'

  - name: 'enable TFTP server'
    service:
      name: 'tftp'
      state: 'started'
    delegate_to: '{{ tftp_server }}'

  - name: 'create vms from clone'
    proxmox_kvm:
      api_user: "{{ api_user }}"
      api_password: "{{ api_password }}"
      api_host: "{{ api_host }}"
      vmid: '{{ hostvars[item].vmid }}'
      node: '{{ hostvars[item].proxmox_node }}'
      name: '{{ hostvars[item].inventory_hostname }}'
      cores: '{{ hostvars[item].cores | default(defaults.cores) }}'
      net: "{{ '{\"net0\":\"virtio=' + hostvars[item].mac_address + ',bridge=vmbr0\"}' | default(defaults.netif) }}"
      virtio: '{{ hostvars[item].virtio | default(defaults.virtio) }}'
      vga: 'qxl'
      memory: '{{ hostvars[item].memory | default(defaults.memory.vm) }}'
      storage: '{{ hostvars[item].storage | default(defaults.storage.gluster) }}'
      onboot: '{{ hostvars[item].onboot | default(defaults.onboot) }}'
      state: 'present'
    with_items: "{{ deploy_vms }}"
    loop_control:
      pause: 5
    notify:
      - 'sleep'
    register: 'created_vms_pve'


  - meta: 'flush_handlers'
    when: 'created_vms_pve.changed'

  - name: 'start VMs'
    proxmox_kvm:
      api_user: "{{ api_user }}"
      api_password: "{{ api_password }}"
      api_host: "{{ api_host }}"
      node: "{{ item['invocation']['module_args']['node'] }}"
      name: "{{ item['item'] }}"
      state: 'started'
    with_items: "{{ created_vms_pve.results }}"
    notify:
      - 'sleep'

  - meta: 'flush_handlers'
    when: 'created_vms_pve.changed'

  - name: 'disable TFTP server'
    service:
      name: 'tftp'
      state: 'started'
    delegate_to: '{{ tftp_server }}'

Conclusion

As usual, there are refinements I will make on the following pass, but for now, I’m able to generate all the necessary information with a few entries in my hostvars files.

See the relevant git commit here.