Skip to content

Commit

Permalink
Merge pull request #9713 from briancain/vagrant-triggers-config
Browse files Browse the repository at this point in the history
Integrate vagrant-triggers plugin functionality into core Vagrant
  • Loading branch information
briancain authored Apr 24, 2018
2 parents b365a58 + afc074f commit 5643ba0
Show file tree
Hide file tree
Showing 17 changed files with 1,634 additions and 2 deletions.
12 changes: 12 additions & 0 deletions lib/vagrant/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,18 @@ class SyncedFolderUnusable < VagrantError
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
Expand Down
9 changes: 8 additions & 1 deletion lib/vagrant/machine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ def initialize(name, provider_name, provider_cls, provider_config, provider_opti
# 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
Expand All @@ -159,6 +161,7 @@ def initialize(name, provider_name, provider_cls, provider_config, provider_opti
# 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 ||= {}
Expand All @@ -185,7 +188,7 @@ def action(name, opts=nil)
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)

Expand All @@ -203,6 +206,10 @@ def action(name, opts=nil)
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,
Expand Down
1 change: 1 addition & 0 deletions lib/vagrant/plugin/v2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module V2
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
2 changes: 1 addition & 1 deletion lib/vagrant/plugin/v2/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def execute
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
Expand Down
242 changes: 242 additions & 0 deletions lib/vagrant/plugin/v2/trigger.rb
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions lib/vagrant/util/powershell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,27 @@ def self.execute_cmd(command)
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]
Expand Down
Loading

0 comments on commit 5643ba0

Please sign in to comment.