How to Reboot a Vagrant Guest VM During Provisioning

June 1st, 2014 Permalink

It is very rarely the case that you find yourself in the awkward position of needing to reboot a Vagrant guest VM during the process of provisioning it. Most people use Vagrant to run Linux server distributions, and there is thus very little you can install or change that absolutely requires a restart. Nonetheless, sometimes you find yourself painted into this corner, and that is when you find out that there is in fact no built in method in Vagrant to reboot a machine during the provisioning process. At present to achieve this goal requires a little monkey-patching and unfortunately some reliance on private APIs in Vagrant.

The approach adopted here is to create two new types of provisioner, one for UNIX and one for Windows machines, each of which reboots the VM and then reinstates the synced folder mappings when it comes back up. Since multiple provisioning blocks can be defined for a given VM, it is possible to split provisioning into (a) pre-reboot, (b) reboot, and (c) post-reboot blocks. You can find the code for these reboot provisioners bundled up and documented at GitHub, as well as presented below.

Update 01/25/2015: There is a much better and more robust approach than the one outlined here, which is to create a new provisioner that carries out the standard reload action. That handles all of the awkward issues of synced folder mapping under the hood without tinkering with private APIs. You can find an implementation of the Vagrant Reload Provisioner at GitHub.

The start of a Vagrantfile that uses the reboot provisioner monkey patch will look something like this:

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

# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"

# Require the reboot plugin.
require './vagrant-provision-reboot-plugin'

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

  # Run your pre-reboot provisioning block.
  #config.vm.provision :chef_solo do |chef|
  #  ...
  #end

  # Run a reboot of a *NIX guest.
  config.vm.provision :unix_reboot
  # Run a reboot of a Windows guest, assuming that you are set up with the
  # relevant plugins and configurations to manage a Windows guest in Vagrant.
  #config.vm.provision :windows_reboot

  # Run your post-reboot provisioning block.
  #config.vm.provision :chef_solo do |chef|
  #  ...
  #end

The code in vagrant-provision-reboot-plugin.rb is not very complicated in and of itself, and the only trick to it is the remapping of synced folders after the reboot completes. Unfortunately that is the part that requires use of non-public APIs; it will be fragile going forward as Vagrant evolves.

# A quick hack to allow rebooting of a Vagrant VM during provisioning.
#
# This is tested with Vagrant 1.4.3 and 1.6.1. It may work with slightly earlier
# versions, but definitely won't work with 1.3.* or earlier. The code is fragile
# with respect to internal changes in Vagrant, as there is no useful public API
# that allows a reboot to be engineered.
#
# Originally adapted from: https://gist.github.com/ukabu/6780121
#
# This file should be placed into the same folder as your Vagrantfile. Then in
# your Vagrantfile, you'll want to do something like the following:
#
# ----------------------------------------------------------------------------
#
# require './vagrant-provision-reboot-plugin'
#
# Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
#
#   # Run your pre-reboot provisioning block.
#   #config.vm.provision :chef_solo do |chef|
#   #  ...
#   #end
#
#   # Run a reboot of a *NIX guest.
#   config.vm.provision :unix_reboot
#   # Run a reboot of a Windows guest, assuming that you are set up with the
#   # relevant plugins and configurations to manage a Windows guest in Vagrant.
#   #config.vm.provision :windows_reboot
#
#   # Run your post-reboot provisioning block.
#   #config.vm.provision :chef_solo do |chef|
#   #  ...
#   #end
#
# ----------------------------------------------------------------------------
#
# The provisioner takes care of remounting the synced folders.
#
# This will work for the VirtualBox provider. For other providers, a
# 'remount_synced_folders' action must be added to the provider implementation.

require 'vagrant'

# Monkey-patch the VirtualBox provider to be able to remap synced folders after
# reboot. This is the tricky part.
#
# This involves pulling out some code fragments from the existing SyncedFolders
# class - which is unpleasant, but there are no usefully exposed methods such
# that we can run only what we need to.
module VagrantPlugins
  module ProviderVirtualBox
    module Action

      class RemountSyncedFolders < SyncedFolders

        def initialize(app, env)
          super(app, env)
        end

        def call(env)
          @env = env
          @app.call(env)

          # Copied out of /lib/vagrant/action/builtin/synced_folders.rb in
          # Vagrant 1.4.3, and surprisingly still working in 1.6.1.
          #
          # This is going to be fragile with respect to future changes, but
          # that's just the way the cookie crumbles.
          #
          # We can't just run the whole SyncedFolders.call() method because
          # it undertakes a lot more setup and will error out if invoked twice
          # during "vagrant up" or "vagrant provision".
          folders = synced_folders(env[:machine])
          folders.each do |impl_name, fs|
            plugins[impl_name.to_sym][0].new.enable(env[:machine], fs, impl_opts(impl_name, env))
          end
        end
      end

      def self.action_remount_synced_folders
        Vagrant::Action::Builder.new.tap do |b|
          b.use RemountSyncedFolders
        end
      end

    end
  end
end

# Define the plugin.
class RebootPlugin < Vagrant.plugin('2')
  name 'Reboot Plugin'

  # This plugin provides a provisioner called unix_reboot.
  provisioner 'unix_reboot' do

    # Create a provisioner.
    class RebootProvisioner < Vagrant.plugin('2', :provisioner)
      # Initialization, define internal state. Nothing needed.
      def initialize(machine, config)
        super(machine, config)
      end

      # Configuration changes to be done. Nothing needed here either.
      def configure(root_config)
        super(root_config)
      end

      # Run the provisioning.
      def provision
        command = 'shutdown -r now'
        @machine.ui.info("Issuing command: #{command}")
        @machine.communicate.sudo(command) do |type, data|
          if type == :stderr
            @machine.ui.error(data);
          end
        end

        begin
          sleep 5
        end until @machine.communicate.ready?

        # Now the machine is up again, perform the necessary tasks.
        @machine.ui.info("Launching remount_synced_folders action...")
        @machine.action('remount_synced_folders')
      end

      # Nothing needs to be done on cleanup.
      def cleanup
        super
      end
    end
    RebootProvisioner

  end

  # This plugin provides a provisioner called windows_reboot.
  provisioner 'windows_reboot' do

    # Create a provisioner.
    class RebootProvisioner < Vagrant.plugin('2', :provisioner)
      # Initialization, define internal state. Nothing needed.
      def initialize(machine, config)
        super(machine, config)
      end

      # Configuration changes to be done. Nothing needed here either.
      def configure(root_config)
        super(root_config)
      end

      # Run the provisioning.
      def provision
        command = 'shutdown -t 0 -r -f'
        @machine.ui.info("Issuing command: #{command}")
        @machine.communicate.execute(command) do
          if type == :stderr
            @machine.ui.error(data);
          end
        end

        begin
          sleep 5
        end until @machine.communicate.ready?

        # Now the machine is up again, perform the necessary tasks.
        @machine.ui.info("Launching remount_synced_folders action...")
        @machine.action('remount_synced_folders')
      end

      # Nothing needs to be done on cleanup.
      def cleanup
        super
      end
    end
    RebootProvisioner

  end
end