From eb6aa2ac8c6238dc4aff71bd5b4463b992bc7189 Mon Sep 17 00:00:00 2001 From: Nicholas Randon Date: Thu, 2 Apr 2015 11:18:31 +0100 Subject: [PATCH] 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. --- plugins/provisioners/ansible/provisioner.rb | 95 ++++++++++++--------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/plugins/provisioners/ansible/provisioner.rb b/plugins/provisioners/ansible/provisioner.rb index 1aa7d57a1..3d699ac57 100644 --- a/plugins/provisioners/ansible/provisioner.rb +++ b/plugins/provisioners/ansible/provisioner.rb @@ -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