diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 128449cf8..bd9e67c26 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -776,6 +776,18 @@ module Vagrant error_key(:synced_folder_unusable) end + class TriggersGuestNotRunning < VagrantError + error_key(:triggers_guest_not_running) + end + + class TriggersNoBlockGiven < VagrantError + error_key(:triggers_no_block_given) + end + + class TriggersNoStageGiven < VagrantError + error_key(:triggers_no_stage_given) + end + class UIExpectsTTY < VagrantError error_key(:ui_expects_tty) end diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 2f2a9fb7c..fefbbc376 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -149,6 +149,8 @@ module Vagrant # Output a bunch of information about this machine in # machine-readable format in case someone is listening. @ui.machine("metadata", "provider", provider_name) + + @triggers = Vagrant::Plugin::V2::Trigger.new(@env, @config.trigger, self) end # This calls an action on the provider. The provider may or may not @@ -159,6 +161,7 @@ module Vagrant # as extra data set on the environment hash for the middleware # runner. def action(name, opts=nil) + @triggers.fire_triggers(name, :before, @name.to_s) @logger.info("Calling action: #{name} on provider #{@provider}") opts ||= {} @@ -185,7 +188,7 @@ module Vagrant locker = @env.method(:lock) if lock && !name.to_s.start_with?("ssh") # Lock this machine for the duration of this action - locker.call("machine-action-#{id}") do + return_env = locker.call("machine-action-#{id}") do # Get the callable from the provider. callable = @provider.action(name) @@ -203,6 +206,10 @@ module Vagrant ui.machine("action", name.to_s, "end") action_result end + + @triggers.fire_triggers(name, :after, @name.to_s) + # preserve returning environment after machine action runs + return return_env rescue Errors::EnvironmentLockedError raise Errors::MachineActionLockedError, action: name, diff --git a/lib/vagrant/plugin/v2.rb b/lib/vagrant/plugin/v2.rb index 953f73ff6..5bf2cdd41 100644 --- a/lib/vagrant/plugin/v2.rb +++ b/lib/vagrant/plugin/v2.rb @@ -19,6 +19,7 @@ module Vagrant autoload :Push, "vagrant/plugin/v2/push" autoload :Provisioner, "vagrant/plugin/v2/provisioner" autoload :SyncedFolder, "vagrant/plugin/v2/synced_folder" + autoload :Trigger, "vagrant/plugin/v2/trigger" end end end diff --git a/lib/vagrant/plugin/v2/command.rb b/lib/vagrant/plugin/v2/command.rb index 94e554dc5..5b39ef49f 100644 --- a/lib/vagrant/plugin/v2/command.rb +++ b/lib/vagrant/plugin/v2/command.rb @@ -45,7 +45,7 @@ module Vagrant def parse_options(opts=nil) # make sure optparse doesn't use POSIXLY_CORRECT parsing ENV["POSIXLY_CORRECT"] = nil - + # Creating a shallow copy of the arguments so the OptionParser # doesn't destroy the originals. argv = @argv.dup diff --git a/lib/vagrant/plugin/v2/trigger.rb b/lib/vagrant/plugin/v2/trigger.rb new file mode 100644 index 000000000..f869af76e --- /dev/null +++ b/lib/vagrant/plugin/v2/trigger.rb @@ -0,0 +1,242 @@ +require 'fileutils' +require 'log4r' +require 'shellwords' + +require Vagrant.source_root.join("plugins/provisioners/shell/provisioner") +require "vagrant/util/subprocess" +require "vagrant/util/platform" +require "vagrant/util/powershell" + +module Vagrant + module Plugin + module V2 + class Trigger + # @return [Kernel_V2::Config::Trigger] + attr_reader :config + + # This class is responsible for setting up basic triggers that were + # defined inside a Vagrantfile. + # + # @param [Vagrant::Environment] env Vagrant environment + # @param [Kernel_V2::TriggerConfig] config Trigger configuration + # @param [Vagrant::Machine] machine Active Machine + def initialize(env, config, machine) + @env = env + @config = config + @machine = machine + + @logger = Log4r::Logger.new("vagrant::trigger::#{self.class.to_s.downcase}") + end + + # Fires all triggers, if any are defined for the action and guest + # + # @param [Symbol] action Vagrant command to fire trigger on + # @param [Symbol] stage :before or :after + # @param [String] guest_name The guest that invoked firing the triggers + def fire_triggers(action, stage, guest_name) + # get all triggers matching action + triggers = [] + if stage == :before + triggers = config.before_triggers.select do |t| + t.command == action || (t.command == :all && !t.ignore.include?(action)) + end + elsif stage == :after + triggers = config.after_triggers.select do |t| + t.command == action || (t.command == :all && !t.ignore.include?(action)) + end + else + raise Errors::TriggersNoStageGiven, + action: action, + stage: stage, + guest_name: guest_name + end + + triggers = filter_triggers(triggers, guest_name) + + if !triggers.empty? + @logger.info("Firing trigger for action #{action} on guest #{guest_name}") + @machine.ui.info(I18n.t("vagrant.trigger.start", stage: stage, action: action)) + fire(triggers, guest_name) + end + end + + protected + + #------------------------------------------------------------------- + # Internal methods, don't call these. + #------------------------------------------------------------------- + + # Filters triggers to be fired based on configured restraints + # + # @param [Array] triggers An array of triggers to be filtered + # @param [String] guest_name The name of the current guest + # @return [Array] The filtered array of triggers + def filter_triggers(triggers, guest_name) + # look for only_on trigger constraint and if it doesn't match guest + # name, throw it away also be sure to preserve order + filter = triggers.dup + + filter.each do |trigger| + index = nil + match = false + if trigger.only_on + trigger.only_on.each do |o| + if o.match(guest_name) + # trigger matches on current guest, so we're fine to use it + match = true + break + end + end + # no matches found, so don't use trigger for guest + index = triggers.index(trigger) unless match == true + end + + if index + @logger.debug("Trigger #{trigger.id} will be ignored for #{guest_name}") + triggers.delete_at(index) + end + end + + return triggers + end + + # Fires off all triggers in the given array + # + # @param [Array] triggers An array of triggers to be fired + def fire(triggers, guest_name) + # ensure on_error is respected by exiting or continuing + + triggers.each do |trigger| + @logger.debug("Running trigger #{trigger.id}...") + + if trigger.name + @machine.ui.info(I18n.t("vagrant.trigger.fire_with_name", + name: trigger.name)) + else + @machine.ui.info(I18n.t("vagrant.trigger.fire")) + end + + if trigger.info + info(trigger.info) + end + + if trigger.warn + warn(trigger.warn) + end + + if trigger.run + run(trigger.run, trigger.on_error) + end + + if trigger.run_remote + run_remote(trigger.run_remote, trigger.on_error) + end + end + end + + # Prints the given message at info level for a trigger + # + # @param [String] message The string to be printed + def info(message) + @machine.ui.info(message) + end + + # Prints the given message at warn level for a trigger + # + # @param [String] message The string to be printed + def warn(message) + @machine.ui.warn(message) + end + + # Runs a script on a guest + # + # @param [Provisioners::Shell::Config] config A Shell provisioner config + def run(config, on_error) + if config.inline + cmd = Shellwords.split(config.inline) + + @machine.ui.detail(I18n.t("vagrant.trigger.run.inline", command: config.inline)) + else + cmd = File.expand_path(config.path, @env.root_path) + cmd << " #{config.args.join(' ' )}" if config.args + cmd = Shellwords.split(cmd) + + @machine.ui.detail(I18n.t("vagrant.trigger.run.script", path: config.path)) + end + + # Pick an execution method to run the script or inline string with + # Default to Subprocess::Execute + exec_method = Vagrant::Util::Subprocess.method(:execute) + + if Vagrant::Util::Platform.windows? + if config.inline + exec_method = Vagrant::Util::PowerShell.method(:execute_inline) + else + exec_method = Vagrant::Util::PowerShell.method(:execute) + end + end + + begin + result = exec_method.call(*cmd, :notify => [:stdout, :stderr]) do |type,data| + options = {} + case type + when :stdout + options[:color] = :green if !config.keep_color + when :stderr + options[:color] = :red if !config.keep_color + end + + @machine.ui.detail(data, options) + end + rescue => e + @machine.ui.error(I18n.t("vagrant.errors.triggers_run_fail")) + @machine.ui.error(e.message) + + if on_error == :halt + @logger.debug("Trigger run encountered an error. Halting on error...") + raise e + else + @logger.debug("Trigger run encountered an error. Continuing on anyway...") + @machine.ui.warn(I18n.t("vagrant.trigger.on_error_continue")) + end + end + end + + # Runs a script on the guest + # + # @param [ShellProvisioner/Config] config A Shell provisioner config + def run_remote(config, on_error) + unless @machine.state.id == :running + if on_error == :halt + raise Errors::TriggersGuestNotRunning, + machine_name: @machine.name, + state: @machine.state.id + else + @machine.ui.error(I18n.t("vagrant.errors.triggers_guest_not_running", + machine_name: @machine.name, + state: @machine.state.id)) + @machine.ui.warn(I18n.t("vagrant.trigger.on_error_continue")) + return + end + end + + prov = VagrantPlugins::Shell::Provisioner.new(@machine, config) + + begin + prov.provision + rescue => e + @machine.ui.error(I18n.t("vagrant.errors.triggers_run_fail")) + + if on_error == :halt + @logger.debug("Trigger run encountered an error. Halting on error...") + raise e + else + @logger.debug("Trigger run encountered an error. Continuing on anyway...") + @machine.ui.error(e.message) + end + end + end + end + end + end +end diff --git a/lib/vagrant/util/powershell.rb b/lib/vagrant/util/powershell.rb index 7c96485ea..4ffa0a75b 100644 --- a/lib/vagrant/util/powershell.rb +++ b/lib/vagrant/util/powershell.rb @@ -87,6 +87,27 @@ module Vagrant return r.stdout.chomp end + # Execute a powershell command and return a result + # + # @param [String] command PowerShell command to execute. + # @param [Hash] opts A collection of options for subprocess::execute + # @param [Block] block Ruby block + def self.execute_inline(*command, **opts, &block) + validate_install! + c = [ + executable, + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", "Bypass", + "-Command", + command + ].flatten.compact + c << opts + + Subprocess.execute(*c, &block) + end + # Returns the version of PowerShell that is installed. # # @return [String] diff --git a/plugins/kernel_v2/config/trigger.rb b/plugins/kernel_v2/config/trigger.rb new file mode 100644 index 000000000..546901aee --- /dev/null +++ b/plugins/kernel_v2/config/trigger.rb @@ -0,0 +1,201 @@ +require "vagrant" +require File.expand_path("../vm_trigger", __FILE__) + +module VagrantPlugins + module Kernel_V2 + class TriggerConfig < Vagrant.plugin("2", :config) + # The TriggerConfig class is what gets called when a user + # defines a new trigger in their Vagrantfile. The two entry points are + # either `config.trigger.before` or `config.trigger.after`. + + def initialize + @logger = Log4r::Logger.new("vagrant::config::trigger") + + # Internal State + @_before_triggers = [] # An array of VagrantConfigTrigger objects + @_after_triggers = [] # An array of VagrantConfigTrigger objects + end + + #------------------------------------------------------------------- + # Trigger before/after functions + #------------------------------------------------------------------- + # + # Commands are expected to be ether: + # - splat + # + config.trigger.before :up, :destroy, :halt do |trigger|.... + # - array + # + config.trigger.before [:up, :destroy, :halt] do |trigger|.... + # + # Config is expected to be given as a block, or the last parameter as a hash + # + # - block + # + config.trigger.before :up, :destroy, :halt do |trigger| + # trigger.option = "option" + # end + # - hash + # + config.trigger.before :up, :destroy, :halt, options: "option" + + # Reads in and parses Vagrant command whitelist and settings for a defined + # trigger + # + # @param [Symbol] command Vagrant command to create trigger on + # @param [Block] block The defined before block + def before(*command, &block) + command.flatten! + blk = block + + if !block_given? && command.last.is_a?(Hash) + # We were given a hash rather than a block, + # so the last element should be the "config block" + # and the rest are commands for the trigger + blk = command.pop + elsif !block_given? + raise Vagrant::Errors::TriggersNoBlockGiven, + command: command + end + + command.each do |cmd| + trigger = create_trigger(cmd, blk) + @_before_triggers << trigger + end + end + + # Reads in and parses Vagrant command whitelist and settings for a defined + # trigger + # + # @param [Symbol] command Vagrant command to create trigger on + # @param [Block] block The defined after block + def after(*command, &block) + command.flatten! + blk = block + + if !block_given? && command.last.is_a?(Hash) + # We were given a hash rather than a block, + # so the last element should be the "config block" + # and the rest are commands for the trigger + blk = command.pop + elsif !block_given? + raise Vagrant::Errors::TriggersNoBlockGiven, + command: command + end + + command.each do |cmd| + trigger = create_trigger(cmd, blk) + @_after_triggers << trigger + end + end + + #------------------------------------------------------------------- + # Internal methods, don't call these. + #------------------------------------------------------------------- + + # Creates a new trigger config. If a block is given, parse that block + # by calling it with the created trigger. Otherwise set the options if it's + # a hash. + # + # @param [Symbol] command Vagrant command to create trigger on + # @param [Block] block The defined config block + # @return [VagrantConfigTrigger] + def create_trigger(command, block) + trigger = VagrantConfigTrigger.new(command) + if block.is_a?(Hash) + trigger.set_options(block) + else + block.call(trigger, VagrantConfigTrigger) + end + return trigger + end + + def merge(other) + super.tap do |result| + new_before_triggers = [] + new_after_triggers = [] + other_defined_before_triggers = other.instance_variable_get(:@_before_triggers) + other_defined_after_triggers = other.instance_variable_get(:@_after_triggers) + + @_before_triggers.each do |bt| + other_bft = other_defined_before_triggers.find { |o| bt.id == o.id } + if other_bft + # Override, take it + other_bft = bt.merge(other_bft) + + # Preserve order, always + bt = other_bft + other_defined_before_triggers.delete(other_bft) + end + + new_before_triggers << bt.dup + end + + other_defined_before_triggers.each do |obt| + new_before_triggers << obt.dup + end + result.instance_variable_set(:@_before_triggers, new_before_triggers) + + @_after_triggers.each do |at| + other_aft = other_defined_after_triggers.find { |o| at.id == o.id } + if other_aft + # Override, take it + other_aft = at.merge(other_aft) + + # Preserve order, always + at = other_aft + other_defined_after_triggers.delete(other_aft) + end + + new_after_triggers << at.dup + end + + other_defined_after_triggers.each do |oat| + new_after_triggers << oat.dup + end + result.instance_variable_set(:@_after_triggers, new_after_triggers) + end + end + + # Iterates over all defined triggers and finalizes their config objects + def finalize! + if !@_before_triggers.empty? + @_before_triggers.map { |t| t.finalize! } + end + + if !@_after_triggers.empty? + @_after_triggers.map { |t| t.finalize! } + end + end + + # Validate Trigger Arrays + def validate(machine) + errors = _detected_errors + @_before_triggers.each do |bt| + error = bt.validate(machine) + errors.concat error if !error.empty? + end + + @_after_triggers.each do |at| + error = at.validate(machine) + errors.concat error if !error.empty? + end + + {"trigger" => errors} + end + + # return [Array] + def before_triggers + @_before_triggers + end + + # return [Array] + def after_triggers + @_after_triggers + end + + # The String representation of this Trigger. + # + # @return [String] + def to_s + "trigger" + end + end + end +end diff --git a/plugins/kernel_v2/config/vm_trigger.rb b/plugins/kernel_v2/config/vm_trigger.rb new file mode 100644 index 000000000..053d9ae74 --- /dev/null +++ b/plugins/kernel_v2/config/vm_trigger.rb @@ -0,0 +1,204 @@ +require 'log4r' +require Vagrant.source_root.join('plugins/provisioners/shell/config') + +module VagrantPlugins + module Kernel_V2 + # Represents a single configured provisioner for a VM. + class VagrantConfigTrigger < Vagrant.plugin("2", :config) + # Defaults + DEFAULT_ON_ERROR = :halt + + #------------------------------------------------------------------- + # Config class for a given Trigger + #------------------------------------------------------------------- + + # Internal unique name for this trigger + # + # Note: This is for internal use only. + # + # @return [String] + attr_reader :id + + # Name for the given Trigger. Defaults to nil. + # + # @return [String] + attr_accessor :name + + # Command to fire the trigger on + # + # @return [Symbol] + attr_reader :command + + # A string to print at the WARN level + # + # @return [String] + attr_accessor :info + + # A string to print at the WARN level + # + # @return [String] + attr_accessor :warn + + # Determines what how a Trigger should behave if it runs into an error. + # Defaults to :halt, otherwise can only be set to :continue. + # + # @return [Symbol] + attr_accessor :on_error + + # If set, will not run trigger for the configured Vagrant commands. + # + # @return [Symbol, Array] + attr_accessor :ignore + + + # If set, will only run trigger for guests that match keys for this parameter. + # + # @return [String, Regex, Array] + attr_accessor :only_on + + # A local inline or file script to execute for the trigger + # + # @return [Hash] + attr_accessor :run + + # A remote inline or file script to execute for the trigger + # + # @return [Hash] + attr_accessor :run_remote + + def initialize(command) + @logger = Log4r::Logger.new("vagrant::config::vm::trigger::config") + + @name = UNSET_VALUE + @info = UNSET_VALUE + @warn = UNSET_VALUE + @on_error = UNSET_VALUE + @ignore = UNSET_VALUE + @only_on = UNSET_VALUE + @run = UNSET_VALUE + @run_remote = UNSET_VALUE + + # Internal options + @id = SecureRandom.uuid + @command = command.to_sym + + @logger.debug("Trigger defined for command: #{command}") + end + + def finalize! + # Ensure all config options are set to nil or default value if untouched + # by user + @name = nil if @name == UNSET_VALUE + @info = nil if @info == UNSET_VALUE + @warn = nil if @warn == UNSET_VALUE + @on_error = DEFAULT_ON_ERROR if @on_error == UNSET_VALUE + @ignore = [] if @ignore == UNSET_VALUE + @run = nil if @run == UNSET_VALUE + @run_remote = nil if @run_remote == UNSET_VALUE + @only_on = nil if @only_on == UNSET_VALUE + + # these values are expected to always be an Array internally, + # but can be set as a single String or Symbol + # + # Guests are stored internally as strings + if @only_on + @only_on = Array(@only_on) + end + + # Commands must be stored internally as symbols + if @ignore + @ignore = Array(@ignore) + @ignore.map! { |i| i.to_sym } + end + + # Convert @run and @run_remote to be a "Shell provisioner" config + if @run && @run.is_a?(Hash) + # Powershell args and privileged for run commands is currently not supported + # so by default use empty string or false if unset. This helps the validate + # function determine if the setting was purposefully set, to print a warning + if !@run.key?(:powershell_args) + @run[:powershell_args] = "" + end + + if !@run.key?(:privileged) + @run[:privileged] = false + end + + new_run = VagrantPlugins::Shell::Config.new + new_run.set_options(@run) + new_run.finalize! + @run = new_run + end + + if @run_remote && @run_remote.is_a?(Hash) + new_run = VagrantPlugins::Shell::Config.new + new_run.set_options(@run_remote) + new_run.finalize! + @run_remote = new_run + end + + end + + # @return [Array] array of strings of error messages from config option validation + def validate(machine) + errors = _detected_errors + + commands = [] + Vagrant.plugin("2").manager.commands.each do |key,data| + commands.push(key) + end + + if !commands.include?(@command) && @command != :all + machine.ui.warn(I18n.t("vagrant.config.triggers.bad_command_warning", + cmd: @command)) + end + + if @run + errorz = @run.validate(machine) + errors.concat errorz["shell provisioner"] if !errorz.empty? + + if @run.privileged == true + machine.ui.warn(I18n.t("vagrant.config.triggers.privileged_ignored", + command: @command)) + end + + if @run.powershell_args != "" + machine.ui.warn(I18n.t("vagrant.config.triggers.powershell_args_ignored")) + end + end + + if @run_remote + errorz = @run_remote.validate(machine) + errors.concat errorz["shell provisioner"] if !errorz.empty? + end + + if @name && !@name.is_a?(String) + errors << I18n.t("vagrant.config.triggers.name_bad_type", cmd: @command) + end + + if @info && !@info.is_a?(String) + errors << I18n.t("vagrant.config.triggers.info_bad_type", cmd: @command) + end + + if @warn && !@warn.is_a?(String) + errors << I18n.t("vagrant.config.triggers.warn_bad_type", cmd: @command) + end + + if @on_error != :halt + if @on_error != :continue + errors << I18n.t("vagrant.config.triggers.on_error_bad_type", cmd: @command) + end + end + + errors + end + + # The String representation of this Trigger. + # + # @return [String] + def to_s + "trigger config" + end + end + end +end diff --git a/plugins/kernel_v2/plugin.rb b/plugins/kernel_v2/plugin.rb index 27737854f..4c836424f 100644 --- a/plugins/kernel_v2/plugin.rb +++ b/plugins/kernel_v2/plugin.rb @@ -39,6 +39,11 @@ module VagrantPlugins require File.expand_path("../config/vm", __FILE__) VMConfig end + + config("trigger") do + require File.expand_path("../config/trigger", __FILE__) + TriggerConfig + end end end end diff --git a/templates/locales/en.yml b/templates/locales/en.yml index c0897632b..330624287 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -279,6 +279,24 @@ en: up some disk space. Press the Enter or Return key to continue. + + trigger: + on_error_continue: |- + Trigger configured to continue on error... + start: |- + Running triggers %{stage} %{action} ... + fire_with_name: |- + Running trigger: %{name}... + fire: |- + Running trigger... + run: + inline: |- + Running local: Inline script + %{command} + script: |- + Running local script: %{path} + + version_current: |- Installed Version: %{version} version_latest: |- @@ -1392,6 +1410,22 @@ en: The synced folder type '%{type}' is reporting as unusable for your current setup. Please verify you have all the proper prerequisites for using this shared folder type and try again. + + triggers_run_fail: |- + Trigger run failed + triggers_guest_not_running: |- + Could not run remote script on %{machine_name} because its state is %{state} + triggers_no_block_given: |- + There was an error parsing the Vagrantfile: + No config was given for the trigger(s) %{command}. + triggers_no_stage_given: |- + The incorrect stage was given to the trigger plugin: + Guest: %{guest_name} + Action: %{action} + Stage: %{stage} + + This is an internal error that should be reported as a bug. + ui_expects_tty: |- Vagrant is attempting to interface with the UI in a way that requires a TTY. Most actions in Vagrant that require a TTY have configuration @@ -1677,6 +1711,33 @@ en: paranoid_deprecated: |- The key `paranoid` is deprecated. Please use `verify_host_key`. Supported values are exactly the same, only the name of the option has changed. + triggers: + bad_command_warning: |- + The command '%{cmd}' was not found for this trigger. + name_bad_type: |- + Invalid type set for `name` on trigger for command '%{cmd}'. `name` should be a String. + info_bad_type: |- + Invalid type set for `info` on trigger for command '%{cmd}'. `info` should be a String. + warn_bad_type: |- + Invalid type set for `warn` on trigger for command '%{cmd}'. `warn` should be a String. + on_error_bad_type: |- + Invalid type set for `on_error` on trigger for command '%{cmd}'. `on_error` can + only be `:halt` (default) or `:continue`. + only_on_bad_type: |- + Invalid type found for `only_on`. All values must be a `String` or `Regexp`. + privileged_ignored: |- + The `privileged` setting for option `run` for trigger command '%{command}' will be ignored and set to false. + powershell_args_ignored: |- + The setting `powershell_args` is not supported for the trigger option `run` and will be ignored. + run: + bad_type: |- + Invalid type set for `run` on trigger for command '%{cmd}'. `run` + must be a Hash. + run_remote: + bad_type: |- + Invalid type set for `run` on trigger for command '%{cmd}'. `run` + must be a Hash. + vm: bad_version: |- Invalid box version constraints: %{version} diff --git a/test/unit/plugins/kernel_v2/config/trigger_test.rb b/test/unit/plugins/kernel_v2/config/trigger_test.rb new file mode 100644 index 000000000..a9bf3bbc2 --- /dev/null +++ b/test/unit/plugins/kernel_v2/config/trigger_test.rb @@ -0,0 +1,194 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/kernel_v2/config/trigger") + +describe VagrantPlugins::Kernel_V2::TriggerConfig do + include_context "unit" + + subject { described_class.new } + + let(:machine) { double("machine") } + + def assert_invalid + errors = subject.validate(machine) + if !errors.values.any? { |v| !v.empty? } + raise "No errors: #{errors.inspect}" + end + end + + def assert_valid + errors = subject.validate(machine) + if !errors.values.all? { |v| v.empty? } + raise "Errors: #{errors.inspect}" + end + end + + before do + env = double("env") + allow(env).to receive(:root_path).and_return(nil) + allow(machine).to receive(:env).and_return(env) + allow(machine).to receive(:provider_config).and_return(nil) + allow(machine).to receive(:provider_options).and_return({}) + end + + it "is valid with test defaults" do + subject.finalize! + assert_valid + end + + let (:hash_block) { {info: "hi", run: {inline: "echo 'hi'"}} } + let (:splat) { [:up, :destroy, :halt] } + let (:arr) { [[:up, :destroy, :halt]] } + + describe "creating a before trigger" do + it "creates a trigger with the splat syntax" do + subject.before(:up, hash_block) + bf_trigger = subject.instance_variable_get(:@_before_triggers) + expect(bf_trigger.size).to eq(1) + expect(bf_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) + end + + it "creates a trigger with the array syntax" do + subject.before([:up], hash_block) + bf_trigger = subject.instance_variable_get(:@_before_triggers) + expect(bf_trigger.size).to eq(1) + expect(bf_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) + end + + it "creates a trigger with the block syntax" do + subject.before :up do |trigger| + trigger.name = "rspec" + end + bf_trigger = subject.instance_variable_get(:@_before_triggers) + expect(bf_trigger.size).to eq(1) + expect(bf_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) + end + + it "creates multiple triggers with the splat syntax" do + subject.before(splat, hash_block) + bf_trigger = subject.instance_variable_get(:@_before_triggers) + expect(bf_trigger.size).to eq(3) + bf_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) } + end + + it "creates multiple triggers with the block syntax" do + subject.before splat do |trigger| + trigger.name = "rspec" + end + bf_trigger = subject.instance_variable_get(:@_before_triggers) + expect(bf_trigger.size).to eq(3) + bf_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) } + end + + it "creates multiple triggers with the array syntax" do + subject.before(arr, hash_block) + bf_trigger = subject.instance_variable_get(:@_before_triggers) + expect(bf_trigger.size).to eq(3) + bf_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) } + end + end + + describe "creating an after trigger" do + it "creates a trigger with the splat syntax" do + subject.after(:up, hash_block) + af_trigger = subject.instance_variable_get(:@_after_triggers) + expect(af_trigger.size).to eq(1) + expect(af_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) + end + + it "creates a trigger with the array syntax" do + subject.after([:up], hash_block) + af_trigger = subject.instance_variable_get(:@_after_triggers) + expect(af_trigger.size).to eq(1) + expect(af_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) + end + + it "creates a trigger with the block syntax" do + subject.after :up do |trigger| + trigger.name = "rspec" + end + af_trigger = subject.instance_variable_get(:@_after_triggers) + expect(af_trigger.size).to eq(1) + expect(af_trigger.first).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) + end + + it "creates multiple triggers with the splat syntax" do + subject.after(splat, hash_block) + af_trigger = subject.instance_variable_get(:@_after_triggers) + expect(af_trigger.size).to eq(3) + af_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) } + end + + it "creates multiple triggers with the block syntax" do + subject.after splat do |trigger| + trigger.name = "rspec" + end + af_trigger = subject.instance_variable_get(:@_after_triggers) + expect(af_trigger.size).to eq(3) + af_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) } + end + + it "creates multiple triggers with the array syntax" do + subject.after(arr, hash_block) + af_trigger = subject.instance_variable_get(:@_after_triggers) + expect(af_trigger.size).to eq(3) + af_trigger.map { |t| expect(t).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) } + end + end + + describe "#create_trigger" do + let(:command) { :up } + let(:hash_block) { {info: "hi", run: {inline: "echo 'hi'"}} } + + it "returns a new VagrantConfigTrigger object if given a hash" do + trigger = subject.create_trigger(command, hash_block) + expect(trigger).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) + end + + it "returns a new VagrantConfigTrigger object if given a block" do + block = Proc.new { |b| b.info = "test"} + + trigger = subject.create_trigger(command, block) + expect(trigger).to be_a(VagrantPlugins::Kernel_V2::VagrantConfigTrigger) + end + end + + describe "#merge" do + it "merges defined triggers" do + a = described_class.new() + b = described_class.new() + + a.before(splat, hash_block) + a.after(arr, hash_block) + b.before(splat, hash_block) + b.after(arr, hash_block) + + result = a.merge(b) + bf_trigger = result.instance_variable_get(:@_before_triggers) + af_trigger = result.instance_variable_get(:@_after_triggers) + + expect(bf_trigger).to be_a(Array) + expect(af_trigger).to be_a(Array) + expect(bf_trigger.size).to eq(6) + expect(af_trigger.size).to eq(6) + end + + it "merges the other triggers if a class is empty" do + a = described_class.new() + b = described_class.new() + + a.before(splat, hash_block) + a.after(arr, hash_block) + + b_bf_trigger = b.instance_variable_get(:@_before_triggers) + b_af_trigger = b.instance_variable_get(:@_after_triggers) + + result = a.merge(b) + bf_trigger = result.instance_variable_get(:@_before_triggers) + af_trigger = result.instance_variable_get(:@_after_triggers) + + expect(bf_trigger.size).to eq(3) + expect(af_trigger.size).to eq(3) + end + end +end diff --git a/test/unit/plugins/kernel_v2/config/vm_trigger_test.rb b/test/unit/plugins/kernel_v2/config/vm_trigger_test.rb new file mode 100644 index 000000000..a98068d29 --- /dev/null +++ b/test/unit/plugins/kernel_v2/config/vm_trigger_test.rb @@ -0,0 +1,124 @@ +require File.expand_path("../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/kernel_v2/config/vm_trigger") + +describe VagrantPlugins::Kernel_V2::VagrantConfigTrigger do + include_context "unit" + + let(:command) { :up } + + subject { described_class.new(command) } + + let(:machine) { double("machine") } + + def assert_invalid + errors = subject.validate(machine) + if !errors.empty? { |v| !v.empty? } + raise "No errors: #{errors.inspect}" + end + end + + def assert_valid + errors = subject.validate(machine) + if !errors.empty? { |v| v.empty? } + raise "Errors: #{errors.inspect}" + end + end + + before do + env = double("env") + allow(env).to receive(:root_path).and_return(nil) + allow(machine).to receive(:env).and_return(env) + allow(machine).to receive(:provider_config).and_return(nil) + allow(machine).to receive(:provider_options).and_return({}) + + subject.name = "foo" + subject.info = "Hello there" + subject.warn = "Warning!!" + subject.ignore = :up + subject.only_on = "guest" + subject.run = {inline: "apt-get update"} + subject.run_remote = {inline: "apt-get update", env: {"VAR"=>"VAL"}} + end + + describe "with defaults" do + it "is valid with test defaults" do + subject.finalize! + assert_valid + end + + it "sets a command" do + subject.finalize! + expect(subject.command).to eq(command) + end + + it "uses default error behavior" do + subject.finalize! + expect(subject.on_error).to eq(:halt) + end + end + + describe "defining a new config that needs to match internal restraints" do + let(:cmd) { :destroy } + let(:cfg) { described_class.new(cmd) } + let(:arr_cfg) { described_class.new(cmd) } + + before do + cfg.only_on = :guest + cfg.ignore = "up" + arr_cfg.only_on = ["guest", /other/] + arr_cfg.ignore = ["up", "destroy"] + end + + it "ensures only_on is an array" do + cfg.finalize! + arr_cfg.finalize! + + expect(cfg.only_on).to be_a(Array) + expect(arr_cfg.only_on).to be_a(Array) + end + + it "ensures ignore is an array of symbols" do + cfg.finalize! + arr_cfg.finalize! + + expect(cfg.ignore).to be_a(Array) + expect(arr_cfg.ignore).to be_a(Array) + + cfg.ignore.each do |a| + expect(a).to be_a(Symbol) + end + + arr_cfg.ignore.each do |a| + expect(a).to be_a(Symbol) + end + end + end + + describe "defining a basic trigger config" do + let(:cmd) { :up } + let(:cfg) { described_class.new(cmd) } + + before do + cfg.info = "Hello there" + cfg.warn = "Warning!!" + cfg.on_error = :continue + cfg.ignore = :up + cfg.only_on = "guest" + cfg.run = {inline: "apt-get update"} + cfg.run_remote = {inline: "apt-get update", env: {"VAR"=>"VAL"}} + end + + it "sets the options" do + cfg.finalize! + expect(cfg.info).to eq("Hello there") + expect(cfg.warn).to eq("Warning!!") + expect(cfg.on_error).to eq(:continue) + expect(cfg.ignore).to eq([:up]) + expect(cfg.only_on).to eq(["guest"]) + expect(cfg.run).to be_a(VagrantPlugins::Shell::Config) + expect(cfg.run_remote).to be_a(VagrantPlugins::Shell::Config) + end + end + +end diff --git a/test/unit/vagrant/plugin/v2/trigger_test.rb b/test/unit/vagrant/plugin/v2/trigger_test.rb new file mode 100644 index 000000000..8c44a5316 --- /dev/null +++ b/test/unit/vagrant/plugin/v2/trigger_test.rb @@ -0,0 +1,319 @@ +require File.expand_path("../../../../base", __FILE__) +require Vagrant.source_root.join("plugins/kernel_v2/config/trigger") + +describe Vagrant::Plugin::V2::Trigger do + include_context "unit" + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + isolated_environment.tap do |env| + env.vagrantfile("") + end + end + let(:iso_vagrant_env) { iso_env.create_vagrant_env } + let(:state) { double("state", id: :running) } + let(:machine) do + iso_vagrant_env.machine(iso_vagrant_env.machine_names[0], :dummy).tap do |m| + allow(m).to receive(:state).and_return(state) + end + end + let(:env) { { + machine: machine, + ui: Vagrant::UI::Silent.new, + } } + + let(:triggers) { VagrantPlugins::Kernel_V2::TriggerConfig.new } + let(:hash_block) { {info: "hi", run: {inline: "echo 'hi'"}} } + let(:hash_block_two) { {warn: "WARNING!!", run_remote: {inline: "echo 'hi'"}} } + + before do + triggers.before(:up, hash_block) + triggers.before(:destroy, hash_block) + triggers.before(:halt, hash_block_two) + triggers.after(:up, hash_block) + triggers.after(:destroy, hash_block) + triggers.finalize! + end + + + let(:subject) { described_class.new(env, triggers, machine) } + + context "#fire_triggers" do + it "raises an error if an inproper stage is given" do + expect{ subject.fire_triggers(:up, :not_real, "guest") }. + to raise_error(Vagrant::Errors::TriggersNoStageGiven) + end + end + + context "#filter_triggers" do + it "returns all triggers if no constraints" do + before_triggers = triggers.before_triggers + filtered_triggers = subject.send(:filter_triggers, before_triggers, "guest") + expect(filtered_triggers).to eq(before_triggers) + end + + it "filters a trigger if it doesn't match guest_name" do + trigger_config = {info: "no", only_on: "notrealguest"} + triggers.after(:up, trigger_config) + triggers.finalize! + + after_triggers = triggers.after_triggers + expect(after_triggers.size).to eq(3) + subject.send(:filter_triggers, after_triggers, "ubuntu") + expect(after_triggers.size).to eq(2) + end + + it "keeps a trigger that has a restraint that matches guest name" do + trigger_config = {info: "no", only_on: /guest/} + triggers.after(:up, trigger_config) + triggers.finalize! + + after_triggers = triggers.after_triggers + expect(after_triggers.size).to eq(3) + subject.send(:filter_triggers, after_triggers, "ubuntu-guest") + expect(after_triggers.size).to eq(3) + end + + it "keeps a trigger that has multiple restraints that matches guest name" do + trigger_config = {info: "no", only_on: ["debian", /guest/]} + triggers.after(:up, trigger_config) + triggers.finalize! + + after_triggers = triggers.after_triggers + expect(after_triggers.size).to eq(3) + subject.send(:filter_triggers, after_triggers, "ubuntu-guest") + expect(after_triggers.size).to eq(3) + end + end + + context "#fire" do + it "calls the corresponding trigger methods if options set" do + expect(subject).to receive(:info).twice + expect(subject).to receive(:warn).once + expect(subject).to receive(:run).twice + expect(subject).to receive(:run_remote).once + subject.send(:fire, triggers.before_triggers, "guest") + end + end + + context "#info" do + let(:message) { "Printing some info" } + + it "prints messages at INFO" do + output = "" + allow(machine.ui).to receive(:info) do |data| + output << data + end + + subject.send(:info, message) + expect(output).to include(message) + end + end + + context "#warn" do + let(:message) { "Printing some warnings" } + + it "prints messages at WARN" do + output = "" + allow(machine.ui).to receive(:warn) do |data| + output << data + end + + subject.send(:warn, message) + expect(output).to include(message) + end + end + + context "#run" do + let(:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new } + let(:shell_block) { {info: "hi", run: {inline: "echo 'hi'", env: {"KEY"=>"VALUE"}}} } + let(:path_block) { {warn: "bye", + run: {path: "script.sh", env: {"KEY"=>"VALUE"}}, + on_error: :continue} } + + let(:path_block_ps1) { {warn: "bye", + run: {path: "script.ps1", env: {"KEY"=>"VALUE"}}, + on_error: :continue} } + + let(:exit_code) { 0 } + let(:options) { {:notify=>[:stdout, :stderr]} } + + let(:subprocess_result) do + double("subprocess_result").tap do |result| + allow(result).to receive(:exit_code).and_return(exit_code) + allow(result).to receive(:stderr).and_return("") + end + end + + before do + trigger_run.after(:up, shell_block) + trigger_run.before(:destroy, path_block) + trigger_run.before(:destroy, path_block_ps1) + trigger_run.finalize! + end + + it "executes an inline script with powershell if windows" do + allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) + allow(Vagrant::Util::PowerShell).to receive(:execute_inline). + and_return(subprocess_result) + + trigger = trigger_run.after_triggers.first + shell_config = trigger.run + on_error = trigger.on_error + + expect(Vagrant::Util::PowerShell).to receive(:execute_inline). + with("echo", "hi", options) + subject.send(:run, shell_config, on_error) + end + + it "executes an path script with powershell if windows" do + allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) + allow(Vagrant::Util::PowerShell).to receive(:execute). + and_return(subprocess_result) + allow(env).to receive(:root_path).and_return("/vagrant/home") + + trigger = trigger_run.before_triggers[1] + shell_config = trigger.run + on_error = trigger.on_error + + expect(Vagrant::Util::PowerShell).to receive(:execute). + with("/vagrant/home/script.ps1", options) + subject.send(:run, shell_config, on_error) + end + + it "executes an inline script" do + allow(Vagrant::Util::Subprocess).to receive(:execute). + and_return(subprocess_result) + + trigger = trigger_run.after_triggers.first + shell_config = trigger.run + on_error = trigger.on_error + + expect(Vagrant::Util::Subprocess).to receive(:execute). + with("echo", "hi", options) + subject.send(:run, shell_config, on_error) + end + + it "executes an path script" do + allow(Vagrant::Util::Subprocess).to receive(:execute). + and_return(subprocess_result) + allow(env).to receive(:root_path).and_return("/vagrant/home") + allow(FileUtils).to receive(:chmod).and_return(true) + + trigger = trigger_run.before_triggers.first + shell_config = trigger.run + on_error = trigger.on_error + + expect(Vagrant::Util::Subprocess).to receive(:execute). + with("/vagrant/home/script.sh", options) + subject.send(:run, shell_config, on_error) + end + + it "continues on error" do + allow(Vagrant::Util::Subprocess).to receive(:execute). + and_raise("Fail!") + allow(env).to receive(:root_path).and_return("/vagrant/home") + allow(FileUtils).to receive(:chmod).and_return(true) + + trigger = trigger_run.before_triggers.first + shell_config = trigger.run + on_error = trigger.on_error + + expect(Vagrant::Util::Subprocess).to receive(:execute). + with("/vagrant/home/script.sh", options) + subject.send(:run, shell_config, on_error) + end + + it "halts on error" do + allow(Vagrant::Util::Subprocess).to receive(:execute). + and_raise("Fail!") + + trigger = trigger_run.after_triggers.first + shell_config = trigger.run + on_error = trigger.on_error + + expect(Vagrant::Util::Subprocess).to receive(:execute). + with("echo", "hi", options) + expect { subject.send(:run, shell_config, on_error) }.to raise_error("Fail!") + end + end + + context "#run_remote" do + let (:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new } + let (:shell_block) { {info: "hi", run_remote: {inline: "echo 'hi'", env: {"KEY"=>"VALUE"}}} } + let (:path_block) { {warn: "bye", + run_remote: {path: "script.sh", env: {"KEY"=>"VALUE"}}, + on_error: :continue} } + let(:provision) { double("provision") } + + before do + trigger_run.after(:up, shell_block) + trigger_run.before(:destroy, path_block) + trigger_run.finalize! + end + + it "raises an error and halts if guest is not running" do + allow(machine.state).to receive(:id).and_return(:not_running) + + trigger = trigger_run.after_triggers.first + shell_config = trigger.run_remote + on_error = trigger.on_error + + expect { subject.send(:run_remote, shell_config, on_error) }. + to raise_error(Vagrant::Errors::TriggersGuestNotRunning) + end + + it "continues on if guest is not running but is configured to continue on error" do + allow(machine.state).to receive(:id).and_return(:not_running) + + allow(env).to receive(:root_path).and_return("/vagrant/home") + allow(FileUtils).to receive(:chmod).and_return(true) + + trigger = trigger_run.before_triggers.first + shell_config = trigger.run_remote + on_error = trigger.on_error + + subject.send(:run_remote, shell_config, on_error) + end + + it "calls the provision function on the shell provisioner" do + allow(machine.state).to receive(:id).and_return(:running) + allow(provision).to receive(:provision).and_return("Provision!") + allow(VagrantPlugins::Shell::Provisioner).to receive(:new). + and_return(provision) + + trigger = trigger_run.after_triggers.first + shell_config = trigger.run_remote + on_error = trigger.on_error + + subject.send(:run_remote, shell_config, on_error) + end + + it "continues on if provision fails" do + allow(machine.state).to receive(:id).and_return(:running) + allow(provision).to receive(:provision).and_raise("Nope!") + allow(VagrantPlugins::Shell::Provisioner).to receive(:new). + and_return(provision) + + trigger = trigger_run.before_triggers.first + shell_config = trigger.run_remote + on_error = trigger.on_error + + subject.send(:run_remote, shell_config, on_error) + end + + it "fails if it encounters an error" do + allow(machine.state).to receive(:id).and_return(:running) + allow(provision).to receive(:provision).and_raise("Nope!") + allow(VagrantPlugins::Shell::Provisioner).to receive(:new). + and_return(provision) + + trigger = trigger_run.after_triggers.first + shell_config = trigger.run_remote + on_error = trigger.on_error + + expect { subject.send(:run_remote, shell_config, on_error) }. + to raise_error("Nope!") + end + end +end diff --git a/website/source/docs/triggers/configuration.html.md b/website/source/docs/triggers/configuration.html.md new file mode 100644 index 000000000..15780e58e --- /dev/null +++ b/website/source/docs/triggers/configuration.html.md @@ -0,0 +1,43 @@ +--- +layout: "docs" +page_title: "Vagrant Triggers Configuration" +sidebar_current: "triggers-configuration" +description: |- + Documentation of various configuration options for Vagrant Triggers +--- + +# Configuration + +Vagrant Triggers has a few options to define trigger behavior. + +## Options + +The trigger class takes various options. + +* `action` (symbol, array) - Expected to be a single symbol value, an array of symbols, or a _splat_ of symbols. The first argument that comes after either __before__ or __after__ when defining a new trigger. Can be any valid Vagrant command. It also accepts a special value `:all` which will make the trigger fire for every action. An action can be ignored with the `ignore` setting if desired. These are the valid action commands for triggers: + + - `destroy` + - `halt` + - `provision` + - `reload` + - `resume` + - `up` + +* `ignore` (symbol, array) - Symbol or array of symbols corresponding to the action that a trigger should not fire on. + +* `info` (string) - A message that will be printed at the beginning of a trigger. + +* `name` (string) - The name of the trigger. If set, the name will be displayed when firing the trigger. + +* `on_error` (symbol) - Defines how the trigger should behave if it encounters an error. By default this will be `:halt`, but can be configured to ignore failures and continue on with `:continue`. + +* `only_on` (string, regex, array) - Guest or guests to be ignored on the defined trigger. Values can be a string or regex that matches a guest name. + +* `run_remote` (hash) - A collection of settings to run a inline or remote script with on the guest. These settings correspond to the [shell provosioner](/docs/provisioning/shell.html). + +* `run` (hash) - A collection of settings to run a inline or remote script with on the host. These settings correspond to the [shell provosioner](/docs/provisioning/shell.html). However, at the moment the only settings `run` takes advantage of are: + + `args` + + `inline` + + `path` + +* `warn` (string) - A warning message that will be printed at the beginning of a trigger. diff --git a/website/source/docs/triggers/index.html.md b/website/source/docs/triggers/index.html.md new file mode 100644 index 000000000..a405e60ed --- /dev/null +++ b/website/source/docs/triggers/index.html.md @@ -0,0 +1,95 @@ +--- +layout: "docs" +page_title: "Vagrant Triggers" +sidebar_current: "triggers" +description: |- + Introduction to Vagrant Triggers +--- + +# Vagrant Triggers + +As of version 2.1.0, Vagrant is capable of executing machine triggers _before_ or +_after_ Vagrant commands. + +Each trigger is expected to be given a command key for when it should be fired +during the Vagrant command lifecycle. These could be defined as a single key or +an array which acts like a _whitelist_ for the defined trigger. + + +```ruby +# single command trigger +config.trigger.after :up do |trigger| +... +end + +# multiple commands for this trigger +config.trigger.before [:up, :destroy, :halt, :package] do |trigger| +... +end + +# or defined as a splat list +config.trigger.before :up, :destroy, :halt, :package do |trigger| +... +end +``` + +Alternatively, the key `:all` could be given which would run the trigger before +or after every Vagrant command. If there is a command you don't want the trigger +to run on, you can ignore that command with the `ignore` option. + +```ruby +# single command trigger +config.trigger.before :all do |trigger| + trigger.info = "Running a before trigger!" + trigger.ignore = [:destroy, :halt] +end +``` + +__Note:__ _If a trigger is defined on a command that does not exist, a warning +will be displayed._ + +Triggers can be defined as a block or hash in a Vagrantfile. The example below +will result in the same trigger: + + +```ruby +config.trigger.after :up do |trigger| + trigger.name = "Finished Message" + trigger.info = "Machine is up!" +end + +config.trigger.after :up, + name: "Finished Message", + info: "Machine is up!" +``` + +Triggers can also be defined within the scope of guests in a Vagrantfile. These +triggers will only run on the configured guest. An example of a guest only trigger: + +```ruby +config.vm.define "ubuntu" do |ubuntu| + ubuntu.vm.box = "ubuntu" + ubuntu.trigger.before :destroy do |trigger| + trigger.warn = "Dumping database to /vagrant/outfile" + trigger.run_remote {inline: "pg_dump dbname > /vagrant/outfile"} + end +end +``` + +Global and machine-scoped triggers will execute in the order that they are +defined within a Vagrantfile. Take for example an abstracted Vagrantfile: + +``` +Vagrantfile + global trigger 1 + global trigger 2 + machine defined + machine trigger 3 + global trigger 4 +end +``` + +In this generic case, the triggers would fire in the order: 1 -> 2 -> 3 -> 4 + +For more information about what options are available for triggers, see the +[configuration section](/docs/triggers/configuration.html). diff --git a/website/source/docs/triggers/usage.html.md b/website/source/docs/triggers/usage.html.md new file mode 100644 index 000000000..1266572d3 --- /dev/null +++ b/website/source/docs/triggers/usage.html.md @@ -0,0 +1,95 @@ +--- +layout: "docs" +page_title: "Vagrant Triggers Usage" +sidebar_current: "triggers-usage" +description: |- + Various Vagrant Triggers examples +--- + +# Basic Usage + +Below are some very simple examples of how to use Vagrant Triggers. + +## Examples + +The following is a basic example of two global triggers. One that runs _before_ +the `:up` command and one that runs _after_ the `:up` command: + +```ruby +Vagrant.configure("2") do |config| + config.trigger.before :up do |trigger| + trigger.name = "Hello world" + trigger.info = "I am running before vagrant up!!" + end + + config.trigger.before :up do |trigger| + trigger.name = "Hello world" + trigger.info = "I am running after vagrant up!!" + end + + config.vm.define "ubuntu" do |ubuntu| + ubuntu.vm.box = "ubuntu" + end +end +``` + +These will run before and after each defined guest in the Vagrantfile. + +Running a remote script to save a database on your host before __destroy__ing a +guest: + +```ruby +Vagrant.configure("2") do |config| + config.vm.define "ubuntu" do |ubuntu| + ubuntu.vm.box = "ubuntu" + + ubuntu.trigger.before :destroy do |trigger| + trigger.warn = "Dumping database to /vagrant/outfile" + trigger.run_remote = {inline: "pg_dump dbname > /vagrant/outfile"} + end + end +end +``` + +Now that the trigger is defined, running the __destroy__ command will fire off +the defined trigger before Vagrant destroys the machine. + +```shell +$ vagrant destroy ubuntu +``` + +An example of defining three triggers that start and stop tinyproxy on your host +machine using homebrew: + +```shell +#/bin/bash +# start-tinyproxy.sh +brew services start tinyproxy +``` + +```shell +#/bin/bash +# stop-tinyproxy.sh +brew services stop tinyproxy +``` + +```ruby +Vagrant.configure("2") do |config| + config.vm.define "ubuntu" do |ubuntu| + ubuntu.vm.box = "ubuntu" + + ubuntu.trigger.before :up do |trigger| + trigger.info = "Starting tinyproxy..." + trigger.run = {path: "start-tinyproxy.sh"} + end + + ubuntu.trigger.after :destroy, :halt do |trigger| + trigger.info = "Stopping tinyproxy..." + trigger.run = {path: "stop-tinyproxy.sh"} + end + end +end +``` + +Running `vagrant up` would fire the before trigger to start tinyproxy, where as +running either `vagrant destroy` or `vagrant halt` would stop tinyproxy. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 984887372..bf17897fe 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -207,6 +207,14 @@ + > + Triggers + + + > Other