Vagrant - Virtual Machine Environments

Development & Testing with Virtual Machines

Linux
Vagrant
Published

August 8, 2013

Modified

June 21, 2024

Why use Vagrant?

Vagrant 1 …command line utility …manage life cycle of virtual machines:

  • …widely used by software developers and system administrators
  • …configuration of a reproducible environments for development
    • …many software source repositories include a Vagrantfile
    • …shared test environment for collaboration between developers
    • …Vagrant configuration part of project version control
  • …supports various platforms, including Linux & Windows
  • isolation of the test- & development-environments

My collection of Vagrant test environments is available on GitHub:

Providers

Providers interface with different virtual machine monitors (aka. hypervisors).

  • On Linux LibVirt and Virtualbox are the most popular.
  • It is possible to use LibVirt and Virtualbox at the same time.
  • Virtualbox is used by default, Libvirt requires and additional plugin [^3].

The following two section illustrate how to install and configure both.

Libvirt

Vagrant Libivirt provider 2 installation:

# Debian/Ubuntu...
sudo apt install -y libvirt-daemon-system vagrant vagrant-libvirt vagrant-mutate

# Fedora...
sudo dnf install -y @virtualization @vagrant

Enterprise Linux packages 3 are available from HashiCorp:

# make sure to use a recent version
dnf install -y https://releases.hashicorp.com/vagrant/2.4.1/vagrant-2.4.1-1.x86_64.rpm
# install Libvirt support via plugin
dnf install -y make gcc rpm-build ruby ruby-devel ruby-devel zlib-devel libvirt-devel
gem install nokogiri
vagrant plugin install vagrant-libvirt

Configuration…

# add yourself to the `libvirt` group
sudo gpasswd -a $USER libvirt
# …or…
usermod -aG libvirt $user 
# re-login or…
newgrp libvirt

# configure the libvirt service to run with your user ID (here illustrated with ID jdow)
>>> sudo grep -e '^user' -e '^group' /etc/libvirt/qemu.conf
user = "jdow"
group = "jdow"

# ...start services ...set `qemu:///system` environment variable
sudo systemctl enable --now  libvirtd virtnetworkd

Configure the default provider…

  • …use the command option--provider=libvirt
  • …set the VAGRANT_DEFAULT_PROVIDER environment variable
# Vagrantfile
ENV['VAGRANT_DEFAULT_PROVIDER'] = 'libvirt'
Vagrant.configure(2) do |config|
  #....
end
# shell environment variable
export VAGRANT_DEFAULT_PROVIDER=libvirt

Force the systems session in the definition file…

Vagrant.configure("2") do |config|
  config.vm.provider  do |libvirt|
    # Use QEMU system instead of session connection
    libvirt.qemu_use_session = false
  end
end

Enable the system session globally by environment variable…

export LIBVIRT_DEFAULT_URI=qemu:///system

Connect to a Libvirt virtual network 4 5:

# ...
  config.vm.provider  do |libvirt|
    # Use QEMU session instead of system connection
    libvirt.qemu_use_session = true
    # Management network device, default is below
    libvirt.management_network_device = 'virbr0'
  end
#...

VirtualBox

VirtualBox 6 on Fedora…

sudo tee /etc/yum.repos.d/virtualbox.repo <<'EOF'
[virtualbox]
name=Fedora $releasever - $basearch - VirtualBox
baseurl=http://download.virtualbox.org/virtualbox/rpm/fedora/$releasever/$basearch
#baseurl=http://download.virtualbox.org/virtualbox/rpm/fedora/36/$basearch
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://www.virtualbox.org/download/oracle_vbox.asc
EOF

# alternatively it is available from RPM Fusion as well...
sudo dnf install \
      https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm \
      https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm

# ...install dependencies
sudo dnf install -y \
                @development-tools \
                kernel-headers \
                kernel-devel \
                dkms
# VritualBox 7.x not supported yet...
sudo dnf install -y VirtualBox-6.1 @vagrant
sudo usermod -a -G vboxusers ${USER} && newgrp vboxusers

Boxes

  • Virtual machine image templates
  • Dedicated box storage for each user
  • Vagrant boxes are all provider-specific
    • A box must be installed for each provider
    • Can share the same name as long as the providers differ

List commonly used boxes:

init

Life cycle sub-Commands…

  • init creates a new Vagrantfile in the working directory
  • Modify the Vagrantfile to configure the virtual machine instance…
  • …and validate if the syntax is correct
  • up starts the virtual machine instance configured in Vagrantfile
  • status list active virtual machine instances
  • halt stops a virtual machine instance keeping the environment
  • destroy remove a virtual including its environment
pushd $(mktemp -d /tmp/$USER-vagrant-XXXXXX) 
# create a ./Vagrantfile for a specified virtual machine image
vagrant init --minimal rockylinux/9
vagrant up && vagrant ssh
# ... work with the VM
vagrant destroy
popd && rm -rf /tmp/$USER-vagrant*

box

Manage virtual machine images on localhost…

  • before a virtual machine instance can be created with init and up
  • box add downloads a virtual machine images from a remote repository
  • box list lists locally available virtual machine images
  • box remove deletes a virtual machine image from localhost

Examples:

# download a specific version
vagrant box add centos/stream8 --box-version 20210210.0

# download for a specific provider
vagrant box add centos/7 --provider=libvirt

# convert a VirtualBox image
vagrant box add ubuntu/focal64
vagrant mutate ubuntu/focal64 libvirt

Using a URL to a box files …must specify --name option

vagrant box add --name rockylinux/8 \
      http://dl.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-Vagrant-Libvirt.latest.x86_64.box

package

package a currently running VM instance

  • …supported by the vagrant-libvirt provider …using Vagrant package
  • …ensure that you have set the config.ssh.insert_key = false in the original Vagrantfile
vagrant package --output $name

Vagrantfile

  • …configure provisioning of a virtual machines
  • …Ruby syntax …use one file per project …commit to version control
pushd $(mktemp -d /tmp/$USER-vagrant-XXXXXX) 
# prepare a virtual machine for testing
cat > Vagrantfile <<EOF
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
  config.vm.box = "rockylinux/9"
  config.vm.provider :libvirt do |libvirt|
    libvirt.memory = 1024
  end
  config.vm.synced_folder ".", "/vagrant", disabled: true
end
EOF
vagrant up && vagrant ssh
# ... work with the VM
vagrant destroy
popd && rm -rf /tmp/$USER-vagrant*

Hardware

Request specific memory and CPU resources…

Vagrantfile
config.vm.provider  do |libvirt|
libvirt.memory = 1024
libvirt.cpus = 4
libvirt.cpuset = '1-4,^3,6'
libvirt.cputopology  => '2',  => '2',  => '1'
libvirt.cpuaffinitiy 0 => '0-4,^3', 1 => '5', 2 => '6,7'
end
Vagrantfile
config.vm.provider "virtualbox" do |virtualbox|
virtualbox.memory = 1024
virtualbox.cpus = 2
end

Synced Folders

Sync a folder on the host machine to the guest machine…

  • …project root directory synced to /vagrant by default
  • …overwrite/additions with config.vm.synced_folder
Vagrantfile
# ...default
config.vm.synced_folder ".", "/vagrant"

# ...path relative to the project root 
config.vm.synced_folder "src/", "/srv/website",
  "root", "root"

rsync

By default the project directory is shared to /vagrant

# sync data with a running VM instance
vagrant rsync

Configuration…

Vagrantfile
# disabling the default /vagrant share
config.vm.synced_folder ".", "/vagrant", true

# explicitly use rsync ...execlude directories
config.vm.synced_folder ".", "/vagrant", "rsync", ".git/"

rsync-back

Vagrant Rsync-Back Plugin, GitHub – https://github.com/smerrill/vagrant-rsync-back

# install the plugin if required
vagrant plugin install vagrant-rsync-back

# sync data from the VM instance to the host
vagrant rsync-back ...

Network

High-level network configuration 9 directly support by Vagrant:

  • …abstraction that works across multiple providers
  • …such as forwarded ports, public network connection, private networks
  • Networks are automatically configured…
    • …as part of the vagrant up or vagrant reload

Set a hostname 10 …modifies /etc/hosts

Vagrantfile
config.vm.hostname = "myhost.local"

IP-Addresses

Defaults to automatic assignment of IP addresses with DHCP

Vagrantfile
config.vm.network "private_network", "dhcp"

IP-address range for the default network…

>>> virsh net-dumpxml default | grep range
      <range start='192.168.122.2' end='192.168.122.254'/>

Assign a static IP-address to an instance…

Vagrantfile
config.vm.network "private_network", "192.168.122.20"

VirtualBox 11 will only allow IP addresses in 192.168.56.0/21 12 range…

Vagrantfile
config.vm.provider "virtualbox" do |virtualbox|
  virtualbox.network "private_network", "192.168.56.4"
end

Port Forwarding

Port forwarding 13:

Vagrantfile
# change the default port got the SSH connection
config.vm.network "forwarded_port", "ssh", 2200, 22

vagrant port…lists forwarding (on VirtualBox)

Nodes Iterator

Configure multiple nodes with the same configuration:

Vagrantfile
nodes = [ 'alpha', 'beta' ]
(0..(nodes.length - 1)).each do |num|
  name = nodes[num]
  config.vm.define "#{name}" do |node|
    node.vm.hostname = name
    node.vm.box = "centos/7"
    node.vm.network "private_network", "192.168.18.#{10+num}"
  end
end

Use names to operate on a specific box:

vagrant up alpha       # start specific box
vagrant ssh alpha      # login to a box
vagrant destroy alpha  # remove a box 

Nested Virtualization

Prerequisites:

# check if nested virtualization is supported
cat /sys/module/kvm_intel/parameters/nested

# enable nested virtualization after next reboot
sudo tee -a /etc/modprobe.d/kvm.conf <<EOF
options kvm_intel nested=1
options kvm_amd nested=1
EOF

Configure a VM to enable nested vitualization:

Vagrantfile
config.vm.provider  do |libvirt|
  libvirt.memory = 4096
  libvirt.nested = true
  libvirt.cpu_mode = "host-passthrough"
end

Autostart

Make sure to automatically start the network bridge…

virsh net-autostart vagrant-libvirt

…add the autostart option to the VM configuration:

Vagrantfile
config.vm.provider  do |libvirt|
  libvirt.autostart = true
end

global-status

State of all active Vagrant environments…

  • Column id used to control a specific machine
  • Column name is defined by config.vm.define "$name"
# manipulate a specific machine
vagrant [up|halt|destroy] $id

# clean up stale cache...
vagrant global-status --prune
  • Option --prune remove invalid entries from the list
  • Note the status sub-commands target a machine in the working director.

SSH Login

General command usage:

# Interactive login via SSH to a Vagrant instance
vagrant ssh ${name:-}

# Execute a command in the Vagrant instance
vagrant ssh ${name:-} -- ls -l

# Dump the SSH configuration...
vagrant ssh-config > ssh-config
# …helps for integration with other tools

Copy Files

Use scp with the SSH configuration…

# Create a file with the SSH configuration
vagrant ssh-config > ssh-config

# Use SSH option to reference the Vagrant SSH configuration
scp -F ssh-config vagrant@${name:-default}:/bin/bash /tmp

SSH Agent Forwarding

Forwards the local ssh-agent

# Create a file with the SSH configuration
vagrant ssh-config > ssh-config

ssh -AF ssh-config vagrant@${name:-default}

Upload Containers

Copy a container image from the local image cache to a Vagrant instance…

# IP address of the Vagrant instance
ip_address=$(vagrant ssh-config | grep -i hostname | cut -d' ' -f)
name=vagrant   # can be an arbitrary name

# Setup the connection
podman system connection add \
    --identity .vagrant/machines/default/libvirt/private_key \
    $name ssh://vagrant@$ip_address:22

# …eventually check the configuration
podman system connection list

# Upload a container 
podman image scp $container_image $name::

# Clean up …remove the connection configuration
podman system connection remove $name

VNC

Defaults to following configuration…

Vagrantfile
config.vm.provider  do |libvirt|
  libvirt.graphics_type = "vnc"
  libvirt.graphics_port = -1 # set automatically by libvirt
  libvirt.graphics_ip = "127.0.0.1" # aka localhost
end

Find the console port number…

  • …displayed during boot for the VM instance
  • …use virsh vncdisplay $instance to retrieve the port number
# search for Qemu processes, instance names and VNC configuration
ps -AfH \
      | grep qemu \
      | grep -o -e 'guest=[a-z_]*' -e 'vnc 127\.0\.0\.1:[0-9]'

Connect to a VM console with a VNC client…

# install required package
sudo dnf install -y vinagre

# connect to VNC client to a port
vinagre 127.0.0.1:0 2>/dev/null & ; disown

# change the password for the root user
vagrant ssh -c "echo 'root:abc123' | sudo chpasswd"

Provisioning

Execute a provisioning mechanism on the guest machine…

  • …multiple options …shell scripts …configuration management systems
  • Command-line options
    • --provision …force provisioning
    • --no-provision …explicitly not run provisioners
# implicitly runs provisioning
vagrant up 

# explicitly disable provisioning 
vagrant up --no-provision

# provision of a running VM instance
vagrant provision
vagrant reload --provision

Run provisioning on demand by setting run: "never"

# ...disable provisioner execution
config.vm.provision "#{name}", "shell", "never" do |shell|
  shell.inline = "echo hello"
end
# ...run a specific provisioner by name
vagrant provision --provision-with $name

shell & file

shell and file provisioner:

# -*- mode: ruby -*-
# vi: set ft=ruby :

content = 'write this to a file'

script = %q(
mkdir ~/projects
echo "foo'bar" > ~/projects/bar.txt
)

Vagrant.configure("2") do |config|
  config.vm.box = "almalinux/8"

  # run a command as root
  config.vm.provision "shell", true , <<-SHELL
     dnf install -y git vim
  SHELL

  # copy multiple files
  [
    ".gitconfig",
    ".gitignore_global"
  ].each do |file|
     config.vm.provision "file", "~/#{file}", file
  end

  # variable expansion...
  config.vm.provision "shell", %Q(echo "#{content}" > /tmp/content.txt)

end

Ansible

Two provisioning methods…

  • Ansible Local Provisioner
    • …does not require to install Ansible on your Vagrant host
    • …Ansible must be installed on the quest machine
    • executes ansible-playbook directly on the guest machine
  • Ansible Provisioner
    • …allows you to provision the guest using Ansible playbooks…
    • executes ansible-playbook from the Vagrant host
    • …auto-generated inventory in .vagrant/provisioners/ansible/inventory/

Shared options for both…

  • Inventory…
    • groups …inventory groups to be included
    • host_vars …inventory host variables to be included
    • Or …inventory_path ..path to an Ansible inventory
  • Tags…
    • tags …execute matching plays, roles and tasks
    • skip_tags …execute non-matching plays, roles and tasks

Example Vagrantfile

# vi: set ft=ruby :
Vagrant.configure("2") do |config|

  config.vm.define "test_http_server"   # guest name used in Ansible groups

  #config.vm.box = "rockylinux/8"
  config.vm.box = "almalinux/8"

  config.vm.provider  do |libvirt|
    libvirt.memory = 1024
    libvirt.cpus = 2
  end

  config.vm.synced_folder ".", "/vagrant", "rsync", ".git/"

  config.vm.provision "ansible" do |ansible|
    ansible.raw_ssh_args = ['-o UserKnownHostsFile=/dev/null']
    ansible.host_key_checking = false
    #ansible.verbose = 'vvv'
    ansible.playbook = "main.yml"
    ansible.groups = { "http_server" => "test_http_server" }
    ansible.host_vars = { "http_test" => { "http_port" => 8080 } }
  end

end

…using a playbook main.yml

---
- hosts: http_server
  become: true
  tasks:
    - name: Install dependency packages
      ansible.builtin.package:
        name: httpd
        state: installed
    # ...

Debugging

Enable logging with

  • …the VAGRANT_LOG environmental variable
  • …or by using the --debug option
VAGRANT_LOG=info vagrant up

# or
vagrant up --debug
vagrant up --debug &> | tee vagrant.log