From dde94a3ce7bc6f89eabbad4bde2b6f8665a37c72 Mon Sep 17 00:00:00 2001 From: Gilles Cornu Date: Mon, 2 Nov 2015 09:03:15 +0100 Subject: [PATCH] provisioners/ansible: add force_remote_user option The benefits of the following "breaking change" are the following: - default behaviour naturally fits with most common usage (i.e. always connect with Vagrant SSH settings) - the autogenerated inventory is more consistent by providing both the SSH username and private key. - no longer needed to explain how to override Ansible `remote_user` parameters Important: With the `force_remote_user` option, people still can fall back to the former behavior (prior to Vagrant 1.8.0), which means that Vagrant integration capabilities are still quite open and flexible. --- CHANGELOG.md | 14 ++++ plugins/provisioners/ansible/config.rb | 5 +- plugins/provisioners/ansible/provisioner.rb | 21 +++-- .../provisioners/ansible/config_test.rb | 5 ++ .../provisioners/ansible/provisioner_test.rb | 80 ++++++++++++++----- .../source/v2/provisioning/ansible.html.md | 31 +------ 6 files changed, 105 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f78eac8bc..602956e8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,22 @@ FEATURES: - **IPv6 Private Networks**: Private networking now supports IPv6. This only works with VirtualBox and VMware at this point. [GH-6342] +BREAKING CHANGES: + + - the `ansible` provisioner now can override the effective ansible remote user + (i.e. `ansible_ssh_user` setting) to always correspond to the vagrant ssh + username. This change is enabled by default, but we expect this to affect + only a tiny number of people as it corresponds to the common usage. + If you however use different remote usernames in your Ansible plays, tasks, + or custom inventories, you can simply set the option `force_remote_user` to + false to make Vagrant behave the same as before. + + IMPROVEMENTS: + - provisioners/ansible: add new `force_remote_user` option to control whether + `ansible_ssh_user` parameter should be applied or not [GH-6348] + BUG FIXES: - communicator/winrm: respect `boot_timeout` setting [GH-6229] diff --git a/plugins/provisioners/ansible/config.rb b/plugins/provisioners/ansible/config.rb index 784cf48f7..f3aa99deb 100644 --- a/plugins/provisioners/ansible/config.rb +++ b/plugins/provisioners/ansible/config.rb @@ -2,6 +2,7 @@ module VagrantPlugins module Ansible class Config < Vagrant.plugin("2", :config) attr_accessor :playbook + attr_accessor :force_remote_user attr_accessor :extra_vars attr_accessor :inventory_path attr_accessor :ask_sudo_pass @@ -24,6 +25,7 @@ module VagrantPlugins def initialize @playbook = UNSET_VALUE + @force_remote_user = UNSET_VALUE @extra_vars = UNSET_VALUE @inventory_path = UNSET_VALUE @ask_sudo_pass = UNSET_VALUE @@ -44,6 +46,7 @@ module VagrantPlugins def finalize! @playbook = nil if @playbook == UNSET_VALUE + @force_remote_user = true if @force_remote_user != false @extra_vars = nil if @extra_vars == UNSET_VALUE @inventory_path = nil if @inventory_path == UNSET_VALUE @ask_sudo_pass = false unless @ask_sudo_pass == true @@ -56,7 +59,7 @@ module VagrantPlugins @tags = nil if @tags == UNSET_VALUE @skip_tags = nil if @skip_tags == UNSET_VALUE @start_at_task = nil if @start_at_task == UNSET_VALUE - @groups = {} if @groups == UNSET_VALUE + @groups = {} if @groups == UNSET_VALUE @host_key_checking = false unless @host_key_checking == true @raw_arguments = nil if @raw_arguments == UNSET_VALUE @raw_ssh_args = nil if @raw_ssh_args == UNSET_VALUE diff --git a/plugins/provisioners/ansible/provisioner.rb b/plugins/provisioners/ansible/provisioner.rb index 6dd1313e0..296345b39 100644 --- a/plugins/provisioners/ansible/provisioner.rb +++ b/plugins/provisioners/ansible/provisioner.rb @@ -20,13 +20,10 @@ module VagrantPlugins # Ansible provisioner options # - # By default, connect with Vagrant SSH username - options = %W[--user=#{@ssh_info[:username]}] - # Connect with native OpenSSH client # Other modes (e.g. paramiko) are not officially supported, # but can be enabled via raw_arguments option. - options << "--connection=ssh" + options = %W[--connection=ssh] # Increase the SSH connection timeout, as the Ansible default value (10 seconds) # is a bit demanding for some overloaded developer boxes. This is particularly @@ -34,6 +31,16 @@ module VagrantPlugins # is not controlled during vagrant boot process. options << "--timeout=30" + if !config.force_remote_user + # Pass the vagrant ssh username as Ansible default remote user, because + # the ansible_ssh_user parameter won't be added to the auto-generated inventory. + options << "--user=#{@ssh_info[:username]}" + elsif config.inventory_path + # Using an extra variable is the only way to ensure that the Ansible remote user + # is overridden (as the ansible inventory is not under vagrant control) + options << "--extra-vars=ansible_ssh_user='#{@ssh_info[:username]}'" + end + # By default we limit by the current machine, but # this can be overridden by the `limit` option. if config.limit @@ -127,7 +134,11 @@ module VagrantPlugins 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]} ansible_ssh_private_key_file='#{m_ssh_info[:private_key_path][0]}'\n" + forced_ssh_user = "" + if config.force_remote_user + forced_ssh_user = "ansible_ssh_user='#{m_ssh_info[:username]}' " + end + inventory += "#{m.name} ansible_ssh_host=#{m_ssh_info[:host]} ansible_ssh_port=#{m_ssh_info[:port]} #{forced_ssh_user}ansible_ssh_private_key_file='#{m_ssh_info[:private_key_path][0]}'\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.") diff --git a/test/unit/plugins/provisioners/ansible/config_test.rb b/test/unit/plugins/provisioners/ansible/config_test.rb index ab1b6dca0..fdfc54c0a 100644 --- a/test/unit/plugins/provisioners/ansible/config_test.rb +++ b/test/unit/plugins/provisioners/ansible/config_test.rb @@ -18,6 +18,7 @@ describe VagrantPlugins::Ansible::Config do supported_options = %w( ask_sudo_pass ask_vault_pass extra_vars + force_remote_user groups host_key_checking inventory_path @@ -41,6 +42,7 @@ describe VagrantPlugins::Ansible::Config do expect(subject.playbook).to be_nil expect(subject.extra_vars).to be_nil + expect(subject.force_remote_user).to be_true expect(subject.ask_sudo_pass).to be_false expect(subject.ask_vault_pass).to be_false expect(subject.vault_password_file).to be_nil @@ -57,6 +59,9 @@ describe VagrantPlugins::Ansible::Config do expect(subject.raw_ssh_args).to be_nil end + describe "force_remote_user option" do + it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :force_remote_user, true + end describe "host_key_checking option" do it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :host_key_checking, false end diff --git a/test/unit/plugins/provisioners/ansible/provisioner_test.rb b/test/unit/plugins/provisioners/ansible/provisioner_test.rb index 24863adc1..2dce27611 100644 --- a/test/unit/plugins/provisioners/ansible/provisioner_test.rb +++ b/test/unit/plugins/provisioners/ansible/provisioner_test.rb @@ -67,15 +67,17 @@ VF # def self.it_should_set_arguments_and_environment_variables( - expected_args_count = 6, expected_vars_count = 4, expected_host_key_checking = false, expected_transport_mode = "ssh") + expected_args_count = 5, + expected_vars_count = 4, + expected_host_key_checking = false, + expected_transport_mode = "ssh") it "sets implicit arguments in a specific order" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| expect(args[0]).to eq("ansible-playbook") - expect(args[1]).to eq("--user=#{machine.ssh_info[:username]}") - expect(args[2]).to eq("--connection=ssh") - expect(args[3]).to eq("--timeout=30") + expect(args[1]).to eq("--connection=ssh") + expect(args[2]).to eq("--timeout=30") inventory_count = args.count { |x| x =~ /^--inventory-file=.+$/ } expect(inventory_count).to be > 0 @@ -162,13 +164,17 @@ VF end end - def self.it_should_create_and_use_generated_inventory + def self.it_should_create_and_use_generated_inventory(with_ssh_user = true) it "generates an inventory with all active machines" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| expect(config.inventory_path).to be_nil expect(File.exists?(generated_inventory_file)).to be_true inventory_content = File.read(generated_inventory_file) - expect(inventory_content).to include("#{machine.name} ansible_ssh_host=#{machine.ssh_info[:host]} ansible_ssh_port=#{machine.ssh_info[:port]} ansible_ssh_private_key_file='#{machine.ssh_info[:private_key_path][0]}'\n") + if with_ssh_user + expect(inventory_content).to include("#{machine.name} ansible_ssh_host=#{machine.ssh_info[:host]} ansible_ssh_port=#{machine.ssh_info[:port]} ansible_ssh_user='#{machine.ssh_info[:username]}' ansible_ssh_private_key_file='#{machine.ssh_info[:private_key_path][0]}'\n") + else + expect(inventory_content).to include("#{machine.name} ansible_ssh_host=#{machine.ssh_info[:host]} ansible_ssh_port=#{machine.ssh_info[:port]} ansible_ssh_private_key_file='#{machine.ssh_info[:private_key_path][0]}'\n") + end expect(inventory_content).to include("# MISSING: '#{iso_env.machine_names[1]}' machine was probably removed without using Vagrant. This machine should be recreated.\n") } end @@ -276,7 +282,7 @@ VF config.host_key_checking = true end - it_should_set_arguments_and_environment_variables 6, 4, true + it_should_set_arguments_and_environment_variables 5, 4, true end describe "with boolean (flag) options disabled" do @@ -288,7 +294,7 @@ VF config.sudo_user = 'root' end - it_should_set_arguments_and_environment_variables 7 + it_should_set_arguments_and_environment_variables 6 it_should_set_optional_arguments({ "sudo_user" => "--sudo-user=root" }) it "it does not set boolean flag when corresponding option is set to false" do @@ -303,6 +309,7 @@ VF describe "with raw_arguments option" do before do config.sudo = false + config.force_remote_user = false config.skip_tags = %w(foo bar) config.limit = "all" config.raw_arguments = ["--connection=paramiko", @@ -352,12 +359,29 @@ VF it_should_set_arguments_and_environment_variables end + context "with force_remote_user option disabled" do + before do + config.force_remote_user = false + end + + it_should_create_and_use_generated_inventory false # i.e. without setting ansible_ssh_user in inventory + + it_should_set_arguments_and_environment_variables 6 + + it "uses a --user argument to set a default remote user" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| + expect(args).not_to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") + expect(args).to include("--user=#{machine.ssh_info[:username]}") + } + end + end + describe "with inventory_path option" do before do config.inventory_path = existing_file end - it_should_set_arguments_and_environment_variables + it_should_set_arguments_and_environment_variables 6 it "does not generate the inventory and uses given inventory path instead" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| @@ -366,6 +390,26 @@ VF expect(File.exists?(generated_inventory_file)).to be_false } end + + it "uses an --extra-vars argument to force ansible_ssh_user parameter" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| + expect(args).not_to include("--user=#{machine.ssh_info[:username]}") + expect(args).to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") + } + end + + describe "with force_remote_user option disabled" do + before do + config.force_remote_user = false + end + + it "uses a --user argument to set a default remote user" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| + expect(args).not_to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") + expect(args).to include("--user=#{machine.ssh_info[:username]}") + } + end + end end describe "with ask_vault_pass option" do @@ -373,7 +417,7 @@ VF config.ask_vault_pass = true end - it_should_set_arguments_and_environment_variables 7 + it_should_set_arguments_and_environment_variables 6 it "should ask the vault password" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| @@ -387,7 +431,7 @@ VF config.vault_password_file = existing_file end - it_should_set_arguments_and_environment_variables 7 + it_should_set_arguments_and_environment_variables 6 it "uses the given vault password file" do expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| @@ -401,7 +445,7 @@ VF config.raw_ssh_args = ['-o ControlMaster=no', '-o ForwardAgent=no'] end - it_should_set_arguments_and_environment_variables 6, 4 + it_should_set_arguments_and_environment_variables it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "passes custom SSH options via ANSIBLE_SSH_ARGS with the highest priority" do @@ -435,7 +479,7 @@ VF ssh_info[:private_key_path] = ['/path/to/my/key', '/an/other/identity', '/yet/an/other/key'] end - it_should_set_arguments_and_environment_variables 6, 4 + it_should_set_arguments_and_environment_variables it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "passes additional Identity Files via ANSIBLE_SSH_ARGS" do @@ -452,7 +496,7 @@ VF ssh_info[:forward_agent] = true end - it_should_set_arguments_and_environment_variables 6, 4 + it_should_set_arguments_and_environment_variables it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "enables SSH-Forwarding via ANSIBLE_SSH_ARGS" do @@ -468,12 +512,12 @@ VF config.verbose = 'v' end - it_should_set_arguments_and_environment_variables 7 + it_should_set_arguments_and_environment_variables 6 it_should_set_optional_arguments({ "verbose" => "-v" }) it "shows the ansible-playbook command" do expect(machine.env.ui).to receive(:detail).with { |full_command| - expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_HOST_KEY_CHECKING=false ANSIBLE_FORCE_COLOR=true ANSIBLE_SSH_ARGS='-o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --user=testuser --connection=ssh --timeout=30 --limit='machine1' --inventory-file=#{generated_inventory_dir} -v playbook.yml") + expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_HOST_KEY_CHECKING=false ANSIBLE_FORCE_COLOR=true ANSIBLE_SSH_ARGS='-o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --connection=ssh --timeout=30 --limit='machine1' --inventory-file=#{generated_inventory_dir} -v playbook.yml") } end end @@ -520,7 +564,7 @@ VF config.raw_ssh_args = ['-o ControlMaster=no'] end - it_should_set_arguments_and_environment_variables 21, 4, true + it_should_set_arguments_and_environment_variables 20, 4, true it_should_explicitly_enable_ansible_ssh_control_persist_defaults it_should_set_optional_arguments({ "extra_vars" => "--extra-vars=@#{File.expand_path(__FILE__)}", "sudo" => "--sudo", @@ -547,7 +591,7 @@ VF it "shows the ansible-playbook command, with additional quotes when required" do expect(machine.env.ui).to receive(:detail).with { |full_command| - expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_HOST_KEY_CHECKING=true ANSIBLE_FORCE_COLOR=true ANSIBLE_SSH_ARGS='-o IdentitiesOnly=yes -o IdentityFile=/my/key1 -o IdentityFile=/my/key2 -o ForwardAgent=yes -o ControlMaster=no -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --user=testuser --connection=ssh --timeout=30 --limit='machine*:&vagrant:!that_one' --inventory-file=#{generated_inventory_dir} --extra-vars=@#{File.expand_path(__FILE__)} --sudo --sudo-user=deployer -vvv --ask-sudo-pass --ask-vault-pass --vault-password-file=#{File.expand_path(__FILE__)} --tags=db,www --skip-tags=foo,bar --start-at-task='an awesome task' --why-not --su-user=foot --ask-su-pass --limit='all' --private-key=./myself.key playbook.yml") + expect(full_command).to eq("PYTHONUNBUFFERED=1 ANSIBLE_HOST_KEY_CHECKING=true ANSIBLE_FORCE_COLOR=true ANSIBLE_SSH_ARGS='-o IdentitiesOnly=yes -o IdentityFile=/my/key1 -o IdentityFile=/my/key2 -o ForwardAgent=yes -o ControlMaster=no -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --connection=ssh --timeout=30 --limit='machine*:&vagrant:!that_one' --inventory-file=#{generated_inventory_dir} --extra-vars=@#{File.expand_path(__FILE__)} --sudo --sudo-user=deployer -vvv --ask-sudo-pass --ask-vault-pass --vault-password-file=#{File.expand_path(__FILE__)} --tags=db,www --skip-tags=foo,bar --start-at-task='an awesome task' --why-not --su-user=foot --ask-su-pass --limit='all' --private-key=./myself.key playbook.yml") } end end diff --git a/website/docs/source/v2/provisioning/ansible.html.md b/website/docs/source/v2/provisioning/ansible.html.md index 381cf5bd1..719e893fe 100644 --- a/website/docs/source/v2/provisioning/ansible.html.md +++ b/website/docs/source/v2/provisioning/ansible.html.md @@ -61,8 +61,8 @@ Vagrant would generate an inventory file that might look like: ``` # Generated by Vagrant -machine1 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2200 ansible_ssh_private_key_file=/home/.../.vagrant/machines/machine1/virtualbox/private_key -machine2 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2201 ansible_ssh_private_key_file=/home/.../.vagrant/machines/machine2/virtualbox/private_key +machine1 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2200 ansible_ssh_user='vagrant' ansible_ssh_private_key_file='/home/.../'.vagrant/machines/machine1/virtualbox/private_key +machine2 ansible_ssh_host=127.0.0.1 ansible_ssh_port=2201 ansible_ssh_user='vagrant' ansible_ssh_private_key_file='/home/.../'.vagrant/machines/machine2/virtualbox/private_key [group1] machine1 @@ -80,6 +80,7 @@ group2 * 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. * Unmanaged machines and undefined groups are not added to the inventory, to avoid useless Ansible errors (e.g. *unreachable host* or *undefined child group*) * 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. + * Prior to Vagrant 1.8.0, the `ansible_ssh_user` variable was not set in generated inventory, but passed as command line argument to `ansible-playbook` command. See also the `force_remote_user` option to enable the former behavior. For example, `machine3`, `group3` and `group1:vars` in the example below would not be added to the generated inventory file: @@ -220,6 +221,7 @@ 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. As of Vagrant 1.7, `raw_arguments` has the highest priority and its values can potentially override or break other Vagrant settings. * `ansible.raw_ssh_args` can be set to an array of strings corresponding to a list of OpenSSH client parameters (e.g. `['-o ControlMaster=no']`). It is an *unsafe wildcard* that can be used to pass additional SSH settings to Ansible via `ANSIBLE_SSH_ARGS` environment variable. * `ansible.host_key_checking` can be set to `true` which will enable host key checking. As of Vagrant 1.5, the default value is `false` and as of Vagrant 1.7 the user known host file (e.g. `~/.ssh/known_hosts`) is no longer read nor modified. In other words: by default, the Ansible provisioner behaves the same as Vagrant native commands (e.g `vagrant ssh`). +* `ansible.force_remote_user` can be set to `false` which will enable the `remote_user` parameters of your Ansible plays or tasks. Otherwise, Vagrant will set the `ansible_ssh_user` setting in the generated inventory, or as an extra variable when a static inventory is used. In this case, all the Ansible `remote_user` parameters will be overridden by the value of `config.ssh.username` of the [Vagrant SSH Settings](/v2/vagrantfile/ssh_settings.html). ## Tips and Tricks @@ -273,31 +275,6 @@ As `ansible-playbook` command looks for local `ansible.cfg` configuration file i 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 does the Ansible provisioner connect as the wrong user? - -It is good to know that the following Ansible settings always override the `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 the Ansible documentation might lead to this problem, as `root` is used as the remote user in many [examples](http://docs.ansible.com/playbooks_intro.html#hosts-and-users). - -Example of an 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. -``` - -In a situation like the above, to override the `remote_user` specified in a play you can use the following line in your Vagrantfile `vm.provision` block: - -``` -ansible.extra_vars = { ansible_ssh_user: 'vagrant' } -``` - ### Force Paramiko Connection Mode The Ansible provisioner is implemented with native OpenSSH support in mind, and there is no official support for [paramiko](https://github.com/paramiko/paramiko/) (A native Python SSHv2 protocol library).