Simulating Dynamic Multiple Host Networking With Vagrant
Tags: devops, automation, networking, infrastructureascode
Published: 2024-03-12 08:19:55

## Introduction

For network testing scenarios where deploying physical machines is impractical, [Vagrant](https://www.vagrantup.com/) offers a solution. This tool allows the creation of multiple virtual hosts within a controlled, private network, enabling you to simulate real-world network interactions and test connectivity between systems.

In this article, you’ll explore the technical aspects of using Vagrant for network simulation and learn about the advantages of using it for interconnected systems.

## Prerequisites

- A working Vagrant installation. See the [installation](https://developer.hashicorp.com/vagrant/docs/installation) guide on their official website.
- A working Virtualbox installation. Check their official [website](https://www.virtualbox.org/wiki/Downloads) to install it.
- A fair understanding of Linux. You can check this [Linux](https://www.digitalocean.com/community/tutorial-series/getting-started-with-linux) tutorial series on Digital Ocean.
- An internet connection to download any necessary dependencies.

## Steps

### Step 1 - Downloading the box

A vagrant box is a copy of a specific operating system. Vagrant makes these copies available on the cloud for reproducing virtual environments.

For this tutorial, you are using a Eurolinux CentOS box. To download this box, enter the following commands in your terminal:

 

```bash
$ vagrant box add eurolinux-vagrant/centos-stream-9
```

This command downloads the CentOS Stream 9 box image from [Vagrant Cloud](https://app.vagrantup.com/boxes/search) (a repository for pre-build Vagrant boxes). The locally stored box enables you to create multiple virtual environments without downloading it again. 

### Step 2 - Provisioning the virtual machine

Create a folder named **netsim** and move into it:

```bash
$ mkdir netsim
$ cd netsim
```

To create a virtual machine you’ll need a Vagrantfile.

A Vagrantfile is a plain text file written in Ruby that tells Vagrant exactly what kind of virtual machine to create and configure. It’s a blueprint for your development environment.

Create a Vagrantfile using this Command:

```bash
$ touch Vagrantfile
```

This tutorial demonstrates using Vim to add configuration details to the file, but you can use any editor you prefer.

Open the file in Vim:

```bash
$ vim Vagrantfile
```

Switch to insert mode by pressing `i`.

- Declare `vm_memory` and `vm_cpus` :

```ruby
vm_memory = 1024
vm_cpus = 1
```

`vm_memory` allocates 1024MB(1GB) of RAM to the virtual machine. `vm_cpus` assigns a single CPU to the VM.

- Configure Base Box for the development environment:

```ruby
Vagrant.configure("2") do |config|
  config.vm.box = "eurolinux-vagrant/centos-stream-9"
  config.vm.box_check_update = false
```

`Vagrant.configure("2") do |config|` initiates a configuration block within the Vagrantfile, specifying version 2 of the Vagrant configuration syntax. `config.vm.box = "eurolinux-vagrant/centos-stream-9"` instructs Vagrant to use CentOS Stream 9 virtual machine image. `config.vm.box_check_update = false` disables automatic box update for the virtual machine.

- Increase memory allocation:

```ruby
  config.vm.provider "virtualbox" do |vb|
  #   Customize the amount of memory on the VM:
      vb.memory = 1024
      vb.cpus = 1
 
   end
```

`vb.memory` allocates 1024MB of RAM to the virtual machine. `vb.cpus` assigns a single CPU to the VM.

- Define a virtual machine:

```ruby
config.vm.define "server-1" do |config|
```

The line above creates a new virtual machine named `server-1`.

Under this configuration block, equip the virtual machine with a shell script that executes during the provisioning process:

```ruby
    config.vm.provision "shell", inline: <<-SHELL
     ip addr | grep \"inet\" | awk '{print $2}'
   SHELL
```

This script retrieves the IP address of the VM and prints it to the console.

- Assign a static IP address to the VM:

```ruby
config.vm.network "private_network", ip: "192.168.56.39", virtualbox__intnet: "true"
  end
end
```

The line above establishes a private network interface for the virtual machine, assigns a static IP address(`192.168.56.39`), and isolates it within a VirtualBox-specific internal network.

Private networks create isolated environments for virtual machines. They are ideal for testing and development in security-sensitive scenarios.

`virtualbox__intnet: "true"` enables VirtualBox’s internal networking for that specific virtual machine. This means the network is completely isolated from the host machine or outside networks and can only communicate with other machines on the same private network.

### Step 3 - Adding a new machine

To add a virtual machine to this private network, repeat the process in the previous step and assign a different static IP:

```ruby
  config.vm.define "server-2" do |config|
    config.vm.provision "shell", inline: <<-SHELL
     ip addr | grep \"inet\" | awk '{print $2}'
   SHELL
  config.vm.network "private_network", ip: "192.168.56.40", virtualbox__intnet: "true"
  end
```

This creates a new virtual machine named `server-2` and provisions it with the same configurations as the first machine.

### Step 4 - Applying DRY principles

DRY stands for Don’t Repeat Yourself. It’s an important principle in software development that emphasizes the importance of writing reusable code to avoid unnecessary duplication.

In the previous step, you duplicated the configuration setup for the first machine. This goes against the DRY principle.

To make the virtual machine creation process reusable, you’re going to make some changes.

1. Clear the configuration setups for both machines.
2. Declare `vm_num` at the top of the file and set it to `2` :

```ruby
vm_num = 2
```

1. Paste the script below:

```ruby
  (1..vm_num) .each do |n|
    #a lAB in the 192.168.56.0/24range
    lan_ip = "192.168.56.#{n+10}"
    config.vm.define "server-#{n}" do |config|
      config.vm.provision "shell", inline: <<-SHELL
        ip addr | grep \"inet\" | awk '{print $2}'
      SHELL
      config.vm.network "private_network", ip: lan_ip, virtualbox__intnet: "true"
    end
  end
```

Here’s a breakdown of what the script does:

- `(1..vm_num) .each do |n|` is a loop that iterates over a range from 1 to `vm_num` , where vm_num is the total number of VMs you want to create. For each iteration, n is the current number in the range.
- `lan_ip = "192.168.56.#{n+10}"` sets the LAN IP address for each VM. The IP addresses start from “192.168.56.11” and increment by 1 for each VM.
- `config.vm.define "server-#{n}" do |config|` defines a new VM with the name “server-n”, where **`n`** is the current number in the range.
- `config.vm.provision :shell, :inline => "ip addr | grep \"inet\" | awk '{print $2}'"`provisions the VM using a shell script. The script retrieves the IP address of the VM.
- `config.vm.network "private_network", ip: lan_ip, virtualbox__intnet: "true"` set up a private network for the VM and assigns it the `lan_ip` defined earlier. The `virtualbox__intnet: "true"` option specifies that the network should be a VirtualBox internal network as explained in the previous steps.
- `end` terminates the definition for the current VM. The loop then moves on to the next number in the range, until all VMs have been defined.

Below is the complete Vagrantfile, after all the necessary configurations:

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

vm_num = 2

Vagrant.configure("2") do |config|
  config.vm.box = "eurolinux-vagrant/centos-stream-9"
  config.vm.box_check_update = false

  config.vm.provider "virtualbox" do |vb|
  #   Customize the amount of memory on the VM:
      vb.memory = 1024
      vb.cpus = 1

   end

  (1..vm_num) .each do |n|
    #a lAB in the 192.168.56.0/24range
    lan_ip = "192.168.56.#{n+10}"
    config.vm.define "server-#{n}" do |config|
      config.vm.provision "shell", inline: <<-SHELL
        ip addr | grep \"inet\" | awk '{print $2}'
      SHELL
      config.vm.network "private_network", ip: lan_ip, virtualbox__intnet: "true"
    end
  end
end
```

> If you have less than 8 gigs of RAM, consider lowering the virtual machine’s memory to 512MB.
> 

Save and exit the file.

Then, run the `vagrant up` command in your terminal:

```ruby
$ vagrant up
```

This command creates the virtual environments as configured in the Vagrantfile.

The provisioning process prints the IP addresses of both machines to the console.

![server-1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oekow5b3h6sxljocizmd.png)

![server-2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hh13bk3uas080xuo2v8e.png)

### Step 5 - Testing their connectivity

You’ve fully set up the virtual machines. To test the connection between them, you’ll need to run a ping test on each machine. If configured successfully, they would operate on the same private network, accessible only to each other.

Login to `server-1` and send 4 packets to `server-2`:

```bash
$ vagrant ssh server-1
$ ping 192.168.56.11 -c 4
```

A solid connection ensures zero packet loss. To verify, log in to server-2 and send 4 packets to server-1:

```bash
$ vagrant ssh server-2
$ ping 192.168.56.12
```

## Conclusion

In this article, you’ve set up a multi-host environment for development with Vagrant. Additionally, you utilized private networking within these environments.

Private networks safeguard your environment from external threats and ensure secure communication.

If you enjoyed this article or have any questions, kindly indicate in the comments.