diff --git a/plugins/provisioners/ansible/provisioner.rb b/plugins/provisioners/ansible/provisioner.rb index aae761f0a..f665fdb4a 100644 --- a/plugins/provisioners/ansible/provisioner.rb +++ b/plugins/provisioners/ansible/provisioner.rb @@ -2,6 +2,7 @@ module VagrantPlugins module Ansible class Provisioner < Vagrant.plugin("2", :provisioner) def provision + @logger = Log4r::Logger.new("vagrant::provisioners::ansible") ssh = @machine.ssh_info # Connect with Vagrant user (unless --user or --private-key are @@ -10,6 +11,10 @@ module VagrantPlugins # TODO: multiple private key support options = %W[--private-key=#{ssh[:private_key_path][0]} --user=#{ssh[:username]}] + # By default we limit by the current machine. + # This can be overridden by the limit config option. + options << "--limit=#{@machine.name}" + # Joker! Not (yet) supported arguments can be passed this way. options.concat(self.as_array(config.raw_arguments)) if config.raw_arguments @@ -63,37 +68,63 @@ module VagrantPlugins ssh = @machine.ssh_info - generated_inventory_file = - @machine.env.root_path.join("vagrant_ansible_inventory_#{machine.name}") + # Managed machines + inventory_machines = {} + + generated_inventory_dir = @machine.env.local_data_path.join(File.join(%w(provisioners ansible inventory))) + 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") - file.write("#{machine.name} ansible_ssh_host=#{ssh[:host]} ansible_ssh_port=#{ssh[:port]}\n") - # Write out groups information. Only include current - # machine and its groups to avoid Ansible errors on - # provisioning. - groups_of_groups = {} - included_groups = [] - - config.groups.each_pair do |gname, gmembers| - if gname.end_with?(":children") - groups_of_groups[gname] = gmembers - elsif gmembers.include?("#{machine.name}") - included_groups << gname - file.write("\n[#{gname}]\n") - file.write("#{machine.name}\n") + @machine.env.active_machines.each do |am| + begin + m = @machine.env.machine(*am) + 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.") end end - groups_of_groups.each_pair do |gname, gmembers| - unless (included_groups & gmembers).empty? + # 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) + + 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 included_groups.include?(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") + gmembers.each do |gm| + file.write("#{gm}\n") if defined_groups.include?(gm) + end + end end return generated_inventory_file.to_s diff --git a/website/docs/source/v2/provisioning/ansible.html.md b/website/docs/source/v2/provisioning/ansible.html.md index 9ad057116..4f2ff5185 100644 --- a/website/docs/source/v2/provisioning/ansible.html.md +++ b/website/docs/source/v2/provisioning/ansible.html.md @@ -8,7 +8,7 @@ sidebar_current: "provisioning-ansible" **Provisioner name: `"ansible"`** The ansible provisioner allows you to provision the guest using -[Ansible](http://ansible.cc) playbooks. +[Ansible](http://ansible.com) playbooks. Ansible playbooks are [YAML](http://en.wikipedia.org/wiki/YAML) documents that comprise the set of steps to be orchestrated on one or more machines. This documentation @@ -28,21 +28,55 @@ a single page of documentation. ## Inventory File When using Ansible, it needs to know on which machines a given playbook should run. It does -this by way of an inventory file which lists those machines. In the context of Vagrant, -there are two ways to approach working with inventory files. The first and simplest option -is to not provide one to Vagrant at all. Vagrant will generate inventory files for each -virtual machine it manages, and use them for provisioning machines. Generated inventory files -are created adjacent to your Vagrantfile, named using the machine names set in your Vagrantfile. +this by way of an [inventory](http://docs.ansible.com/intro_inventory.html) file which lists those machines. In the context of Vagrant, +there are two ways to approach working with inventory files. -The second option is for situations where you'd like to have more than one virtual machine -in a single inventory file, perhaps leveraging more complex playbooks or inventory grouping. -If you provide the `ansible.inventory_path` option referencing a specific inventory file -dedicated to your Vagrant project, that one will be used instead of generating them. -Such an inventory file for use with Vagrant might look like: +The first and simplest option is to not provide one to Vagrant at all. Vagrant will generate an +inventory file encompassing all of the virtual machine it manages, and use it for provisioning +machines. The generated inventory file is stored as part of your local Vagrant environment in `.vagrant/provisioners/ansible/inventory/vagrant_ansible_inventory`. + +The `ansible.groups` option can be used to pass a hash of group +names and group members to be included in the generated inventory file. Group variables +are intentionally not supported, as this practice is not recommended. +For example: ``` -[vagrant] -192.168.111.222 +ansible.groups = { + "group1" => ["machine1"], + "group2" => ["machine2", "machine3"], + "all_groups:children" => ["group1", "group2", "group3"] +} +``` + +Note that unmanaged machines and undefined groups are not added to the inventory. +For example, `group3` in the above example would not be added to the inventory file. + +A generated inventory might look like: + +``` +# Generated by Vagrant + +machine1 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2200 +machine2 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2201 + +[group1] +machine1 + +[group2] +machine2 + +[all_groups:children] +group1 +group2 +``` + +The second option is for situations where you'd like to have more control over the inventory management. +With the `ansible.inventory_path` option, you can reference a specific inventory resource (e.g. a static inventory file, a [dynamic inventory script](http://docs.ansible.com/intro_dynamic_inventory.html) or even [multiple inventories stored in the same directory](http://docs.ansible.com/intro_dynamic_inventory.html#using-multiple-inventory-sources)). Vagrant will then use this inventory information instead of generating it. + +A very simple inventory file for use with Vagrant might look like: + +``` +default ansible_ssh_host=192.168.111.222 ``` Where the above IP address is one set in your Vagrantfile: @@ -51,13 +85,16 @@ Where the above IP address is one set in your Vagrantfile: config.vm.network :private_network, ip: "192.168.111.222" ``` +Note that machine names in `Vagrantfile` and `ansible.inventory_path` file should correspond, +unless you use `ansible.limit` option to reference the correct machines. + ## Playbook The second component of a successful Ansible provisioner setup is the Ansible playbook which contains the steps that should be run on the guest. Ansible's -[playbook documentation](http://ansible.cc/docs/playbooks.html) goes into great +[playbook documentation](http://docs.ansible.com/playbooks.html) goes into great detail on how to author playbooks, and there are a number of -[best practices](http://ansible.cc/docs/bestpractices.html) that can be applied to use +[best practices](http://docs.ansible.com/playbooks_best_practices.html) that can be applied to use Ansible's powerful features effectively. A playbook that installs and starts (or restarts if it was updated) the NTP daemon via YUM looks like: @@ -75,7 +112,7 @@ if it was updated) the NTP daemon via YUM looks like: ``` You can of course target other operating systems that don't have YUM by changing the -playbook tasks. Ansible ships with a number of [modules](http://ansible.cc/docs/modules.html) +playbook tasks. Ansible ships with a number of [modules](http://docs.ansible.com/modules.html) that make running otherwise tedious tasks dead simple. ## Running Ansible @@ -90,7 +127,6 @@ Vagrant.configure("2") do |config| end ``` -This causes Vagrant to run the `playbook.yml` playbook against all hosts in the inventory file. Since an Ansible playbook can include many files, you may also collect the related files in a directory structure like this: @@ -114,6 +150,13 @@ Vagrant.configure("2") do |config| end ``` +Vagrant will try to run the `playbook.yml` playbook against all machines defined in your Vagrantfile. + +**Backward Compatibility Note**: + +Up to Vagrant 1.4, the Ansible provisioner could potentially connect (multiple times) to all hosts from the inventory file. +This behaviour is still possible by setting `ansible.limit = 'all'` (see more details below). + ## Additional Options The Ansible provisioner also includes a number of additional options that can be set, @@ -135,7 +178,9 @@ all of which get passed to the `ansible-playbook` command that ships with Ansibl * `ansible.sudo_user` can be set to a string containing a username on the guest who should be used by the sudo command. * `ansible.ask_sudo_pass` can be set to `true` to require Ansible to prompt for a sudo password. -* `ansible.limit` can be set to a string or an array of machines or groups from the inventory file to further narrow down which hosts are affected. +* `ansible.limit` can be set to a string or an array of machines or groups from the inventory file to further control which hosts are affected. Note that: + * As Vagrant 1.5, the machine name (taken from Vagrantfile) is set as **default limit** to ensure that `vagrant provision` steps only affect the expected machine. Setting `ansible.limit` will override this default. + * Setting `ansible.limit = 'all'` can be used to make Ansible connects to all machines from the inventory file. * `ansible.verbose` can be set to increase Ansible's verbosity to obtain detailed logging: * `'v'`, verbose mode * `'vv'` @@ -147,14 +192,56 @@ by the sudo command. * `ansible.raw_arguments` can be set to an array of strings corresponding to a list of `ansible-playbook` arguments (e.g. `['--check', '-M /my/modules']`). It is an *unsafe wildcard* that can be used to apply Ansible options that are not (yet) supported by this Vagrant provisioner. Following precedence rules apply: * Any supported options (described above) will override conflicting `raw_arguments` value (e.g. `--tags` or `--start-at-task`) * Vagrant default user authentication can be overridden via `raw_arguments` (with custom values for `--user` and `--private-key`) -* `ansible.groups` can be used to pass a hash of group names and group members to be included in the generated inventory file. For example: - - ``` - ansible.groups = { - "group1" => ["machine1"], - "group2" => ["machine2", "machine3"], - "all_groups:children" => ["group1", "group2"] - } - ``` - Note that only the current machine and its related groups will be added to the inventory file. * `ansible.host_key_checking` can be set to `false` which will disable host key checking and prevent `"FAILED: (25, 'Inappropriate ioctl for device')"` errors from being reported during provisioner runs. The default value is `true`, which matches the default behavior of Ansible 1.2.1 and later. + +## Tips and Tricks + +### Ansible Parallel Execution + +Vagrant is designed to provision [multi-machine environments](/v2/multi-machine) in sequence, but the following configuration pattern can be used to take advantage of Ansible parallelism: + +``` + config.vm.define 'machine2' do |machine| + machine.vm.hostname = 'machine2' + machine.vm.network "private_network", ip: "192.168.77.22" + end + + config.vm.define 'machine1' do |machine| + machine.vm.hostname = 'machine1' + machine.vm.network "private_network", ip: "192.168.77.21" + + machine.vm.provision :ansible do |ansible| + ansible.playbook = "playbook.yml" + + # Disable default limit (required with Vagrant 1.5+) + ansible.limit = 'all' + end + end +``` + +### Provide a local `ansible.cfg` file + +Certain settings in Ansible are (only) adjustable via a [configuration file](http://docs.ansible.com/intro_configuration.html), and you might want to ship such a file in your Vagrant project. + +As `ansible-playbook` command looks for local [`ansible.cfg`] configuration file in its *current directory* (but not in the directory that contains the main playbook), you have to store this file adjacent to your Vagrantfile. + +Note that it is also possible to reference an Ansible configuration file via `ANSIBLE_CONFIG` environment variable, if you want to be flexible about the location of this file. + +### Why Ansible provisioner does connect with a wrong user ? + +It is good to know that following Ansible settings always override `config.ssh.username` option defined in [Vagrant SSH Settings](/v2/vagrantfile/ssh_settings.html): + +* `ansible_ssh_user` variable +* `remote_user` (or `user`) play attribute +* `remote_user` task attribute + +Be aware that copying snippets from Ansible documentation might lead to this problem, as `root` is used as remote user in many [examples](http://docs.ansible.com/playbooks_intro.html#hosts-and-users). + +Example of SSH error (with `vvv` log level), where an undefined remote user `xyz` has replaced `vagrant`: + +``` +TASK: [my_role | do something] ***************** +<127.0.0.1> ESTABLISH CONNECTION FOR USER: xyz +<127.0.0.1> EXEC ['ssh', '-tt', '-vvv', '-o', 'ControlMaster=auto',... +fatal: [ansible-devbox] => SSH encountered an unknown error. We recommend you re-run the command using -vvvv, which will enable SSH debugging output to help diagnose the issue. +```