diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e4f48173..249891a71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,8 @@ IMPROVEMENTS: scripts [GH-6185] - provisioners/shell: add `env` option [GH-6588, GH-6516] - provisioners/ansible+ansible_local: add support for ansible-galaxy [GH-2718] + - provisioners/ansible+ansible_local: add support for group and host variables + in the generated inventory [GH-6619] - provisioners/ansible: add support for WinRM settings [GH-5086] - provisioners/ansible: add new `force_remote_user` option to control whether `ansible_ssh_user` parameter should be applied or not [GH-6348] diff --git a/plugins/provisioners/ansible/config/base.rb b/plugins/provisioners/ansible/config/base.rb index 2cefe4a89..14ae1a3e6 100644 --- a/plugins/provisioners/ansible/config/base.rb +++ b/plugins/provisioners/ansible/config/base.rb @@ -9,6 +9,7 @@ module VagrantPlugins attr_accessor :galaxy_role_file attr_accessor :galaxy_roles_path attr_accessor :galaxy_command + attr_accessor :host_vars attr_accessor :groups attr_accessor :inventory_path attr_accessor :limit @@ -27,6 +28,7 @@ module VagrantPlugins @galaxy_role_file = UNSET_VALUE @galaxy_roles_path = UNSET_VALUE @galaxy_command = UNSET_VALUE + @host_vars = UNSET_VALUE @groups = UNSET_VALUE @inventory_path = UNSET_VALUE @limit = UNSET_VALUE @@ -46,6 +48,7 @@ module VagrantPlugins @galaxy_role_file = nil if @galaxy_role_file == UNSET_VALUE @galaxy_roles_path = nil if @galaxy_roles_path == UNSET_VALUE @galaxy_command = GALAXY_COMMAND_DEFAULT if @galaxy_command == UNSET_VALUE + @host_vars = {} if @host_vars == UNSET_VALUE @groups = {} if @groups == UNSET_VALUE @inventory_path = nil if @inventory_path == UNSET_VALUE @limit = nil if @limit == UNSET_VALUE diff --git a/plugins/provisioners/ansible/provisioner/base.rb b/plugins/provisioners/ansible/provisioner/base.rb index 7a455b2bf..6671f0218 100644 --- a/plugins/provisioners/ansible/provisioner/base.rb +++ b/plugins/provisioners/ansible/provisioner/base.rb @@ -70,11 +70,29 @@ module VagrantPlugins end end + def get_inventory_host_vars_string(machine_name) + # In Ruby, Symbol and String values are different, but + # Vagrant has to unify them for better user experience. + vars = config.host_vars[machine_name.to_sym] + if !vars + vars = config.host_vars[machine_name.to_s] + end + s = nil + if vars.is_a?(Hash) + s = vars.each.collect{ |k, v| "#{k}=#{v}" }.join(" ") + elsif vars.is_a?(Array) + s = vars.join(" ") + elsif vars.is_a?(String) + s = vars + end + if s and !s.empty? then s else nil end + end + def generate_inventory inventory = "# Generated by Vagrant\n\n" - # This "abstract" step must fill the @inventory_machines list - # and return the list of supported host(s) + # This "abstract" step must fill the @inventory_machines list + # and return the list of supported host(s) inventory += generate_inventory_machines inventory += generate_inventory_groups @@ -88,21 +106,31 @@ module VagrantPlugins # 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. def generate_inventory_groups groups_of_groups = {} defined_groups = [] + group_vars = {} inventory_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.is_a?(Symbol) + gname = gname.to_s + end + + if gmembers.is_a?(String) + gmembers = gmembers.split(/\s+/) + elsif gmembers.is_a?(Hash) + gmembers = gmembers.each.collect{ |k, v| "#{k}=#{v}" } + elsif !gmembers.is_a?(Array) + gmembers = [] + end if gname.end_with?(":children") groups_of_groups[gname] = gmembers defined_groups << gname.sub(/:children$/, '') - elsif !gname.include?(':vars') + elsif gname.end_with?(":vars") + group_vars[gname] = gmembers + else defined_groups << gname inventory_groups += "\n[#{gname}]\n" gmembers.each do |gm| @@ -119,6 +147,12 @@ module VagrantPlugins end end + group_vars.each_pair do |gname, gmembers| + if defined_groups.include?(gname.sub(/:vars$/, "")) + inventory_groups += "\n[#{gname}]\n" + gmembers.join("\n") + "\n" + end + end + return inventory_groups end diff --git a/plugins/provisioners/ansible/provisioner/guest.rb b/plugins/provisioners/ansible/provisioner/guest.rb index a82bf7562..6f0b43f95 100644 --- a/plugins/provisioners/ansible/provisioner/guest.rb +++ b/plugins/provisioners/ansible/provisioner/guest.rb @@ -128,6 +128,8 @@ module VagrantPlugins else machines += "#{machine_name}\n" end + host_vars = get_inventory_host_vars_string(machine_name) + machines.sub!(/\n$/, " #{host_vars}\n") if host_vars end end diff --git a/plugins/provisioners/ansible/provisioner/host.rb b/plugins/provisioners/ansible/provisioner/host.rb index 54928bdfc..dcce98fb8 100644 --- a/plugins/provisioners/ansible/provisioner/host.rb +++ b/plugins/provisioners/ansible/provisioner/host.rb @@ -151,12 +151,15 @@ module VagrantPlugins # Call only once the SSH and WinRM info computation # Note that machines configured with WinRM communicator, also have a "partial" ssh_info. m_ssh_info = m.ssh_info + host_vars = get_inventory_host_vars_string(m.name) if m.config.vm.communicator == :winrm m_winrm_net_info = CommunicatorWinRM::Helper.winrm_info(m) # can raise a WinRMNotReady exception... machines += get_inventory_winrm_machine(m, m_winrm_net_info) + machines.sub!(/\n$/, " #{host_vars}\n") if host_vars @inventory_machines[m.name] = m elsif !m_ssh_info.nil? machines += get_inventory_ssh_machine(m, m_ssh_info) + machines.sub!(/\n$/, " #{host_vars}\n") if host_vars @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.") diff --git a/test/unit/plugins/provisioners/ansible/config_test.rb b/test/unit/plugins/provisioners/ansible/config_test.rb index 78e352914..f836382a3 100644 --- a/test/unit/plugins/provisioners/ansible/config_test.rb +++ b/test/unit/plugins/provisioners/ansible/config_test.rb @@ -30,6 +30,7 @@ describe VagrantPlugins::Ansible::Config::Host do galaxy_roles_path groups host_key_checking + host_vars inventory_path limit playbook @@ -62,6 +63,7 @@ describe VagrantPlugins::Ansible::Config::Host do expect(subject.tags).to be_nil expect(subject.skip_tags).to be_nil expect(subject.start_at_task).to be_nil + expect(subject.host_vars).to eq({}) expect(subject.groups).to eq({}) expect(subject.host_key_checking).to be_false expect(subject.raw_arguments).to be_nil diff --git a/test/unit/plugins/provisioners/ansible/provisioner_test.rb b/test/unit/plugins/provisioners/ansible/provisioner_test.rb index 55b40cd18..9a8188f0f 100644 --- a/test/unit/plugins/provisioners/ansible/provisioner_test.rb +++ b/test/unit/plugins/provisioners/ansible/provisioner_test.rb @@ -234,42 +234,110 @@ VF end end + describe "with host_vars option" do + it_should_create_and_use_generated_inventory + + it "adds host variables (given in Hash format) to the generated inventory" do + config.host_vars = { + machine1: {"http_port" => 80, "maxRequestsPerChild" => 808} + } + expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| + inventory_content = File.read(generated_inventory_file) + expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 maxRequestsPerChild=808") + } + end + + it "adds host variables (given in Array format) to the generated inventory" do + config.host_vars = { + machine1: ["http_port=80", "maxRequestsPerChild=808"] + } + expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| + inventory_content = File.read(generated_inventory_file) + expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 maxRequestsPerChild=808") + } + end + + it "adds host variables (given in String format) to the generated inventory " do + config.host_vars = { + :machine1 => "http_port=80 maxRequestsPerChild=808" + } + expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| + inventory_content = File.read(generated_inventory_file) + expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 maxRequestsPerChild=808") + } + end + + it "retrieves the host variables by machine name, also in String format" do + config.host_vars = { + "machine1" => "http_port=80 maxRequestsPerChild=808" + } + expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| + inventory_content = File.read(generated_inventory_file) + expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 maxRequestsPerChild=808") + } + end + end + describe "with groups option" do it_should_create_and_use_generated_inventory it "adds group sections to the generated inventory" do config.groups = { - "group1" => "#{machine.name}", - "group1:children" => 'bar', + "group1" => "machine1", + "group1:children" => 'bar group3', "group2" => [iso_env.machine_names[1]], "group3" => ["unknown", "#{machine.name}"], + group4: [machine.name], "bar" => ["#{machine.name}", "group3"], - "bar:children" => ["group1", "group2", "group3", "group4"], - "bar:vars" => ["myvar=foo"], + "bar:children" => ["group1", "group2", "group3", "group5"], } expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| inventory_content = File.read(generated_inventory_file) - # Group variables are intentionally not supported in generated inventory - expect(inventory_content).not_to match(/^\[.*:vars\]$/) - - # Accept String instead of Array for group that contains a single item - expect(inventory_content).to include("[group1]\n#{machine.name}\n") - expect(inventory_content).to include("[group1:children]\nbar\n") + # Accept String instead of Array for group member list + expect(inventory_content).to include("[group1]\nmachine1\n\n") + expect(inventory_content).to include("[group1:children]\nbar\ngroup3\n\n") # Skip "lost" machines expect(inventory_content).to include("[group2]\n\n") # Skip "unknown" machines - expect(inventory_content).to include("[group3]\n#{machine.name}\n") + expect(inventory_content).to include("[group3]\n#{machine.name}\n\n") + + # Accept Symbol datatype for group names + expect(inventory_content).to include("[group4]\n#{machine.name}\n\n") # Don't mix group names and host names - expect(inventory_content).to include("[bar]\n#{machine.name}\n") + expect(inventory_content).to include("[bar]\n#{machine.name}\n\n") # A group of groups only includes declared groups - expect(inventory_content).not_to match(/^group4$/) - expect(inventory_content).to include("[bar:children]\ngroup1\ngroup2\ngroup3\n") + expect(inventory_content).not_to include("group5") + expect(inventory_content).to match(Regexp.quote("[bar:children]\ngroup1\ngroup2\ngroup3\n") + "$") + } + end + + it "adds group vars to the generated inventory" do + config.groups = { + "group1" => [machine.name], + "group2" => [machine.name], + "group3" => [machine.name], + "group1:vars" => {"hashvar1" => "hashvalue1", "hashvar2" => "hashvalue2"}, + "group2:vars" => ["arrayvar1=arrayvalue1", "arrayvar2=arrayvalue2"], + "group3:vars" => "stringvar1=stringvalue1 stringvar2=stringvalue2", + } + + expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| + inventory_content = File.read(generated_inventory_file) + + # Hash syntax + expect(inventory_content).to include("[group1:vars]\nhashvar1=hashvalue1\nhashvar2=hashvalue2\n") + + # Array syntax + expect(inventory_content).to include("[group2:vars]\narrayvar1=arrayvalue1\narrayvar2=arrayvalue2\n") + + # Single string syntax + expect(inventory_content).to include("[group3:vars]\nstringvar1=stringvalue1\nstringvar2=stringvalue2\n") } end end diff --git a/website/docs/source/v2/provisioning/ansible_common.html.md b/website/docs/source/v2/provisioning/ansible_common.html.md index cb8daf55e..44b787e7e 100644 --- a/website/docs/source/v2/provisioning/ansible_common.html.md +++ b/website/docs/source/v2/provisioning/ansible_common.html.md @@ -31,6 +31,23 @@ Some of these options are for advanced usage only and should not be used unless ``` These variables take the highest precedence over any other variables. +- `host_vars` (hash) - Set of inventory host variables to be included in the [auto-generated inventory file](http://docs.ansible.com/ansible/intro_inventory.html#host-variables). + + Example: + + ```ruby + ansible.host_vars = { + "host1" => {"http_port" => 80, + "maxRequestsPerChild" => 808}, + "host2" => {"http_port" => 303, + "maxRequestsPerChild" => 909} + } + ``` + + Notes: + + - This option has no effect when the `inventory_path` option is defined. + - `groups` (hash) - Set of inventory groups to be included in the [auto-generated inventory file](/v2/provisioning/ansible_intro.html). Example: @@ -41,6 +58,15 @@ Some of these options are for advanced usage only and should not be used unless "db" => ["vm3"] } ``` + Example with [group variables](http://docs.ansible.com/ansible/intro_inventory.html#group-variables): + + ```ruby + ansible.groups = { + "atlanta" => ["host1", "host2"], + "atlanta:vars" => {"ntp_server" => "ntp.atlanta.example.com", + "proxy" => "proxy.atlanta.example.com"} + } + ``` Notes: diff --git a/website/docs/source/v2/provisioning/ansible_intro.html.md b/website/docs/source/v2/provisioning/ansible_intro.html.md index b49568f07..daa21aeb4 100644 --- a/website/docs/source/v2/provisioning/ansible_intro.html.md +++ b/website/docs/source/v2/provisioning/ansible_intro.html.md @@ -107,9 +107,39 @@ default ansible_connection=local Note that the generated inventory file is uploaded to the guest VM in a subdirectory of [`tmp_path`](/v2/provisioning/ansible_local.html), e.g. `/tmp/vagrant-ansible/inventory/vagrant_ansible_local_inventory`. +**Host variables:** + +As of Vagrant 1.8.0, the [`host_vars`](/v2/provisioning/ansible_common.html) option can be used to set [variables for individual hosts](http://docs.ansible.com/ansible/intro_inventory.html#host-variables) in the generated inventory file (see also the notes on group variables below). + +``` +Vagrant.configure(2) do |config| + config.vm.define "host1" + config.vm.define "host2" + config.vm.provision "ansible" do |ansible| + ansible.playbook = "playbook.yml" + ansible.host_vars = { + "host1" => {"http_port" => 80, + "maxRequestsPerChild" => 808}, + "host2" => {"http_port" => 303, + "maxRequestsPerChild" => 909} + } + end +end +``` + +Generated inventory: + +``` +# Generated by Vagrant + +host1 ansible_ssh_host=... http_port=80 maxRequestsPerChild=808 +host2 ansible_ssh_host=... http_port=303 maxRequestsPerChild=909 +``` + **How to generate Inventory Groups:** The [`groups`](/v2/provisioning/ansible_common.html) option can be used to pass a hash of group names and group members to be included in the generated inventory file. +As of Vagrant 1.8.0, it is also possible to specify [group variables](http://docs.ansible.com/ansible/intro_inventory.html#group-variables). With this configuration example: @@ -126,7 +156,9 @@ Vagrant.configure(2) do |config| ansible.groups = { "group1" => ["machine1"], "group2" => ["machine2"], - "all_groups:children" => ["group1", "group2"] + "all_groups:children" => ["group1", "group2"], + "group1:vars" => {"variable1" => 9, + "variable2" => "example"} } end end @@ -149,22 +181,25 @@ machine2 [all_groups:children] group1 group2 + +[group1:vars] +variable1=9 +variable2=example ``` **Notes:** - Prior to Vagrant 1.7.3, the `ansible_ssh_private_key_file` variable was not set in generated inventory, but passed as command line argument to `ansible-playbook` command. - - The generation of group variables blocks (e.g. `[group1:vars]`) are intentionally not supported, as it is [not recommended to store group variables in the main inventory file](http://docs.ansible.com/intro_inventory.html#splitting-out-host-and-group-specific-data). A good practice is to store these group (or host) variables in `YAML` files stored in `group_vars/` or `host_vars/` directories in the playbook (or inventory) directory. + - The generation of group variables blocks (e.g. `[group1:vars]`) is only possible since Vagrant 1.8.0. Note however that setting variables directly in the inventory is not the [preferred practice in Ansible](http://docs.ansible.com/intro_inventory.html#splitting-out-host-and-group-specific-data). If possible, group (or host) variables should be set in `YAML` files stored in the `group_vars/` or `host_vars/` directories in the playbook (or inventory) directory instead. - Unmanaged machines and undefined groups are not added to the inventory, to avoid useless Ansible errors (e.g. *unreachable host* or *undefined child group*) -For example, `machine3`, `group3` and `group1:vars` in the example below would not be added to the generated inventory file: +For example, `machine3` and `group3` in the example below would not be added to the generated inventory file: ``` ansible.groups = { "group1" => ["machine1"], "group2" => ["machine2", "machine3"], - "all_groups:children" => ["group1", "group2", "group3"], - "group1:vars" => { "variable1" => 9, "variable2" => "example" } + "all_groups:children" => ["group1", "group2", "group3"] } ```