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 # @param [Vagrant::UI] ui Class for printing messages to user def initialize(env, config, machine, ui) @env = env @config = config @machine = machine @ui = ui @logger = Log4r::Logger.new("vagrant::trigger::#{self.class.to_s.downcase}") end # Fires all triggers, if any are defined for the named type and guest. Returns early # and logs a warning if the community plugin `vagrant-triggers` is installed # # @param [Symbol] name Name of `type` thing to fire trigger on # @param [Symbol] stage :before or :after # @param [String] guest The guest that invoked firing the triggers # @param [Symbol] type Type of trigger to fire (:action, :hook, :command) def fire(name, stage, guest, type) if community_plugin_detected? @logger.warn("Community plugin `vagrant-triggers detected, so core triggers will not fire") return end return @logger.warn("Name given is nil, no triggers will fire") if !name return @logger.warn("Name given cannot be symbolized, no triggers will fire") if !name.respond_to?(:to_sym) name = name.to_sym # get all triggers matching action triggers = find(name, stage, guest, type) if !triggers.empty? @logger.info("Firing trigger for #{type} #{name} on guest #{guest}") @ui.info(I18n.t("vagrant.trigger.start", type: type, stage: stage, name: name)) execute(triggers) end end # Find all triggers defined for the named type and guest. # # @param [Symbol] name Name of `type` thing to fire trigger on # @param [Symbol] stage :before or :after # @param [String] guest The guest that invoked firing the triggers # @param [Symbol] type Type of trigger to fire # @return [Array] def find(name, stage, guest, type) triggers = nil name = nameify(name) if stage == :before triggers = config.before_triggers.select do |t| (t.command == :all && !t.ignore.include?(name)) || (type == :hook && matched_hook?(t.command, name)) || nameify(t.command) == name end elsif stage == :after triggers = config.after_triggers.select do |t| (t.command == :all && !t.ignore.include?(name)) || (type == :hook && matched_hook?(t.command, name)) || nameify(t.command) == name end else raise Errors::TriggersNoStageGiven, name: name, stage: stage, type: type, guest_name: guest end filter_triggers(triggers, guest, type) end protected # Convert object into name # # @param [Object, Class] object Object to name # @return [String] def nameify(object) if object.is_a?(Class) object.name.to_s else object.to_s end end #------------------------------------------------------------------- # Internal methods, don't call these. #------------------------------------------------------------------- # Generate all valid lookup keys for given action key # # @param [Class, String] key Base key for generation # @return [Array] all valid keys def matched_hook?(key, subject) subject = nameify(subject) Vagrant.plugin("2").manager.generate_hook_keys(key).any? do |k| k == subject end end # Looks up if the community plugin `vagrant-triggers` is installed # and also caches the result # # @return [Boolean] def community_plugin_detected? if !defined?(@_triggers_enabled) plugins = Vagrant::Plugin::Manager.instance.installed_plugins @_triggers_enabled = plugins.keys.include?("vagrant-triggers") end @_triggers_enabled end # 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 # @param [Symbol] type The type of trigger (:command or :type) # @return [Array] The filtered array of triggers def filter_triggers(triggers, guest_name, type) # 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.to_s) # 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 trigger.type != type index = triggers.index(trigger) end if index @logger.debug("Trigger #{trigger.id} will be ignored for #{guest_name}") triggers.delete_at(index) end end return triggers end # Execute all triggers in the given array # # @param [Array] triggers An array of triggers to be fired def execute(triggers) # ensure on_error is respected by exiting or continuing triggers.each do |trigger| @logger.debug("Running trigger #{trigger.id}...") if trigger.name @ui.info(I18n.t("vagrant.trigger.fire_with_name", name: trigger.name)) else @ui.info(I18n.t("vagrant.trigger.fire")) end if trigger.info info(trigger.info) end if trigger.warn warn(trigger.warn) end if trigger.abort trigger_abort(trigger.abort) end if trigger.run run(trigger.run, trigger.on_error, trigger.exit_codes) end if trigger.run_remote run_remote(trigger.run_remote, trigger.on_error, trigger.exit_codes) end if trigger.ruby_block execute_ruby(trigger.ruby_block) end end end # Prints the given message at info level for a trigger # # @param [String] message The string to be printed def info(message) @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) @ui.warn(message) end # Runs a script on a guest # # @param [Provisioners::Shell::Config] config A Shell provisioner config def run(config, on_error, exit_codes) if config.inline if Vagrant::Util::Platform.windows? cmd = config.inline else cmd = Shellwords.split(config.inline) end @ui.detail(I18n.t("vagrant.trigger.run.inline", command: config.inline)) else cmd = File.expand_path(config.path, @env.root_path).shellescape args = Array(config.args) cmd << " #{args.join(' ')}" if !args.empty? cmd = Shellwords.split(cmd) @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 @ui.detail(data, options) end if !exit_codes.include?(result.exit_code) raise Errors::TriggersBadExitCodes, code: result.exit_code end rescue => e @ui.error(I18n.t("vagrant.errors.triggers_run_fail")) @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...") @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, exit_codes) if !@machine # machine doesn't even exist. if on_error == :halt raise Errors::TriggersGuestNotExist else @ui.warn(I18n.t("vagrant.errors.triggers_guest_not_exist")) @ui.warn(I18n.t("vagrant.trigger.on_error_continue")) return end elsif @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 # Exits Vagrant immediately # # @param [Integer] code Code to exit Vagrant on def trigger_abort(exit_code) if Thread.current[:batch_parallel_action] @ui.warn(I18n.t("vagrant.trigger.abort_threaded")) @logger.debug("Trigger abort within parallel batch action. " \ "Setting exit code and terminating.") Thread.current[:exit_code] = exit_code Thread.current.terminate else @ui.warn(I18n.t("vagrant.trigger.abort")) @logger.debug("Trigger abort within non-parallel action, exiting directly") Process.exit!(exit_code) end end # Calls the given ruby block for execution # # @param [Proc] ruby_block def execute_ruby(ruby_block) ruby_block.call(@env, @machine) end end end end end