Override Vagrant configuration settings locally (per-dev)

RubyVagrant

Ruby Problem Overview


I'd like the question to be answered in general, but to illustrate it, here's a use case:

I'm using Vagrant for a simple LMAP project. I use standalone Puppet for provisioning. Now, there might be some developers who sit behind a proxy and they would need some additional configuration to be made to the VM. I have things working on the Puppet side: I can pass the proxy IP (if any) as a fact to puppet in the Vagrantfile and Puppet reacts accordingly if it's set.

The only issue I have is: how can developers specify/override this setting for their development environment without having to change the Vagrantfile (which is under version control and must remain dev-environment-neutral)?

If would be awesome if people could override some Vagrant settings in a file called e.g. Vagrantfile.local, which I would exclude via .gitignore.

Since a Vagrantfile is just Ruby, I tried the following:

# Also load per-dev custom vagrant config
custom_vagrantfile = 'Vagrantfile.local'
load custom_vagrantfile if File.exist?(custom_vagrantfile)

The file inclusion basically works, but it looks like in the included file, I'm not in the same Vagrant context anymore...

Vagrant::Config.run do |config|
  config.vm.provision :puppet do |puppet|
    puppet.facter = { "proxy" => "proxy.host:80" }
  end
end

... also "resets" all other puppet config values I made in the main Vagrantfile, which makes me think I'm heading in the wrong direction here. I should note that I'm a total noob at Ruby ;)

Can anyone give me a hint or even a working solution for how per-dev customization could be done here in general?

Ruby Solutions


Solution 1 - Ruby

The Vagrantfile is just Ruby, so YAML is another option.

For example, in the Vagrantfile I do this:

# -*- mode: ruby -*-
# vi: set ft=ruby :
require 'yaml'

settings = YAML.load_file 'vagrant.yml'
db_ip_address = settings['db']['ip_address']
api_ip_address = settings['api']['ip_address']

Vagrant.configure("2") do |config|
  config.vm.box = "ffuenf/ubuntu-13.10-server-amd64"
  config.vm.box_url = "https://vagrantcloud.com/ffuenf/ubuntu-13.10-server-amd64/version/4/provider/virtualbox.box"

  config.vm.define "db" do |db|
    db.vm.synced_folder settings['db']['artifacts_dir']['host'], settings['db']['artifacts_dir']['guest']
    db.vm.network "private_network", ip: db_ip_address
    ... other stuff ...
  end

  config.vm.define "api" do |api|
    api.vm.synced_folder settings['api']['artifacts_dir']['host'], settings['api']['artifacts_dir']['guest']
    api.vm.network "private_network", ip: api_ip_address
    api.vm.network "forwarded_port", guest: settings['api']['forwarded_port']['guest'], host: settings['api']['forwarded_port']['host']
  end
end

Then I have a vagrant.yml file (I just made up the name; you can use whatever name you like) for the developer-specific configuration:

db:
  ip_address: 192.168.4.14
  artifacts_dir:
    host: /Users/willie/myapp/db-scripts
    guest: /opt/myapp/db

api:
  ip_address: 192.168.4.15
  forwarded_port:
    host: 9080
    guest: 8080
  artifacts_dir:
    host: /Users/willie/myapp/artifacts
    guest: /opt/myapp/api

Solution 2 - Ruby

I would suggest using environment variables to dynamically change the behavior of the Vagrantfile without editing the file itself.

To give a real world example, here's how you could use an Ubuntu base box by default but have an environment variable define an alternative Linux distribution:

if ENV['OPERATINGSYSTEM']
  if ENV['OPERATINGSYSTEM'].downcase == 'redhat'
    os_name = 'centos'
    config.vm.box     = 'centos'
    config.vm.box_url = 'https://dl.dropbox.com/u/7225008/Vagrant/CentOS-6.3-x86_64-minimal.box'
  else
    raise(Exception, "undefined operatingsystem: #{ENV['OPERATINGSYSTEM']}")
  end
else
  os_name = 'precise64'
  config.vm.box     = 'precise64'
  config.vm.box_url = 'http://files.vagrantup.com/precise64.box'
end

This example comes from https://github.com/puppetlabs/puppetlabs-openstack_dev_env

Solution 3 - Ruby

If you are prepared to define settings that are applied to all your vagrant boxes it's worth noting that, "Vagrant actually loads a series of Vagrantfiles, merging the settings as it goes." (ref https://docs.vagrantup.com/v2/vagrantfile/)

So I have the following defined in ~/.vagrant.d/Vagrantfile to increase the amount of RAM for my Vagrant boxes:

Vagrant.configure(2) do |config|
    config.vm.provider "virtualbox" do |vb|
      vb.memory = 2048
    end
end

Solution 4 - Ruby

Here's an idea. It may be "ugly" and "wrong", but, at least, it works :)

# file2.rb, this is your per-dev configuration file
puts "included external file which uses outer var: #{foo}"

# file1.rb, this would be your Vagrantfile
puts 'first'
foo = 'bar'

external = File.read 'file2.rb'
eval external
puts 'second'

Let's run that

$ ruby file1.rb
first
included external file which uses outer var: bar
second

Adapting to your example, file2.rb would contain only usage of config without defining it (config will be provided from outer context)

  config.vm.provision :puppet do |puppet|
    puppet.facter = { "proxy" => "proxy.host:80" }
  end

And your Vagrant file may look like this:

Vagrant::Config.run do |config|
  external = File.read 'Vagrantfile.local'
  eval external

  # proceed with general settings here
  config.vm.provision :puppet do |puppet|
    puppet.facter = { "proxy" => "proxy.host:80" }
  end
end

Update (another, "data-driven" approach)

# Vagranfile.local
config_values[:puppet][:facter][:proxy] = 'proxy.host:80'

# Vargantfile
Vagrant::Config.run do |config|
  config_values = {
    puppet: {
      facter: {
        proxy: nil
      },
      manifests_file: 'my_manifest.pp'
  
    }
  }
  external = File.read 'Vagrantfile.local'
  eval external # this should overwrite proxy config

  # proceed with general settings here
  config.vm.provision :puppet do |puppet|
    if config_values[:puppet][:facter][:proxy]
      puppet.facter = { "proxy" => config_values[:puppet][:facter][:proxy] } 
    end

    puppet.manifests_file = config_values[:puppet][:manifests_file]
  end
end

Solution 5 - Ruby

I believe that's the exact use case that Nugrant plugin was created to solve. It allows each of your devs to have a .vagrantuser (which is a .gitignore-ed file) in YAML specifying custom configuration values then reference these values with ease in Vagrantfile.

In your case, a proxied developer would have their .vagrantuser file looking like this:

proxy: 'proxy.host:80'

And your Vagrantfile would look like this (pseudo code, I don't really know ruby):

Vagrant::Config.run do |config|
  config.vm.provision :puppet do |puppet|
    if config.user.has_key?('proxy')
      puppet.facter = { "proxy" => config.user.proxy }
    end
  end
end

You should bundle a sample/reference vagrantuser (i.e. vagrantuser.example) file for your devs to copy and adjust to their environment.

Solution 6 - Ruby

To extend on @Willie Wheeler 's answer. My setup is:

Root
|-- defaults.yml
|-- env.yml
|-- Vagrantfile

Vagrantfile

# Load local env config
require 'yaml'
dir = File.dirname(File.expand_path(__FILE__))

# defaults
settings = YAML::load_file("#{dir}/defaults.yml")

if File.exist?("#{dir}/env.yml")
    env_settings = YAML::load_file("#{dir}/env.yml")
    settings.merge!(env_settings)
end
...
# Customize the amount of memory on the VM:
    vb.memory = settings["vb"]["memory"]

defaults.yml

vb:
  memory: 1024

env.yml

vb:
  memory: 204

This will merge whatever defaults you have with your per-dev config. Also it is clear to developers what values they can actually change

Solution 7 - Ruby

Consider using vagrant-proxyconf plugin. It allows to set proxy for all Vagrant VMs globally.

Another solution is to run external shell script during provisioning. I use separate config.vm.provision section at the beginning of Vagrantfile to do it:

# reset: true below is needed to reset the connection to the VM so that new
# environment variables set in /etc/environment will be picked up in next
# provisioning steps
config.vm.provision "shell", reset: true, inline: <<-SHELL
    
  if [ -f /vagrant/Vagrantfile-settings.sh ]
  then
    /vagrant/Vagrantfile-settings.sh
  fi
SHELL

Then just put a Vagrantfile-settings.sh file next to Vagrantfile, add it to .gitignore (or whatever) and put any script inside, for example to set proxy for interactive terminal, all daemons and docker containers:

# Proxy for interactive terminals
echo "http_proxy=http://PROXY_ADDRESS:PROXY_PORT" >> /etc/environment
echo "https_proxy=http://PROXY_ADDRESS:PROXY_PORT" >> /etc/environment
echo "no_proxy=127.0.0.1,localhost" >> /etc/environment
# Proxy for daemons (e.g. Docker deamon - used to pull images, apt - run from default daily cron job)
mkdir /etc/systemd/system.conf.d
echo [Manager] > /etc/systemd/system.conf.d/01-http-proxy.conf
echo "DefaultEnvironment=\"http_proxy=PROXY_ADDRESS:PROXY_PORT\"" >> /etc/systemd/system.conf.d/01-http-proxy.conf
echo "DefaultEnvironment=\"https_proxy=PROXY_ADDRESS:PROXY_PORT\"" >> /etc/systemd/system.conf.d/01-http-proxy.conf
echo "DefaultEnvironment=\"no_proxy=127.0.0.1,localhost\"" >> /etc/systemd/system.conf.d/01-http-proxy.conf
echo "# Docker requires upper-case http proxy environment variables..." >> /etc/systemd/system.conf.d/01-http-proxy.conf
echo "DefaultEnvironment=\"HTTP_PROXY=http://PROXY_ADDRESS:PROXY_PORT2\"" >> /etc/systemd/system.conf.d/01-http-proxy.conf
echo "DefaultEnvironment=\"HTTPS_PROXY=http://PROXY_ADDRESS:PROXY_PORT\"" >> /etc/systemd/system.conf.d/01-http-proxy.conf
echo "DefaultEnvironment=\"NO_PROXY=127.0.0.1,localhost\"" >> /etc/systemd/system.conf.d/01-http-proxy.conf
# Proxy for docker containers started with `docker run`
mkdir /home/vagrant/.docker
cat <<EOF > /home/vagrant/.docker/config.json
{
  "proxies": {
    "default": {
      "httpProxy": "http:/PROXY_ADDRESS:PROXY_PORT",
      "httpsProxy": "http://PROXY_ADDRESS:PROXY_PORT",
      "noProxy": "127.0.0.1,localhost"
    }
  }
}
EOF
chown -R vagrant:vagrant /home/vagrant/.docker

Solution 8 - Ruby

You can load the settings from YAML file. This is demonstrated in Drupal VM as below:

# Use config.yml for basic VM configuration.
require 'yaml'
dir = File.dirname(File.expand_path(__FILE__))
if !File.exist?("#{dir}/config.yml")
  raise 'Configuration file not found! Please copy example.config.yml to config.yml and try again.'
end
vconfig = YAML::load_file("#{dir}/config.yml")

So then you can create config.yml like:

vagrant_box: geerlingguy/ubuntu1404
vagrant_user: vagrant
vagrant_ip: 192.168.88.88

and in Vagrantfile you can use variables as:

config.vm.box = vconfig['vagrant_box']
config.vm.network "private_network", ip: vconfig['vagrant_ip']

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionnetmikeyView Question on Stackoverflow
Solution 1 - Rubyuser41871View Answer on Stackoverflow
Solution 2 - RubyPhilip DurbinView Answer on Stackoverflow
Solution 3 - RubyCourtney MilesView Answer on Stackoverflow
Solution 4 - RubySergio TulentsevView Answer on Stackoverflow
Solution 5 - RubyAmr MostafaView Answer on Stackoverflow
Solution 6 - RubyTomasView Answer on Stackoverflow
Solution 7 - RubyRafał KłysView Answer on Stackoverflow
Solution 8 - RubykenorbView Answer on Stackoverflow