Vagrant: Support ruby code for trigger execution

Created on 16 May 2018  ยท  21Comments  ยท  Source: hashicorp/vagrant

Vagrant version

$ vagrant -v
Vagrant 2.1.1

Host operating system

This is the operating system that you run locally.

$ uname -a
Linux arch 4.16.8-1-ARCH #1 SMP PREEMPT Wed May 9 11:25:02 UTC 2018 x86_64 GNU/Linux

Guest operating system

This is the operating system you run in the virtual machine.

$ uname -a
Linux vagrant-triggers-test 4.4.0-104-generic #127-Ubuntu SMP Mon Dec 11 12:16:42 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

Vagrantfile

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

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"
  config.vm.box_check_update = false
  config.vm.hostname = "vagrant-triggers-test"

  config.trigger.before :up do |trigger|
    trigger.info = 'test before up'
    trigger.name = 'test before up'
    print "Hello I am before up trigger"
  end

end

Debug output

https://gist.github.com/john2x/891a83d965df6e085bf438a9f5338303

Expected behavior

The vagrant halt command should not have printed the message defined in the config.trigger.before :up block.

Actual behavior

The vagrant halt command executed the trigger code intended only for the up command.

Steps to reproduce

  1. Copy Vagrantfile
  2. vagrant up
  3. vagrant halt

References

None

enhancement triggers

Most helpful comment

The print statements are just to show a minimal example. In the real Vagrantfile I'm using, I use the hooks to write and delete files to allow decrypting the VM.

  config.trigger.before :up do |trigger|
      password_id = config.vm.hostname
      if not File.exists?(PASSWORD_PATH)
        print "The VM is encrypted, please enter the password\n#{password_id}: "
        password = STDIN.noecho(&:gets).strip
        File.write(PASSWORD_PATH, password)
      end
      if File.exists?(PASSWORD_PATH)
        puts "Password file saved"
      else
        puts "Password file not saved"
      end
  end

  config.trigger.after :up do |trigger|
    File.delete(PASSWORD_PATH) if File.exists?(PASSWORD_PATH)
  end

  config.trigger.after :destroy do |trigger|
    File.delete(PASSWORD_PATH) if File.exists?(PASSWORD_PATH)
  end

  config.vm.provider :virtualbox do |vb|
    vb.name = NAME
    vb.gui = false
    password_id = config.vm.hostname
    vb.customize "post-boot", [
      "controlvm", password_id, "addencpassword", password_id, Dir.pwd + "/" + PASSWORD_PATH, "--removeonsuspend", "yes"
    ]
  end

What I wanted to happen is when I run vagrant up, I get asked for the decryption password and it's saved to a file which Virtualbox needs to decrypt (I know, not good to store the password in a file, but couldn't find an alternative). Then after up is done, it deletes the password file.

These triggers worked fine with the vagrant-triggers plugin, but after upgrading to 2.1 I had to uninstall the plugin.

With 2.1's triggers, all the triggers are executed, including the File.delete lines, so the post-boot command doesn't have the chance to use the password file.

All 21 comments

Hi @john2x - If you run Vagrant in debug mode, you'll see that the trigger is not actually running. When triggers run, Vagrant will print an info to the console saying that it will execute a trigger. The print you are seeing is when Vagrant parses your Vagrantfile to store its config. So basically what's happening here is essentially Vagrant parses your config file by evaluating each ruby block in your Vagrantfile. When it reaches your trigger block, it sees the print statement and actually executes that. If you just want the trigger to print when it executes, that is what the info or warn options are for. Thanks!

The print statements are just to show a minimal example. In the real Vagrantfile I'm using, I use the hooks to write and delete files to allow decrypting the VM.

  config.trigger.before :up do |trigger|
      password_id = config.vm.hostname
      if not File.exists?(PASSWORD_PATH)
        print "The VM is encrypted, please enter the password\n#{password_id}: "
        password = STDIN.noecho(&:gets).strip
        File.write(PASSWORD_PATH, password)
      end
      if File.exists?(PASSWORD_PATH)
        puts "Password file saved"
      else
        puts "Password file not saved"
      end
  end

  config.trigger.after :up do |trigger|
    File.delete(PASSWORD_PATH) if File.exists?(PASSWORD_PATH)
  end

  config.trigger.after :destroy do |trigger|
    File.delete(PASSWORD_PATH) if File.exists?(PASSWORD_PATH)
  end

  config.vm.provider :virtualbox do |vb|
    vb.name = NAME
    vb.gui = false
    password_id = config.vm.hostname
    vb.customize "post-boot", [
      "controlvm", password_id, "addencpassword", password_id, Dir.pwd + "/" + PASSWORD_PATH, "--removeonsuspend", "yes"
    ]
  end

What I wanted to happen is when I run vagrant up, I get asked for the decryption password and it's saved to a file which Virtualbox needs to decrypt (I know, not good to store the password in a file, but couldn't find an alternative). Then after up is done, it deletes the password file.

These triggers worked fine with the vagrant-triggers plugin, but after upgrading to 2.1 I had to uninstall the plugin.

With 2.1's triggers, all the triggers are executed, including the File.delete lines, so the post-boot command doesn't have the chance to use the password file.

Hello @john2x ! Yes....with the community plugin, you could run ruby code inside a trigger, where as the 2.1 triggers are essentially shell provisioners, not ruby code. I'm going to reopen this as an enhancement request to support ruby code for an upcoming release. Thanks!

Also I'm going to rename the issue to reflect the enhancement request. Thanks!

I'm also really missing executing code from a trigger (like the good https://github.com/emyl/vagrant-triggers plugin). Is there any hope on having this in vagrant core?

For now @rgl you can execute a ruby script (or any other executable) with core triggers options run or run_remote to execute code. This issue is on the roadmap for looking to support inline ruby like the community plugin. Thanks!

I'm missing something, as I cannot seem to find a way to get the vm id from within the run script (I need the id in order to modify the guest vm). Do you known how?

@rgl - are you trying to get the vm ID like from vagrant global-status?

@rgl - You should be able to embed ruby values into inline scripts like this:

config.trigger.before :up do |trigger|
  trigger.run = {inline: "echo 'hello this is the id: #{@machine.id}'"}
end

@briancain whenever I run this I get no such method machine.id? Am I doing something horribly wrong?

Including

config.trigger.after :provision do |trigger| trigger.run = {inline: "echo 'hello this is the id: #{@machine.id}'"} end

results in

````
$ vagrant provision server1
There was an error loading a Vagrantfile. The file being loaded
and the error message are shown below. This is usually caused by
an invalid or undefined variable.

Path: C
Line number: 22
Message: undefined method `id'
````

Is there any workaround?

Hi @Alan01252 and @ArloL : apologies if my last comment mislead you both. I believe the @machine object is something specific to that users Vagrantfile and I was just using it as an example for them since that's what they have already. In general I don't believe you can access a machines id that way.

Thanks for the reply @briancain. Are you sure about @machine? Because in the old plugin it worked: https://github.com/emyl/vagrant-triggers/issues/29

@ArloL - oh, I see! So that's a feature of the plugin itself, and not something that Vagrant has support for. We can add this to the list of things to support. Thanks!

Adding a native Ruby execution option for triggers would help me quite a bit. Right now I've got sh and PowerShell scripts (because I don't know the platform of the Vagrant user) to add/remove shared disks in the host environment. This could be done trivially in Ruby and remove my Vagrantfile's dependence on a herd of scripts in several types of shell.

Adding a native Ruby execution option for triggers would help me quite a bit too.

To get machine id you can use code similar to:

hostname = "default"
provider = "virtualbox"
machID = ".vagrant/machines/#{hostname}/#{provider}/id"
if File.file?(machID)
   machineID = File.read(machID)
end
if defined?(machineID)
   trigger.run = {inline: "VBoxManage storageattach '#{machineID}'" +
           " --storagectl 'IDE' --port 1 --device 0 --type hdd --medium #{machine[:hdd_name]}"}
end

Code above gets machineID, but fails (it attaches device multiple times, "File.file?(machID)" checks for file during parse time)

For yours to work, you'd have to run the File.read() inside the trigger, everything else will get executed when the file is parsed (which is why you want native Ruby triggers... ;)

I tried a different work-around to no avail: you can use Gem.ruby to get the full path to the current Ruby interpreter (which you know exists, because you are using it). I tried to do something like (from memory):

ruby = Gem.ruby
trigger.run = {
  path: "#{ruby}",
  args: [ "scripts/vmware_mkdisk.rb", %Q["#{shared_disk_file}"], size ]
 }

This gets all of the correct info and would work, except that somehow Ruby knows that it was spawned from Ruby and is in a completely different environment than if it were run from the command-line.

Amusingly, if I substitute the interpreter string in the shebang line it'll mostly work. I'd just rather not build a Vagrantfile that modifies the scripts it depends on with every invocation, and then on Windows I'd have to modify the file extension association on the fly -- which also seems like a bad idea.

That's a lot of ugly to work-around something that I feel like Vagrant should help with.

Just my two cent's b/c I was just running into this unexpectedly.

From the current VagrantConfigTrigger could we maybe get something like:

config.trigger.after :up do |trigger|
  trigger.run ruby: ->(env, machine) do
    # do happy stuff
  end
  # or just
  trigger.run_ruby do |env, machine|
     # do happy stuff
  end
end

I know, it's kind of hard core, but as I see it there's no other way of getting runtime machine information (such as network configs with IP in public or DHCP mode).

(Kind of reminds me on the very first version of plugins where one could write a simple inline hook without writing an entire plugin)

Hey everyone! Thanks for all the feedback on this issue. I've written up the change via #10267 , and it should be out in the next release of Vagrant. All of your examples and requests were super useful. You should also now have access to the machine object inside a ruby trigger.

I'm going to lock this issue because it has been closed for _30 days_ โณ. This helps our maintainers find and focus on the active issues.

If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

DreadPirateShawn picture DreadPirateShawn  ยท  3Comments

dorinlazar picture dorinlazar  ยท  3Comments

jazzfog picture jazzfog  ยท  3Comments

gwagner picture gwagner  ยท  3Comments

bbaassssiiee picture bbaassssiiee  ยท  3Comments