58

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?

Daniel Serodio
  • 4,229
  • 5
  • 37
  • 33
netmikey
  • 2,422
  • 2
  • 28
  • 35

8 Answers8

91

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
  • This seems more extensible than all the other options, I wonder why this isn't the answer. – adeelx Jun 15 '14 at 18:02
  • 1
    Well, I added this a couple years after all the other answers. :-) –  Jun 15 '14 at 19:38
  • 3
    This should be a Vagrant idiom. This will be making its way in to all of my projects. – Jim Mitchener Nov 16 '14 at 21:26
  • I never did any Ruby, so forgive the question, but is there a simple way to merge the 'settings' Hash into 'config' object, so that if yml files mirrors the vagrant config object, all desired properties would be automatically overriden? – alh84001 Feb 23 '15 at 10:08
  • 1
    Please create a question for it. –  Feb 23 '15 at 16:09
  • I have no idea of Ruby, does it somehow prefer yaml to json, properties, ini files, etc. ? – sandris Feb 26 '15 at 14:17
  • Ruby doesn't dictate YAML over JSON, XML, properties, etc. They all have their own characteristics and it's mostly a matter of preference. –  Feb 26 '15 at 16:36
  • Homestead, using vagrant, uses something similar, but way more complicated. It actually uses YAML *and* JSON. – trysis Mar 07 '15 at 17:27
  • This solution doesn't provide defaults with a way to override them does it? This makes the vagrant.yml file required? – eric.frederich Jul 25 '16 at 19:51
  • You could easily add defaults to the code, and then override those using vagrant.yml if it exists. –  Feb 02 '17 at 16:33
28

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

Philip Durbin
  • 4,042
  • 2
  • 25
  • 36
  • 3
    I've been using this approach and like it... except that I'm now moving to managing multiple different AWS boxes all provisioned using different Vagrant scripts from the same machine... and it's a pain to set the proper ENV settings each time. Have you thought through other concepts than ENV variables? Perhaps reading a non-checked in config file in a higher-up directory that manages all settings? – JayCrossler Jan 01 '14 at 19:35
12

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
Courtney Miles
  • 3,756
  • 3
  • 29
  • 47
  • Much easier than jacking with environment variables if, like the answer states, you're okay with settings applied to all vagrant boxes. – Seth May 22 '15 at 19:42
  • As Seth and answerer mentioned, this can have unintended consequences if you have added a provisioner or anything else in the .vagrant.d/Vagrantfile as they will get applied to any VM brought up by Vagrant anywhere on your machine. Much safer to use the yaml parsing as mentioned in another answer. – dragon788 Dec 18 '15 at 17:37
6

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
Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
  • The problem is that it looks like multiple `config.vm.provision :puppet`... blocks override each other (completely). So it's impossible to configure part of Vagrant's Puppet block in the main Vagrantfile and the other on the customized one. I tried using eval within said block. That, of course, works but that would only allow dev's to set puppet parameters, which is very specifc, ugly (as you said), and gets repetitive if they want to configure more than just the puppet block... – netmikey Oct 25 '12 at 09:49
  • You could use the same schema, but make external files provide values which you'll later use in blocks. I updated the answer. This will make your main Vagrantfile somewhat more cluttered, but at least it should be configurable. – Sergio Tulentsev Oct 25 '12 at 09:58
  • It is possible, maybe, to sweeten this with some dynamic tricks (`send` and the like). – Sergio Tulentsev Oct 25 '12 at 10:00
  • Or what about using environment variables for the same purpose? – Sergio Tulentsev Oct 25 '12 at 10:10
  • Yes, one of our devs had that idea too. If there's no way to have a second file "interact" with the main Vagrantfile in a native way, I think using dedicated env vars, though being less flexible, might be an okay solution. Could you give a quick example of a (preferably short) way to express `some.config.val = use value of env variable 'MYVAR' if set, else use the default value '123'` and `if (env_var_set('MYVAR')) { do something with env_var('MYVAR') }`? Maybe in another response so I could validate that :) – netmikey Oct 25 '12 at 10:39
5

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.

Amr Mostafa
  • 23,147
  • 2
  • 29
  • 24
5

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

Tomas
  • 2,676
  • 5
  • 41
  • 51
0

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
Rafał Kłys
  • 590
  • 6
  • 14
-2

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']
kenorb
  • 155,785
  • 88
  • 678
  • 743