Allow Ansible provisioner to run reliably in parallel

The Ansible Vagrant provisioner has a race where the inventory file is
updated every time the provisioner runs unless a file is provided.

Therefore if Ansible attempts to provision two nodes in parallel, you
may see the following race:
   * System A writes the inventory file and calls Ansible.
   * System B starts to provision and truncates the file before
     creating a new one.
   * Ansible on system A now attempts to read the inventory
     file, which is blank. Ansible bombs out with "ERROR: provided
     hosts list is empty".

To fix this, we only allow Vagrant to update the inventory file if
it needs to.
This commit is contained in:
Nicholas Randon 2015-04-02 11:18:31 +01:00 committed by Gilles Cornu
parent 04ed8d3d03
commit eb6aa2ac8c
1 changed files with 53 additions and 42 deletions

View File

@ -1,9 +1,12 @@
require "vagrant/util/platform"
require "thread"
module VagrantPlugins
module Ansible
class Provisioner < Vagrant.plugin("2", :provisioner)
@@lock = Mutex.new
def initialize(machine, config)
super
@ -117,55 +120,63 @@ module VagrantPlugins
FileUtils.mkdir_p(generated_inventory_dir) unless File.directory?(generated_inventory_dir)
generated_inventory_file = generated_inventory_dir.join('vagrant_ansible_inventory')
generated_inventory_file.open('w') do |file|
file.write("# Generated by Vagrant\n\n")
inventory = "# Generated by Vagrant\n\n"
@machine.env.active_machines.each do |am|
begin
m = @machine.env.machine(*am)
m_ssh_info = m.ssh_info
if !m_ssh_info.nil?
file.write("#{m.name} ansible_ssh_host=#{m_ssh_info[:host]} ansible_ssh_port=#{m_ssh_info[:port]}\n")
inventory_machines[m.name] = m
else
@logger.error("Auto-generated inventory: Impossible to get SSH information for machine '#{m.name} (#{m.provider_name})'. This machine should be recreated.")
# Let a note about this missing machine
file.write("# MISSING: '#{m.name}' machine was probably removed without using Vagrant. This machine should be recreated.\n")
end
rescue Vagrant::Errors::MachineNotFound => e
@logger.info("Auto-generated inventory: Skip machine '#{am[0]} (#{am[1]})', which is not configured for this Vagrant environment.")
@machine.env.active_machines.each do |am|
begin
m = @machine.env.machine(*am)
m_ssh_info = m.ssh_info
if !m_ssh_info.nil?
inventory += "#{m.name} ansible_ssh_host=#{m_ssh_info[:host]} ansible_ssh_port=#{m_ssh_info[:port]}\n"
inventory_machines[m.name] = m
else
@logger.error("Auto-generated inventory: Impossible to get SSH information for machine '#{m.name} (#{m.provider_name})'. This machine should be recreated.")
# Let a note about this missing machine
inventory += "# MISSING: '#{m.name}' machine was probably removed without using Vagrant. This machine should be recreated.\n"
end
rescue Vagrant::Errors::MachineNotFound => e
@logger.info("Auto-generated inventory: Skip machine '#{am[0]} (#{am[1]})', which is not configured for this Vagrant environment.")
end
end
# Write out groups information.
# All defined groups will be included, but only supported
# machines and defined child groups will be included.
# Group variables are intentionally skipped.
groups_of_groups = {}
defined_groups = []
# Write out groups information.
# All defined groups will be included, but only supported
# machines and defined child groups will be included.
# Group variables are intentionally skipped.
groups_of_groups = {}
defined_groups = []
config.groups.each_pair do |gname, gmembers|
# Require that gmembers be an array
# (easier to be tolerant and avoid error management of few value)
gmembers = [gmembers] if !gmembers.is_a?(Array)
config.groups.each_pair do |gname, gmembers|
# Require that gmembers be an array
# (easier to be tolerant and avoid error management of few value)
gmembers = [gmembers] if !gmembers.is_a?(Array)
if gname.end_with?(":children")
groups_of_groups[gname] = gmembers
defined_groups << gname.sub(/:children$/, '')
elsif !gname.include?(':vars')
defined_groups << gname
file.write("\n[#{gname}]\n")
gmembers.each do |gm|
file.write("#{gm}\n") if inventory_machines.include?(gm.to_sym)
end
end
end
defined_groups.uniq!
groups_of_groups.each_pair do |gname, gmembers|
file.write("\n[#{gname}]\n")
if gname.end_with?(":children")
groups_of_groups[gname] = gmembers
defined_groups << gname.sub(/:children$/, '')
elsif !gname.include?(':vars')
defined_groups << gname
inventory += "\n[#{gname}]\n"
gmembers.each do |gm|
file.write("#{gm}\n") if defined_groups.include?(gm)
inventory += "#{gm}\n" if inventory_machines.include?(gm.to_sym)
end
end
end
defined_groups.uniq!
groups_of_groups.each_pair do |gname, gmembers|
inventory += "\n[#{gname}]\n"
gmembers.each do |gm|
inventory += "#{gm}\n" if defined_groups.include?(gm)
end
end
@@lock.synchronize do
if ! File.exists?(generated_inventory_file) or
inventory != File.read(generated_inventory_file)
generated_inventory_file.open('w') do |file|
file.write(inventory)
end
end
end