Vagrantfile ruby syntax explanation - ruby

I'm confused with the ruby syntax used to configure Vagrant. Especially with this construct. Is this an assignment, a method call, or something else? Is it pure ruby or vagrant specific dialect?
config.vm.network "forwarded_port", guest: 3000, host: 3000
And this one. Is the "ansible" an assignment or an argument, and where |ansible| comes from?
config.vm.provision "ansible" do |ansible|
ansible.playbook = "provisioners/docker.yml"
end
Where I can find more information about those specific expressions?

Those are DSLs, Ruby is a very good language for writing DSL, take a look a this other question
Although those are DSLs, you can throw vanilla Ruby code outside those blocks, and probably inside as well as long as it gets evaluated.

The Vagrantfile is written in standard Ruby syntax.
Here is an example Vagrantfile
Vagrant.configure("2") do |config|
# this is an evaluation statement
# .box is an string attribute
config.vm.box = "debian/stretch64"
# this is a method call
# .synced_folder is a method that takes two positional arguments ('synced_folder', '/vagrant'),
# followed by some keyword arguments (disabled: true)
config.vm.synced_folder 'synced_folder', '/vagrant', disabled: true
# this is a method call, followed by a "do ... end" block
# .provider is a method that takes one positional argument (:libvirt)
config.vm.provider :libvirt do |node|
# these are two evaluation statements
node.cpus = 4
node.memory = 4096
end
end
From the official documents,
you can see the variable "string" in "config.vm.box (string)",
but not following the methods config-vm-provider and config-vm-synced_folder.
I was also confused about the Vagrantfile syntax, even after reading the offical documents.
I think it's because Ruby looks very different to me compared to other languages I used before.

Related

How to Add to Vagrant Settings From Loaded Vagrantfile

This may actually be a basic Ruby question more than a Vagrant-specific question. I have two Vagrantfiles. One makes a number of generic settings. The second one makes more specific settings for the particular instance. The first is loaded into the second. The idea is to keep common settings that are the same across all Vagrantfiles in one place so it's normalizes and I don't repeat myself. Some Vagrant settings are an array of strings. If I set one of these in the first Vagrantfile, and I try to add an element to it in the second file, I get an error:
Message: NoMethodError: undefined method `<<' for :__UNSET__VALUE__:Symbol
How can I add to an array?
Here is a boiled down first Vagrantfile with just what I'm interested in:
Vagrantfile.general
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure('2') do |config|
config.vm.provider 'cloud_service' do |cs|
cs.tags = ['ExampleTag1']
end
end
Vagrantfile (Specific)
load 'Vagrantfile.general'
Vagrant.configure('2') do |config|
config.vm.provider 'cloud_service' do |cs|
cs.tags << 'ExampleTag2'
end
end
The following works, but I want to know how to add to the previous array rather than overriding the whole array.
Vagrantfile (Workaround)
load 'Vagrantfile.general'
Vagrant.configure('2') do |config|
config.vm.provider 'cloud_service' do |cs|
cs.tags = ['ExampleTag1', 'ExampleTag2']
end
end
Note: I understand that the tags can be kept in a YAML file that is loaded by first and second files and then added to or subtracted from before setting the tags in the provider. I have read this answer elsewhere. What I am seeking is a more elegant way to handle this. Basically I want to know if it is possible to access the object created by the first Vagrantfile where the array is located and add to it programmatically in the second Vagrantfile using Ruby code.
Update
Here is one option that is Ruby code, but does not access the array inside the Vagrant object: set a global variable in the first Vagrantfile. I'd still like to see an answer that accesses the Vagrant config object and modifies the array.
First
# -*- mode: ruby -*-
# vi: set ft=ruby :
$cs_tags = ['ExampleTag1']
Vagrant.configure('2') do |config|
config.vm.provider 'cloud_service' do |cs|
cs.tags = $cs_tags
end
end
Second
load 'Vagrantfile.general'
$cs_tags << 'ExampleTag2'
Vagrant.configure('2') do |config|
config.vm.provider 'cloud_service' do |cs|
cs.tags = $cs_tags
end
end

Run a dynamic number of shell provisioners on the same VM in Vagrant without changing the Vagrantfile?

I am writing a Vagrantfile to set up a VM. There are some config parameters that I do not want to hardcode in the Vagrantfile, such as Memory and number of CPUs. As a consequence, I'm using a YAML file which gets loaded in the Vagrantfile to store these config parameters. One thing that is stored in the YAML file is the list of shell provisioner scripts to run. For instance:
---
machine_config:
mem: 2048
cpus: 2
provisioners:
-name: shell-script-1
path: <path-to-shell-script-1>
-name: shell-script-2
path: <path-to-shell-script-2>
---
The number of provisioners is not known a priori: in the YAML above there are two, but it's only an example. I'd like to have a Vagrantfile which can run all of the provisioners in the YAML file. What I mean is that I want to be able to add/remove provisioners to the YAML file without touching the Vagrantfile, yet the Vagrantfile should correctly run all of the provisioners in the YAML file. I searched on Google and there are plenty of examples on how to run the same, hardcoded provisioners on a dynamic number of VMs, but could find none for my problem.
What I'd like to do, written in pseudo-vagrantfile syntax, is:
require "yaml"
current_dir = File.dirname(File.expand_path(__FILE__))
yaml_config = YAML.load_file("#{current_dir}/machine_config.yaml")
machine_config = yaml_config["machine_config"]
additional_scripts = machine_config["provisioners"]
Vagrant.configure("2") do |config|
config.vm.box = <vm-box-to-use>
for each item $script in additional_scripts do
config.vm.provision "shell", path: $script["path"]
end
end
where machine_config.yaml is a YAML file like the one in the first example of this question, and $script is a variable that at every iteration of the loop holds a provisioner among those described in machine_config.yaml. As a last remark, I know nothing about Ruby and Ruby's syntax (maybe to someone with that knowledge the answer to my question is trivial, but I couldn't find it by googling).
The following will work
require "yaml"
current_dir = File.dirname(File.expand_path(__FILE__))
yaml_config = YAML.load_file("#{current_dir}/machine_config.yaml")
machine_config = yaml_config["machine_config"]
Vagrant.configure("2") do |config|
config.vm.box = "<vm-box-to-use>"
machine_config["provisioners"].each do |script|
config.vm.provision "shell", name: script['name'], path: script['path']
end
end

Where to learn ruby DSL?

Vagrant.configure(2) do |config|
config.vm.define "chefnode" do |chefnode|
chefnode.vm.box = "geerlingguy/ubuntu1604"
chefnode.vm.hostname = 'cnode'
chefnode.vm.network "public_network"
end
config.vm.define "chefserver" do |chefserver|
chefserver.vm.box = "geerlingguy/ubuntu1604"
chefserver.vm.hostname = 'cserver'
chefserver.vm.network "public_network"
end
end
I am badly struggling on the above (vagrant dsl) code. Is this Ruby DSL or plain Ruby or something else? Why is there an "=" sign for vm.box and vm.hostname, but not for vm.network??
first of all it's ruby, which as a language can be very handy when you want to create your own DSL. All DSLs are built using base ruby concepts like blocks (do ... end) - no magic here.
In your case we may say it's DSL created by Vagrant owners.
Why do they once use = and an another time not? In this specific case I assume it's caused by fact you have one argument which is a String and is required, and later on yo may pass a hash with different setup options, like in example from documentation.
Vagrant.configure("2") do |config|
config.vm.network "forwarded_port", guest: 80, host: 8080
end
If they wanted to use = they would have to enforce you to pass a Hash in which name key would be required and the rest would be optional, I mean something like this:
config.vm.network = { name: "default" }
This is standard Ruby, no DSL or metaprogramming magic here.
Ruby is unusual in this regard, but the following are both calling methods on the object returned by chefserver.vm:
chefserver.vm.hostname = 'chefserver' # calls method 'hostname='
chefserver.vm.network "public_network" # calls method 'network'
Ruby does not require the use of parentheses for a method call (although sometimes they are necessary to clarify to the interpreter what you mean).
I don't know Chef, so I can't say why the network call is not network=, except that in Ruby calling a method whose name ends with '=' with more than 1 parameter does not work. And although only 1 parameter to network is specified here, in Ruby there can be optional arguments. The method may have been defined something like this:
def network(name, something = 'foo')
# ...
end
...so that the method can be called with either 1 or 2 arguments.

Naming and variables of config.vm.define

In the tutorial
https://docs.vagrantup.com/v2/multi-machine/
there are a few examples of code such as
config.vm.define :testing do |test|
config.vm.define "web" do |web|
In some of these examples, the string after define is the same as after do (web, web) , in some it is not (testing, test). Why?
Also, why use quotes with "web" but colon with :testing ?
Its more ruby language than vagrant, but basically config.vm.define is a method which takes one parameter, then there is a ruby block statement and within this block the method parameter has a specific name which is defined between the |
Also, why use quotes with "web" but colon with :testing ?
As a ruby novice, I would say it is the same - the :x is called symbols and you can read some differences about using one or the other

Trying to loop over hash for Vagrant boxes, failing

Ok, i'm very new to Ruby (i come from PHP, Symfony2 and AngularJS) and relatively new when it comes to properly writing Vagrantfiles. I'm trying to create a multi-machine environment while trying to stick to DRY principles.
As i read that Vagrantfiles understand Ruby syntax, i looked up the way Ruby defines associative arrays. This happened to be quite easy, apparently not.
My Vagrantfile:
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
#hash for boxes: 'box_name' => 'last_ip_octet'
boxes = {
'frontend' => '10',
'qp' => '11'
}
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "chef/ubuntu-14.04"
#All machines, see the hash defined in top of the Vagrantfile for all the boxes
boxes.each do |key, value|
config.vm.define "#{key}.qp" do |#{key}_qp|
#{key}_qp.vm.network "private_network", ip: "192.168.51.#{value}"
#{key}_qp.vm.provision "shell", path: "../provisioning/agentinstall.sh"
#{key}_qp.vm.synced_folder "./share/#{key}.qp", "/var/www/html"
end
end
end
My problem reads as follows:
There is a syntax error in the following Vagrantfile. The syntax error
message is reproduced below for convenience:
/Users/Zowie/Documents/vagrant/project/temp/Vagrantfile:30: syntax error, unexpected keyword_end, expecting '|'
end
^
Unfortunately, i can't find any info on using Hashes or anything similar in Vagrantfiles.
I really hope you can help me out, because i'd not feel good while writing a super-long Vagrantfile with a lot of repetitions...
Thanks in advance!
The Stackoverflow website answered my question for me!
Thanks to Stackoverflow's code block feature, i noticed that my machine-specific configurations were commented out because i used a '#'.
I fixed it by using the following syntax in my loop (which is also easier to read):
boxes.each do |key, value|
config.vm.define "#{key}.qp" do |node|
node.vm.network "private_network", ip: "192.168.51.#{value}"
node.vm.provision "shell", path: "../provisioning/agentinstall.sh"
node.vm.synced_folder "./share/#{key}.qp", "/var/www/html"
end
end

Resources