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.
This commit is contained in:
Gilles Cornu 2015-11-02 09:03:15 +01:00
parent 36d6c430d2
commit dde94a3ce7
6 changed files with 105 additions and 51 deletions

View File

@ -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]

View File

@ -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

View File

@ -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.")

View File

@ -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

View File

@ -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

View File

@ -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).