232 lines
8.1 KiB
Ruby

require 'log4r'
require 'shellwords'
require 'fileutils'
require "vagrant/util/subprocess"
require Vagrant.source_root.join("plugins/provisioners/shell/provisioner")
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 [Object] env Vagrant environment
# @param [Object] config Trigger configuration
# @param [Object] 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.any?(action))
end
elsif stage == :after
triggers = config.after_triggers.select do |t|
t.command == action || (t.command == :all && !t.ignore.any?(action))
end
else
raise Errors::TriggersNoStageGiven,
action: action,
stage: stage,
guest_name: guest_name
end
triggers = filter_triggers(triggers, guest_name)
unless 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.nil?
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.nil?
@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}...")
# TODO: I18n me
if !trigger.name.nil?
@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.nil?
@logger.debug("Executing trigger info message...")
self.info(trigger.info)
end
if !trigger.warn.nil?
@logger.debug("Executing trigger warn message...")
self.warn(trigger.warn)
end
if !trigger.run.nil?
@logger.debug("Executing trigger run script...")
self.run(trigger.run, trigger.on_error)
end
if !trigger.run_remote.nil?
@logger.debug("Executing trigger run_remote script on #{guest_name}...")
self.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 [ShellProvisioner/Config] config A Shell provisioner config
def run(config, on_error)
if !config.inline.nil?
cmd = Shellwords.split(config.inline)
@machine.ui.detail(I18n.t("vagrant.trigger.run.inline"))
else
cmd = File.expand_path(config.path, @env.root_path)
FileUtils.chmod("+x", cmd) # TODO: what about windows
@machine.ui.detail(I18n.t("vagrant.trigger.run.script", path: config.path))
end
begin
# TODO: should we check config or command for sudo? And if so, WARN the user?
result = Vagrant::Util::Subprocess.execute(*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 Exception => 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 proper Vagrant error to avoid ugly stacktrace
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 Exception => e
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(I18n.t("vagrant.errors.triggers.run_fail"))
@machine.ui.error(e.message)
end
end
end
end
end
end
end