Wednesday, November 5, 2014

How To Use Vagrant To Create Small Virtual Test Lab on a Linux / OS X / MS-Windows

Vagrant is a multi-platform command line tool for creating lightweight, reproducible and portable virtual environments. Vagrant acts as a glue layer between different virtualization solutions (Software, hardware PaaS and IaaS) and different configuration management utilities (Puppet, Chef, etc'). Vagrant was started back at 2010 by Mitchell Hashimoto as a side project and later became one of the first products of HashiCorp - the company Mitchell founded.
While officially described as a tool for setting up development environments, Vagrant can be used for a lot of other purposes by non developers as well:
  • Creating demo labs
  • Testing configuration management tools
  • Speeding up the work with non multi-platform tools such as Docker
In this tutorial I'll show how can we take Vagrant as use it to create small virtual test lab which we will be able to pass to our colleagues.

Vagrant installation

Vagrant installation packages are available for OS X, Windows and Linux (deb and rpm format). Installation is simple "Next, Next, Next, Done" process and doesn't require any special user interaction.

A note for Linux users

I strongly suggest that you will install Vagrant from its website and not by using your package manager. A lot of times the official repositories contains very old versions of Vagrant. In this tutorial I will use Vagrant 1.6.5.
Since Vagrant is not a virtualization software by itself, it relies on 3rd party providers for doing the virtualization part. For this tutorial I'll assume you have installed Oracle's VirtualBox. VirtualBox is a free multi-platform virtualization software which is supported by Vagrant out of the box.

Vagrant environment variables

By default Vagrant comes with sane default values and you don't need to change any to make it work. However, I will point out number of important ones you should be familiar with:
  • VAGRANT_DOTFILE_PATH> - This is the directory where Vagrant stores VM-specific state. By default it's '.vagrant' in the same directory as your
  • VAGRANT_HOME - This is the directory where Vagrant stores its global state. It can get quiet large since it will contain all the base boxes.
    By default this is '~/.vagrant.d'
  • VAGRANT_LOG - Verbosity level of Vagrant log messages. By default is 'info', change this to 'debug' in case you really want to know what's going
    on or want to submit bug report with log for example.
  • VAGRANT_DEFAULT_PROVIDER - By default Vagrant is using Virtualbox as its virtualization provider unless requested otherwise. If you using other
    provider most of the time, it would make sense to change the default.

Understanding Vagrant terminology

A quick terminology break down:


Providers are the components that enables Vagrant to use different virtualization solutions. Vagrant supports number of software hypervisors out of the box - VirtualBox and Hyper-V. Vagrant also allows adding additional providers via its plugins mechanism. For example you can add support for VMWare products as well as IaaS providers such as AWS.


Provisioners allow Vagrant to bring machine into desired state by using simple shell scripts or different configuration management tools such as Puppet or Chef. Provisioning in Vagrant usually happens after machine initialization but can be initiated on demand.
Configuration Management (CM) vs shell scripts: There's nothing wrong to use Vagrant with simple shell script instead of a full blown CM system. In fact in this tutorial I will use shell as well in order to not over complicate the tutorial. However CM system like Puppet or Chef has certain principles behind them which can help you with provisioning of your environment. For example one of the main principles of CMs is Idempotence which is the ability to apply certain action number of times but be sure the result won't change after the initial result. In environment you find provisioning to be helpful even after the initial provision, you should probably consider using CM instead of shell script.

Vagrant boxes

Boxes are vagrant packages that allows bundling provider specific machine data (such as vmdk disk images). These packages can be imported on any machine that runs vagrant.

Shared/Synced folders

Vagrant allows sharing or syncing of folders from host machine to guest machine. This allows you to edit your files locally and see the changes in the guest machine. Sharing of folders depends on your provider, for example VirtualBox provides its shared folder mechanism. Vagrant also allows syncing by using tools such as rsync or network file share using NFS.

Port forwarding

Some providers such as VirtualBox allow running VMs in NAT network mode. In this mode the VM sits in its own private address space which is not accessible from the host machine. In this case port forwarding allows creating forwarding rules that will forward traffic from local port to the port of the virtual machine.

Vagrant plugins

Vagrant provides extendability via its plugins API. This creates possibilities to add support for new provisioners, providers and other utilities. For a list of currently available Vagrant plugins take a look here.

Creating your first Vagrantfile

Vagrant initial goal was to speed up the creation of virtual environments for development. However, virtual environments has a broader use case. One of the common uses of Virtualization is to set up practice labs, and I will demonstrate how we can do it by using Vagrant.

Our (not so) imaginary scenario

Your company using Nagios as its monitoring solution and your team is responsible for maintaining it. You have new personal on your team which isn't familiar with Nagios and you'll like to do few training sessions for them. For the training session you want each member to have its own sandbox environment to play with.
One options is to provide your students virtual machine disk images that they can import and run. This options has its advantages:
  • Almost no time to first boot.
  • Relatively easy process (if you are familiar with Virtualization).
  • Access to the internet is not needed (the image can be saved on USB, local file server, etc).
And some disadvantages too:
  • Users usually not aware how the environment inside the virtual machine was created.
  • Can be time consuming to update image with new components.
  • Might need to provide additional instructions to users about how to set up the virtual environment (Assign more CPUs and RAM, network settings, etc).
To overcome some of the disadvantages above lets try to setup our lab environment by using Vagrant and VirtualBox.
Note: I'm not suggesting in any way that using Vagrant doesn't come with its own set of disadvantages.

The Vagrant configuration

Before we start you should download the tarball with configurations files from here. Bellow is the main config file (Vagrantfile). If you don't have any programing/scripting experience it may seem a bit confusing at first:
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| = "ubuntu/trusty64"
  config.vm.provider "virtualbox" do |vb|
    vb.cpus = 2
  end "private_network", type: "dhcp"
  config.hostmanager.enabled = true
  config.hostmanager.ip_resolver = proc do |vm, resolving_vm|
      `VBoxManage guestproperty get #{} "/VirtualBox/GuestInfo/Net/1/V4/IP"`.split()[1]
  config.vm.define :server do |srv|
    srv.vm.hostname = "nagios-server"
    srv.vm.synced_folder "server/", "/usr/local/nagios/etc", create: true "forwarded_port", guest: 80, host: 8080
    srv.vm.provision "shell", path: "server-provision"
  config.vm.define :client do |cl|
    cl.vm.hostname = "nagios-client"
    cl.vm.synced_folder "client/", "/usr/local/nagios/etc", create: true
    cl.vm.provision "shell", path: "client-provision"
Vagrantfile is really a ruby file (or more specifically a Ruby DSL). While this makes the configuration look a bit weird for the average joe, it's nothing to be worried about and can be pretty powerful feature when needed.

Let's break this block by block:

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  #this is the outer block
The outer block creates the main configuration object. Think of it as creation of an object (named 'config') that will hold all our settings. VAGRANTFILE_API_VERSION is a variable storing the value "2". This represent the version number we want out config object to work with. This is mainly done for backwards compatibility between recent versions of Vagrant and older ones. If you are not sure about it, just leave it as is. = "ubuntu/trusty64"
Here we use the config object to set our Vagrant base box to ubuntu/trusty64. More about where this box comes from later in the tutorial.
config.vm.provider "virtualbox" do |vb|
  vb.cpus = 2
Here we create provider specific config block. In our case we ask that if machines are being run with VirtualBox provider, then each machine should receive 2 vCPUs by default. Each provider expose different set of options, so provider specific settings needs to be in separate blocks. "private_network", type: "dhcp"
Network config is something that gets special treatment in Vagrant. While it is possible to configure Network via the provider block configuration, Vagrant tries to abstract as much as possible of the (usually) tedious network configuration into few high level settings. In our case, we ask Vagrant to configure private_network networking and assign IP addresses via DHCP. private_network means that each machine will get private ip address from the private address space and machines will be able to communicate with one to another via these addresses. I choose to use DHCP instead of static IPs because I would like my Vagrantfile to be bit more portable. Choosing static IPs could mean collisions with local networks at home or work.
config.hostmanager.enabled = true
config.hostmanager.ip_resolver = proc do |vm, resolving_vm|
    `VBoxManage guestproperty get #{} "/VirtualBox/GuestInfo/Net/1/V4/IP"`.split()[1]
Hostmanager config namespace is not a part of core Vagrant install. It is added by using the Hostmanager Vagrat plugin. This plugin allows a dynamic edit of host file on host and guest machines. It allows us to skip hardcoding IPs into our configuration and to work with hostnames instead of IPs. So later on when actually working with Nagios I can refer to server as nagios-server instead of its IP.
Note for Windows users: In Windows, VBoxManage.exe is not in the %PATH%. You'll want to add VirtualBox's directory (usually something like C:\Program Files\Oracle\VirtualBox) into the PATH variable for this to work.
config.vm.define :server do |srv|
  srv.vm.hostname = "nagios-server"
  srv.vm.synced_folder "server/", "/usr/local/nagios/etc", create: true "forwarded_port", guest: 80, host: 8080
  srv.vm.provision "shell", path: "server-provision"
config.vm.define :client do |cl|
  cl.vm.hostname = "nagios-client"
  cl.vm.synced_folder "client/", "/usr/local/nagios/etc", create: true
  cl.vm.provision "shell", path: "client-provision"
These are two blocks doing the same thing - defining a virtual machine. First line of each block defines machine hostname. Second line of each block defines a synced folder between host and guest machine. These are the folder of Nagios configurations which will allow us to quickly reconfigure Nagios server and Nagios client without the need to SSH into the machine. Third line of the first block defines a port forwarding. This is done mainly for example only since we use private_network and can access machine by IP instead. Last line of each block describes what provisioner we want to use. In our case we would use the simple shell provisioner. Vagrant will copy the files specified in path and execute them after machine is up and running. The server-provision & client-provision files exists inside the tarball. I will not review them in this tutorial.

Run Vagrant

In this section we will initialize our Vagrant environment.

Vagrant CLI crash course

Vagrant CLI is quiet straightforward. To see the list of all the common commands type the following in your terminal:
vagrant --help
Sample outputs:
Usage: vagrant [options]  []
    -v, --version                    Print the version and exit.
    -h, --help                       Print this help.
Common commands:
     box             manages boxes: installation, removal, etc.
     connect         connect to a remotely shared Vagrant environment
     destroy         stops and deletes all traces of the vagrant machine
     global-status   outputs status Vagrant environments for this user
     halt            stops the vagrant machine
     help            shows the help for a subcommand
     init            initializes a new Vagrant environment by creating a Vagrantfile
     login           log in to Vagrant Cloud
     package         packages a running vagrant environment into a box
     plugin          manages plugins: install, uninstall, update, etc.
     provision       provisions the vagrant machine
     rdp             connects to machine via RDP
     reload          restarts vagrant machine, loads new Vagrantfile configuration
     resume          resume a suspended vagrant machine
     share           share your Vagrant environment with anyone in the world
     ssh             connects to machine via SSH
     ssh-config      outputs OpenSSH valid configuration to connect to the machine
     status          outputs status of the vagrant machine
     suspend         suspends the machine
     up              starts and provisions the vagrant environment
     version         prints current and latest Vagrant version
For help on any individual command run `vagrant COMMAND -h`
Additional subcommands are available, but are either more advanced
or not commonly used. To see all subcommands, run the command
`vagrant list-commands`.
Primary commands you want to focus on at first:
  1. init - Create Vagrantfile in current directory.
  2. up - If environment never been created before, create it and run provision on machines. If machines are stopped (halt), just start them without provision
  3. halt - Shutdown environment. Equivalent to powering off machines.
  4. provision - (re)Run provision scripts as defined in Vagrantfile.
  5. destroy - Stop and delete environment.
  6. reload - Will restart environment and apply new setting from Vagrantfile without destroying.
  7. ssh - SSH into machine.
  8. status - Get environment status to see whether machines are running or not.
The commands above effects the states of the virtual machines and the general flow can be described in the following diagram:
Fig.01: Vagrant state
Fig.01: Vagrant state
Vagrant commands are executed on all machines in the environment. For example in our case, running vagrant up will cause vagrant to start and provision two virtual machines (server & client). If we wanted to create only one of them, we could do vagrant up server and only the server virtual machine would be initialized by vagrant.

Installing plugins

Before we can start our environment, lets install the hostmanager plugin:
$ vagrant plugin install vagrant-hostmanager
Installing the 'vagrant-hostmanager' plugin. This can take a few minutes...
Installed the plugin 'vagrant-hostmanager (1.5.0)'!
You can list all installed vagrant plugins with the following command:
$ vagrant plugin list

Vagrant up

Now we are ready to initialize our environment:
$ vagrant up
Bringing machine 'server' up with 'virtualbox' provider...
Bringing machine 'client' up with 'virtualbox' provider...
1==> server: Box 'ubuntu/trusty64' could not be found. Attempting to find and install...
    server: Box Provider: virtualbox
    server: Box Version: >= 0
==> server: Loading metadata for box 'ubuntu/trusty64'
    server: URL:
==> server: Adding box 'ubuntu/trusty64' (v14.04) for provider: virtualbox
1    server: Downloading:
1==> server: Successfully added box 'ubuntu/trusty64' (v14.04) for 'virtualbox'!
2==> server: Importing base box 'ubuntu/trusty64'...
==> server: Matching MAC address for NAT networking...
==> server: Checking if box 'ubuntu/trusty64' is up to date...
3==> server: Setting the name of the VM: nagioslab_server_1410009838995_55673
==> server: Clearing any previously set forwarded ports...
==> server: Clearing any previously set network interfaces...
==> server: Preparing network interfaces based on configuration...
    server: Adapter 1: nat
    server: Adapter 2: hostonly
4==> server: Forwarding ports...
    server: 80 => 8080 (adapter 1)
    server: 22 => 2222 (adapter 1)
==> server: Running 'pre-boot' VM customizations...
==> server: Booting VM...
==> server: Waiting for machine to boot. This may take a few minutes...
    server: SSH address:
    server: SSH username: vagrant
    server: SSH auth method: private key
    server: Warning: Connection timeout. Retrying...
    server: Warning: Remote connection disconnect. Retrying...
5==> server: Machine booted and ready!
==> server: Checking for guest additions in VM...
==> server: Setting hostname...
==> server: Configuring and enabling network interfaces...
5==> server: Mounting shared folders...
    server: /vagrant => /Users/michael/VagrantLab/nagioslab
    server: /usr/local/nagios/etc => /Users/michael/VagrantLab/nagioslab/server
5==> server: Updating /etc/hosts file on active guest machines...
5==> server: Running provisioner: shell...
    server: Running: /var/folders/fz/81ddjj6s2bx9s7jg9z7347pr0000gn/T/vagrant-shell20140906-90949-1a8ntti
==> server: stdin: is not a tty
==> server: Ign trusty InRelease
... provisioning log ...
==> server: Starting nagios:
==> server:  done.
==> client: Box 'ubuntu/trusty64' could not be found. Attempting to find and install...
    client: Box Provider: virtualbox
    client: Box Version: >= 0
==> client: Loading metadata for box 'ubuntu/trusty64'
    client: URL:
6==> client: Adding box 'ubuntu/trusty64' (v14.04) for provider: virtualbox
==> client: Importing base box 'ubuntu/trusty64'...
... similar log to server part
==> client: Starting nagios remote plugin daemon: nrpe
==> client: .
Some key points in log:
  • 1 At first Vagrant looks for a base box (ubuntu/trusty64), once it doesn't find it, it will download it from (unless configured otherwise).
  • 2 After download is finished, it will import the image into Virtualbox.
  • 3 Vagrant will rename the machine in Virtualbox to the name specified in the log. If you open Virtualbox, you'll find a running machine with this name.
  • 4 Port forwarding takes place.
  • 5 After machine is running and ready Vagrant will mount shared folders, edit hosts file (hostmanager plugin) and execute provision shell script that was copied to the machine.
  • 6 Notice that the second machine won't re-download the box before importing it since it was saved to Vagrant's cache in the first time.
The whole process took between 10 and 15 minutes on my computer including downloading of the base box. So in case I would like to re-create the whole environment from scratch again, it's going to take less then 10 minutes.

Checking for status

First of all, lets make sure our machines are really up using vagrant status command:
$ vagrant status
Current machine states:
server                    running (virtualbox)
client                    running (virtualbox)
This environment represents multiple VMs. The VMs are all listed
above with their current state. For more information about a specific
VM, run `vagrant status NAME`.
Now let's check nagios is really running on server:
$ vagrant ssh server -c "service nagios status"
nagios (pid 4154) is running...
Connection to closed.
Great. Server is up and Nagios is running. You can now open your web browser and point it to http://localhost:8080/nagios. User is nagiosadmin and password is nagios:
Fig.02: Nagios home
Fig.02: Nagios home
If you browse around the menus you should see that currently Nagios is only configured to monitor itself. In the next part we'll add the configurations needed so Nagios will start monitoring the client machine as well.

Working with Nagios

If you list your folder content after provisioning is finished, you should find two new directories which wasn't there before we ran vagrant up.
It's the client and server folders created during initial provisioning and now contains the configurations of Nagios server
and NRPE agent.
First we'll add the following configuration directive in server/nagios.cfg:
Next, we'll create server/object/client.cfg and put the following configuration inside:
define host{
        use                     linux-server
        host_name               client
        hostgroups              linux-servers
        alias                   client
        address                 nagios-client
and now restart nagios daemon:
$ vagrant ssh server -c "sudo service nagios restart"
Running configuration check...
Stopping nagios: /etc/init.d/nagios: 147: kill: No such process
Starting nagios: done.
Connection to closed.
In your browser under "Host Groups" you should see new host with the name client and its status should change to UP after few seconds. We have instruct Nagios to watch add new configuration file and in that file we defined new host Nagios should monitor. The address of the new host is nagios-client and its being resolved to the correct address because of the hostmanager plugin which dynamically edits /etc/hosts file and added the correct IP addresses of nagios-client machine.
Now let's do one more thing and configure the client host to provide additional information to Nagios by using the NRPE monitoring daemon. First in client/nrpe.cfg we need to change line number 81 to:
Then restart nrpe daemon:
$ vagrant ssh client -c "sudo service nrpe restart"
Restarting nagios remote plugin daemon: nrpe.
Connection to closed.
Now lets go back to server/object/client.cfg and add the following lines:
define command{
        command_name            check_nrpe
        command_line            $USER1$/check_nrpe -H $HOSTADDRESS$ -c $ARG1$
define service{
        use                     generic-service
        host_name               client
        service_description     Logged users
        check_command           check_nrpe!check_users
define service{
        use                     generic-service
        host_name               client
        service_description     Load average
        check_command           check_nrpe!check_load
define service{
        use                     generic-service
        host_name               client
        service_description     Zombie procs
        check_command           check_nrpe!check_zombie_procs
Now restart nagios daemon, and after few moments you should see our newly added service checks in
Fig.03: Nagios status (click to enlarge)
Fig.03: Nagios status (click to enlarge)

Rinse and repeat

So we created our small Nagios lab environment where we can experiment with different Nagios configurations, plugins and so on. The whole environment is defined in bunch of script files which you can store in your source repository such as Git. Your co-workers can now simply download these configuration files to their machine and run vagrant up and in the end of the process get exactly the same environment as you have on your machine.

Things to watch out for

Portability can be tricky thing and there are couple of thing you need to watch out for when you work with Vagrant. First of all, make sure you all work with same Vagrant versions. Vagrant fixes a lot of issues between releases (and obviously adds a bug or two as well) so you want to be on the same page when running Vagrant. You can include Vagrant.require_version helper config in your Vagrantfile to force only specific versions of Vagrant to run your config.
In case your Vagrantfile use plugins, you want to make sure all your users have the that plugin installed. You can add small ruby test to your Vagrant file that looks something like that:
if !Vagrant.has_plugin?('plugin-name')
    puts "plugin is missing. Bye!"
    exit 1
Generally though, you want to minimize the number of 3rd party plugins to essential minimum.
Vagrant doesn't support running Vagrantfile with multiple number of providers at the same time. However you can run same Vagrantfile with different providers separately (using the --provider flag). However, supporting multiple providers in same Vagrantfile can be tricky as well since some options and features are not the same between providers. So for best compatibility you might want to decide to work with specific provider only. In our tutorial we assume that VirtualBox is used since we use the VBoxManage command line utility to find the IP address of our virtual machines. Running it under VMWare will cause hostmanager to act differently.
Another point you want to be extra careful with is running Vagrant on different OS. In our tutorial for example, the path of VBoxManage.exe needs to exists in the PATH variable or else hostmanager will fail.

No comments:

Post a Comment