From 8834afbd8e6198b49b0a0ad2b3756c139a8d27b7 Mon Sep 17 00:00:00 2001 From: Gilles Cornu Date: Sun, 13 Nov 2016 20:58:26 +0100 Subject: [PATCH] provisioners/ansible(both): Add compatibility mode With this change, it is now possible to get rid of many deprecation messages successively introduced in Ansible 1.9, and 2.0. More interesting, the generated inventory will contain the recommended variable names (e.g. `ansible_host` instead of `ansible_ssh_host`) when the compatibility mode is set to '2.0'. Details: - Add `compatibility_mode` option to control the Ansible parameters format to be used. The value corresponds to the minimal version supported. For the moment, possible values are '1.8' (corresponding to Vagrant's former behaviour) or '2.0'. Note that a dynamic inventory generated in compatibility mode '2.0' is not supported by Ansible 1.x. On the other hand, Ansible 2.x so far supports inventory format generated by the compatibility mode '1.8'. - Add compatibility mode auto-detection, based on the available Ansible version. This is the default behaviour in order to bring a maximum of user friendliness. The drawback of this approach is to let potential compatibility breaking risks, for `ansible` provisioner setups that already integrate Ansible 2.x **AND** rely on the existence of the generated `_ssh` variable names. Thanks to the vagrant warnings (and its release notes), I argue that it is worth to offer auto-detection by default, which offers a sweet transition to most users. - Add `become`, `become_user` and `ask_become_pass` options and their backwards compatible aliases. The legacy options are now deprecated. Note that we intentionally didn't provide a '1.9' compatibility mode, as it would add extra-complexity for practically no added-value. To my knowledge, the Ansible 2.x series haven't introduced yet any major changes or deprecations that would motivate to introduce a higher version compatibility mode (to be confirmed/verified). Resolve GH-6570 Still Pending: - Optimization: Reduce the number of `ansible` command executions. Currently two exec calls will be performed when the compatibility mode auto-detection is enabled (i.e. by default). We could make the provisioner a little bit smarter to only execute `ansible` only once in any situation (by combining "presence" and "version" checks). - User-friendliness: Add better validator on `compatibility_mode` option, and shows a warning or an error instead of the silent fallback on the auto-detection modus. - Test coverage: All the added behaviours are not fully covered yet. --- plugins/provisioners/ansible/config/base.rb | 39 ++- plugins/provisioners/ansible/config/host.rb | 15 +- plugins/provisioners/ansible/constants.rb | 9 + .../provisioners/ansible/provisioner/base.rb | 75 ++++- .../provisioners/ansible/provisioner/guest.rb | 17 +- .../provisioners/ansible/provisioner/host.rb | 43 ++- templates/locales/en.yml | 14 + .../provisioners/ansible/config/guest_test.rb | 5 +- .../provisioners/ansible/config/host_test.rb | 16 +- .../provisioners/ansible/config/shared.rb | 39 ++- .../provisioners/ansible/provisioner_test.rb | 274 ++++++++++++++---- .../source/docs/provisioning/ansible.html.md | 9 +- .../docs/provisioning/ansible_common.html.md | 41 ++- 13 files changed, 503 insertions(+), 93 deletions(-) create mode 100644 plugins/provisioners/ansible/constants.rb diff --git a/plugins/provisioners/ansible/config/base.rb b/plugins/provisioners/ansible/config/base.rb index 36fb5b903..1b58a60d4 100644 --- a/plugins/provisioners/ansible/config/base.rb +++ b/plugins/provisioners/ansible/config/base.rb @@ -1,3 +1,5 @@ +require_relative "../constants" + module VagrantPlugins module Ansible module Config @@ -6,6 +8,9 @@ module VagrantPlugins GALAXY_COMMAND_DEFAULT = "ansible-galaxy install --role-file=%{role_file} --roles-path=%{roles_path} --force".freeze PLAYBOOK_COMMAND_DEFAULT = "ansible-playbook".freeze + attr_accessor :become + attr_accessor :become_user + attr_accessor :compatibility_mode attr_accessor :config_file attr_accessor :extra_vars attr_accessor :galaxy_role_file @@ -20,13 +25,28 @@ module VagrantPlugins attr_accessor :raw_arguments attr_accessor :skip_tags attr_accessor :start_at_task - attr_accessor :sudo - attr_accessor :sudo_user attr_accessor :tags attr_accessor :vault_password_file attr_accessor :verbose + # + # Deprecated options + # + alias :sudo :become + def sudo=(value) + show_deprecation_info 'sudo', 'become' + @become = value + end + alias :sudo_user :become_user + def sudo_user=(value) + show_deprecation_info 'sudo_user', 'become_user' + @become_user = value + end + def initialize + @become = UNSET_VALUE + @become_user = UNSET_VALUE + @compatibility_mode = UNSET_VALUE @config_file = UNSET_VALUE @extra_vars = UNSET_VALUE @galaxy_role_file = UNSET_VALUE @@ -41,14 +61,15 @@ module VagrantPlugins @raw_arguments = UNSET_VALUE @skip_tags = UNSET_VALUE @start_at_task = UNSET_VALUE - @sudo = UNSET_VALUE - @sudo_user = UNSET_VALUE @tags = UNSET_VALUE @vault_password_file = UNSET_VALUE @verbose = UNSET_VALUE end def finalize! + @become = false if @become != true + @become_user = nil if @become_user == UNSET_VALUE + @compatibility_mode = nil unless Ansible::COMPATIBILITY_MODES.include?(@compatibility_mode) @config_file = nil if @config_file == UNSET_VALUE @extra_vars = nil if @extra_vars == UNSET_VALUE @galaxy_role_file = nil if @galaxy_role_file == UNSET_VALUE @@ -63,8 +84,6 @@ module VagrantPlugins @raw_arguments = nil if @raw_arguments == UNSET_VALUE @skip_tags = nil if @skip_tags == UNSET_VALUE @start_at_task = nil if @start_at_task == UNSET_VALUE - @sudo = false if @sudo != true - @sudo_user = nil if @sudo_user == UNSET_VALUE @tags = nil if @tags == UNSET_VALUE @vault_password_file = nil if @vault_password_file == UNSET_VALUE @verbose = false if @verbose == UNSET_VALUE @@ -112,6 +131,14 @@ module VagrantPlugins end end + + protected + + def show_deprecation_info(deprecated_option, new_option) + puts "DEPRECATION: The '#{deprecated_option}' option for the Ansible provisioner is deprecated." + puts "Please use the '#{new_option}' option instead." + puts "The '#{deprecated_option}' option will be removed in a future release of Vagrant.\n\n" + end end end end diff --git a/plugins/provisioners/ansible/config/host.rb b/plugins/provisioners/ansible/config/host.rb index 4e075ae64..06df0d1c4 100644 --- a/plugins/provisioners/ansible/config/host.rb +++ b/plugins/provisioners/ansible/config/host.rb @@ -5,16 +5,25 @@ module VagrantPlugins module Config class Host < Base - attr_accessor :ask_sudo_pass + attr_accessor :ask_become_pass attr_accessor :ask_vault_pass attr_accessor :force_remote_user attr_accessor :host_key_checking attr_accessor :raw_ssh_args + # + # Deprecated options + # + alias :ask_sudo_pass :ask_become_pass + def ask_sudo_pass=(value) + show_deprecation_warning 'ask_sudo_pass', 'ask_become_pass' + @ask_become_pass = value + end + def initialize super - @ask_sudo_pass = false + @ask_become_pass = false @ask_vault_pass = false @force_remote_user = true @host_key_checking = false @@ -24,7 +33,7 @@ module VagrantPlugins def finalize! super - @ask_sudo_pass = false if @ask_sudo_pass != true + @ask_become_pass = false if @ask_become_pass != true @ask_vault_pass = false if @ask_vault_pass != true @force_remote_user = true if @force_remote_user != false @host_key_checking = false if @host_key_checking != true diff --git a/plugins/provisioners/ansible/constants.rb b/plugins/provisioners/ansible/constants.rb new file mode 100644 index 000000000..5e1d9e3f8 --- /dev/null +++ b/plugins/provisioners/ansible/constants.rb @@ -0,0 +1,9 @@ + +module VagrantPlugins + module Ansible + COMPATIBILITY_MODE_V1_8 = "1.8".freeze + COMPATIBILITY_MODE_V2_0 = "2.0".freeze + DEFAULT_COMPATIBILITY_MODE = COMPATIBILITY_MODE_V1_8 + COMPATIBILITY_MODES = [COMPATIBILITY_MODE_V1_8, COMPATIBILITY_MODE_V2_0].freeze + end +end \ No newline at end of file diff --git a/plugins/provisioners/ansible/provisioner/base.rb b/plugins/provisioners/ansible/provisioner/base.rb index c2add455a..8d6c8f413 100644 --- a/plugins/provisioners/ansible/provisioner/base.rb +++ b/plugins/provisioners/ansible/provisioner/base.rb @@ -1,3 +1,4 @@ +require_relative "../constants" require_relative "../errors" require_relative "../helpers" @@ -14,6 +15,27 @@ module VagrantPlugins RANGE_PATTERN = %r{(?:\[[a-z]:[a-z]\]|\[[0-9]+?:[0-9]+?\])}.freeze + ANSIBLE_PARAMETER_NAMES = { + Ansible::COMPATIBILITY_MODE_V1_8 => { + ansible_host: "ansible_ssh_host", + ansible_password: "ansible_ssh_pass", + ansible_port: "ansible_ssh_port", + ansible_user: "ansible_ssh_user", + ask_become_pass: "ask-sudo-pass", + become: "sudo", + become_user: "sudo-user", + }, + Ansible::COMPATIBILITY_MODE_V2_0 => { + ansible_host: "ansible_host", + ansible_password: "ansible_password", + ansible_port: "ansible_port", + ansible_user: "ansible_user", + ask_become_pass: "ask-become-pass", + become: "become", + become_user: "become-user", + } + } + protected def initialize(machine, config) @@ -25,6 +47,55 @@ module VagrantPlugins @inventory_path = nil end + def set_compatibility_mode + unless config.compatibility_mode + detect_compatibility_mode(gather_ansible_version) + end + + unless Ansible::COMPATIBILITY_MODES.include?(config.compatibility_mode) + raise "Programming Error: compatibility_mode must correctly set at this stage!" + end + + @lexicon = ANSIBLE_PARAMETER_NAMES[config.compatibility_mode] + end + + def detect_compatibility_mode(ansible_version_stdoutput) + if config.compatibility_mode + raise "Programming Error: detect_compatibility_mode() shouldn't have been called." + end + + begin + first_line = ansible_version_stdoutput.lines[0] + full_version = first_line.match(/ansible (\d)(\.\d+){1,}/) + + if full_version + major_version, _ = full_version.captures + + if major_version.to_i <= 1 + config.compatibility_mode = Ansible::COMPATIBILITY_MODE_V1_8 + else + config.compatibility_mode = Ansible::COMPATIBILITY_MODE_V2_0 + end + + @machine.env.ui.warn(I18n.t("vagrant.provisioners.ansible.compatibility_mode_warning", + compatibility_mode: config.compatibility_mode, + ansible_version: full_version) + + "\n") + end + rescue + # Nothing to do here, the fallback to default compatibility_mode is done below + end + + unless config.compatibility_mode + config.compatibility_mode = Ansible::DEFAULT_COMPATIBILITY_MODE + + @machine.env.ui.warn(I18n.t("vagrant.provisioners.ansible.compatibility_mode_not_detected", + compatibility_mode: config.compatibility_mode, + gathered_version: ansible_version_stdoutput) + + "\n") + end + end + def check_files_existence check_path_is_a_file(config.playbook, :playbook) @@ -97,8 +168,8 @@ module VagrantPlugins @command_arguments << "--inventory-file=#{inventory_path}" @command_arguments << "--extra-vars=#{extra_vars_argument}" if config.extra_vars - @command_arguments << "--sudo" if config.sudo - @command_arguments << "--sudo-user=#{config.sudo_user}" if config.sudo_user + @command_arguments << "--#{@lexicon[:become]}" if config.become + @command_arguments << "--#{@lexicon[:become_user]}=#{config.become_user}" if config.become_user @command_arguments << "#{verbosity_argument}" if verbosity_is_enabled? @command_arguments << "--vault-password-file=#{config.vault_password_file}" if config.vault_password_file @command_arguments << "--tags=#{Helpers::as_list_argument(config.tags)}" if config.tags diff --git a/plugins/provisioners/ansible/provisioner/guest.rb b/plugins/provisioners/ansible/provisioner/guest.rb index cdf50d9bc..863095dbb 100644 --- a/plugins/provisioners/ansible/provisioner/guest.rb +++ b/plugins/provisioners/ansible/provisioner/guest.rb @@ -14,8 +14,10 @@ module VagrantPlugins end def provision - check_files_existence check_and_install_ansible + check_files_existence + set_compatibility_mode + execute_ansible_galaxy_on_guest if config.galaxy_role_file execute_ansible_playbook_on_guest end @@ -67,6 +69,19 @@ module VagrantPlugins end end + def gather_ansible_version + raw_output = nil + result = @machine.communicate.execute("ansible --version", error_check: false) do |type, output| + if type == :stdout && output.lines[0] + raw_output = output.lines[0] + end + end + if result != 0 + raw_output = nil + end + raw_output + end + def get_provisioning_working_directory config.provisioning_path end diff --git a/plugins/provisioners/ansible/provisioner/host.rb b/plugins/provisioners/ansible/provisioner/host.rb index 352a13664..7682a2af3 100644 --- a/plugins/provisioners/ansible/provisioner/host.rb +++ b/plugins/provisioners/ansible/provisioner/host.rb @@ -18,8 +18,9 @@ module VagrantPlugins # At this stage, the SSH access is guaranteed to be ready @ssh_info = @machine.ssh_info - check_files_existence warn_for_unsupported_platform + check_files_existence + set_compatibility_mode execute_ansible_galaxy_from_host if config.galaxy_role_file execute_ansible_playbook_from_host @@ -31,7 +32,7 @@ module VagrantPlugins def warn_for_unsupported_platform if Vagrant::Util::Platform.windows? - @machine.env.ui.warn(I18n.t("vagrant.provisioners.ansible.windows_not_supported_for_control_machine")) + @machine.env.ui.warn(I18n.t("vagrant.provisioners.ansible.windows_not_supported_for_control_machine") + "\n") end end @@ -49,15 +50,15 @@ module VagrantPlugins 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. + # the ansible_ssh_user/ansible_user parameter won't be added to the auto-generated inventory. @command_arguments << "--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) - @command_arguments << "--extra-vars=ansible_ssh_user='#{@ssh_info[:username]}'" + @command_arguments << "--extra-vars=#{@lexicon[:ansible_user]}='#{@ssh_info[:username]}'" end - @command_arguments << "--ask-sudo-pass" if config.ask_sudo_pass + @command_arguments << "--#{@lexicon[:ask_become_pass]}" if config.ask_become_pass @command_arguments << "--ask-vault-pass" if config.ask_vault_pass prepare_common_command_arguments @@ -88,6 +89,30 @@ module VagrantPlugins end end + def gather_ansible_version + raw_output = nil + command = %w(ansible --version) + + command << { + notify: [:stdout, :stderr] + } + + begin + result = Vagrant::Util::Subprocess.execute(*command) do |type, output| + if type == :stdout && output.lines[0] + raw_output = output + end + end + if result.exit_code != 0 + raw_output = nil + end + rescue Vagrant::Errors::CommandUnavailable + raise Ansible::Errors::AnsibleNotFoundOnHost + end + + raw_output + end + def execute_ansible_galaxy_from_host prepare_ansible_config_environment_variable @@ -199,19 +224,19 @@ module VagrantPlugins def get_inventory_ssh_machine(machine, ssh_info) forced_remote_user = "" if config.force_remote_user - forced_remote_user = "ansible_ssh_user='#{ssh_info[:username]}' " + forced_remote_user = "#{@lexicon[:ansible_user]}='#{ssh_info[:username]}' " end - "#{machine.name} ansible_ssh_host=#{ssh_info[:host]} ansible_ssh_port=#{ssh_info[:port]} #{forced_remote_user}ansible_ssh_private_key_file='#{ssh_info[:private_key_path][0]}'\n" + "#{machine.name} #{@lexicon[:ansible_host]}=#{ssh_info[:host]} #{@lexicon[:ansible_port]}=#{ssh_info[:port]} #{forced_remote_user}ansible_ssh_private_key_file='#{ssh_info[:private_key_path][0]}'\n" end def get_inventory_winrm_machine(machine, winrm_net_info) forced_remote_user = "" if config.force_remote_user - forced_remote_user = "ansible_ssh_user='#{machine.config.winrm.username}' " + forced_remote_user = "#{@lexicon[:ansible_user]}='#{machine.config.winrm.username}' " end - "#{machine.name} ansible_connection=winrm ansible_ssh_host=#{winrm_net_info[:host]} ansible_ssh_port=#{winrm_net_info[:port]} #{forced_remote_user}ansible_ssh_pass='#{machine.config.winrm.password}'\n" + "#{machine.name} ansible_connection=winrm #{@lexicon[:ansible_host]}=#{winrm_net_info[:host]} #{@lexicon[:ansible_port]}=#{winrm_net_info[:port]} #{forced_remote_user}#{@lexicon[:ansible_password]}='#{machine.config.winrm.password}'\n" end def ansible_ssh_args diff --git a/templates/locales/en.yml b/templates/locales/en.yml index f012557d9..37a21c712 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -2387,6 +2387,20 @@ en: windows_not_supported_for_control_machine: |- Windows is not officially supported for the Ansible Control Machine. Please check https://docs.ansible.com/intro_installation.html#control-machine-requirements + compatibility_mode_not_detected: |- + Vagrant gathered an unknown Ansible version: + + %{gathered_version} + and falls back on the compatibility mode '%{compatibility_mode}'. + + Alternatively, the compatibility mode can be specified in your Vagrantfile: + https://www.vagrantup.com/docs/provisioning/ansible_common.html#compatibility_mode + compatibility_mode_warning: |- + Vagrant has automatically selected the compatibility mode '%{compatibility_mode}' + according to the Ansible version installed (%{ansible_version}). + + Alternatively, the compatibility mode can be specified in your Vagrantfile: + https://www.vagrantup.com/docs/provisioning/ansible_common.html#compatibility_mode docker: wrong_provisioner: |- diff --git a/test/unit/plugins/provisioners/ansible/config/guest_test.rb b/test/unit/plugins/provisioners/ansible/config/guest_test.rb index 92b7851c5..a9ea5d9da 100644 --- a/test/unit/plugins/provisioners/ansible/config/guest_test.rb +++ b/test/unit/plugins/provisioners/ansible/config/guest_test.rb @@ -16,7 +16,10 @@ describe VagrantPlugins::Ansible::Config::Guest do let(:existing_file) { "this/path/is/a/stub" } it "supports a list of options" do - supported_options = %w( config_file + supported_options = %w( become + become_user + compatibility_mode + config_file extra_vars galaxy_command galaxy_role_file diff --git a/test/unit/plugins/provisioners/ansible/config/host_test.rb b/test/unit/plugins/provisioners/ansible/config/host_test.rb index 1a23fecfe..2e18005ee 100644 --- a/test/unit/plugins/provisioners/ansible/config/host_test.rb +++ b/test/unit/plugins/provisioners/ansible/config/host_test.rb @@ -13,8 +13,12 @@ describe VagrantPlugins::Ansible::Config::Host, :skip_windows => true do let(:existing_file) { File.expand_path(__FILE__) } it "supports a list of options" do - supported_options = %w( ask_sudo_pass + supported_options = %w( ask_become_pass + ask_sudo_pass ask_vault_pass + become + become_user + compatibility_mode config_file extra_vars force_remote_user @@ -47,7 +51,8 @@ describe VagrantPlugins::Ansible::Config::Host, :skip_windows => true do it "assigns default values to unset host-specific options" do subject.finalize! - expect(subject.ask_sudo_pass).to be(false) + expect(subject.ask_become_pass).to be(false) + expect(subject.ask_sudo_pass).to be(false) # deprecated expect(subject.ask_vault_pass).to be(false) expect(subject.force_remote_user).to be(true) expect(subject.host_key_checking).to be(false) @@ -61,7 +66,14 @@ describe VagrantPlugins::Ansible::Config::Host, :skip_windows => true do describe "host_key_checking option" do it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :host_key_checking, false end + describe "ask_become_pass option" do + it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :ask_become_pass, false + end describe "ask_sudo_pass option" do + before do + # Filter the deprecation notice + allow($stdout).to receive(:puts) + end it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :ask_sudo_pass, false end describe "ask_vault_pass option" do diff --git a/test/unit/plugins/provisioners/ansible/config/shared.rb b/test/unit/plugins/provisioners/ansible/config/shared.rb index d9bd8cca3..c30d64f2f 100644 --- a/test/unit/plugins/provisioners/ansible/config/shared.rb +++ b/test/unit/plugins/provisioners/ansible/config/shared.rb @@ -3,6 +3,9 @@ shared_examples_for 'options shared by both Ansible provisioners' do it "assigns default values to unset common options" do subject.finalize! + expect(subject.become).to be(false) + expect(subject.become_user).to be_nil + expect(subject.compatibility_mode).to be_nil expect(subject.config_file).to be_nil expect(subject.extra_vars).to be_nil expect(subject.galaxy_command).to eql("ansible-galaxy install --role-file=%{role_file} --roles-path=%{roles_path} --force") @@ -17,8 +20,8 @@ shared_examples_for 'options shared by both Ansible provisioners' do expect(subject.raw_arguments).to be_nil expect(subject.skip_tags).to be_nil expect(subject.start_at_task).to be_nil - expect(subject.sudo).to be(false) - expect(subject.sudo_user).to be_nil + expect(subject.sudo).to be(false) # deprecated + expect(subject.sudo_user).to be_nil # deprecated expect(subject.tags).to be_nil expect(subject.vault_password_file).to be_nil expect(subject.verbose).to be(false) @@ -41,6 +44,30 @@ shared_examples_for 'an Ansible provisioner' do | path_prefix, ansible_setup | ]) end + describe "compatibility_mode option" do + + %w(1.8 2.0).each do |minimal_version| + it "supports compatibility mode '#{minimal_version}'" do + subject.compatibility_mode = minimal_version + subject.finalize! + + result = subject.validate(machine) + expect(subject.compatibility_mode).to eql(minimal_version) + end + end + + %w(invalid 1.9 2.3).each do |invalid_mode| + it "silently forces the compatibility mode detection for invalid mode '#{invalid_mode}'" do + subject.compatibility_mode = invalid_mode + subject.finalize! + + result = subject.validate(machine) + expect(subject.compatibility_mode).to be_nil + end + end + + end + it "passes if the extra_vars option is a hash" do subject.extra_vars = { var1: 1, var2: "foo" } subject.finalize! @@ -102,7 +129,15 @@ shared_examples_for 'an Ansible provisioner' do | path_prefix, ansible_setup | value: subject.raw_arguments.to_s)) end + describe "become option" do + it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :become, false + end + describe "sudo option" do + before do + # Filter the deprecation notice + allow($stdout).to receive(:puts) + end it_behaves_like "any VagrantConfigProvisioner strict boolean attribute", :sudo, false end diff --git a/test/unit/plugins/provisioners/ansible/provisioner_test.rb b/test/unit/plugins/provisioners/ansible/provisioner_test.rb index 861a4e1c6..264a0bb96 100644 --- a/test/unit/plugins/provisioners/ansible/provisioner_test.rb +++ b/test/unit/plugins/provisioners/ansible/provisioner_test.rb @@ -60,6 +60,8 @@ VF stubbed_ui = Vagrant::UI::Colored.new allow(stubbed_ui).to receive(:detail).and_return("") + allow(stubbed_ui).to receive(:warn).and_return("") + allow(machine.env).to receive(:ui).and_return(stubbed_ui) config.playbook = 'playbook.yml' @@ -69,6 +71,15 @@ VF # Class methods for code reuse across examples # + def self.it_should_check_ansible_version() + it "execute 'ansible --version' before executing 'ansible-playbook'" do + expect(Vagrant::Util::Subprocess).to receive(:execute). + once.with('ansible', '--version', { :notify => [:stdout, :stderr] }) + expect(Vagrant::Util::Subprocess).to receive(:execute). + once.with('ansible-playbook', any_args) + end + end + def self.it_should_set_arguments_and_environment_variables( expected_args_count = 5, expected_vars_count = 4, @@ -76,9 +87,7 @@ VF expected_transport_mode = "ssh") it "sets implicit arguments in a specific order" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| - - expect(args[0]).to eq("ansible-playbook") + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args[1]).to eq("--connection=ssh") expect(args[2]).to eq("--timeout=30") @@ -90,7 +99,7 @@ VF end it "sets --limit argument" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| all_limits = args.select { |x| x =~ /^(--limit=|-l)/ } if config.raw_arguments raw_limits = config.raw_arguments.select { |x| x =~ /^(--limit=|-l)/ } @@ -108,7 +117,7 @@ VF end it "exports environment variables" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last if expected_host_key_checking @@ -116,6 +125,7 @@ VF else expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o UserKnownHostsFile=/dev/null") end + expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentitiesOnly=yes") expect(cmd_opts[:env]['ANSIBLE_FORCE_COLOR']).to eql("true") expect(cmd_opts[:env]).to_not include("ANSIBLE_NOCOLOR") @@ -126,14 +136,14 @@ VF # "roughly" verify that only expected args/vars have been defined by the provisioner it "sets the expected number of arguments and environment variables" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| - expect(args.length-2).to eq(expected_args_count) + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| + expect(args.length - 2).to eq(expected_args_count) expect(args.last[:env].length).to eq(expected_vars_count) }.and_return(default_execute_result) end it "enables '#{expected_transport_mode}' as default transport mode" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| index = args.rindex("--connection=#{expected_transport_mode}") expect(index).to be > 0 expect(find_last_argument_after(index, args, /--connection=\w+/)).to be(false) @@ -144,7 +154,7 @@ VF def self.it_should_set_optional_arguments(arg_map) it "sets optional arguments" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| arg_map.each_pair do |vagrant_option, ansible_argument| index = args.index(ansible_argument) if config.send(vagrant_option) @@ -159,7 +169,7 @@ VF def self.it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "configures ControlPersist (like Ansible defaults) via ANSIBLE_SSH_ARGS" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ControlMaster=auto") expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ControlPersist=60s") @@ -167,23 +177,24 @@ VF end end - def self.it_should_create_and_use_generated_inventory(with_ssh_user = true) + def self.it_should_create_and_use_generated_inventory(with_user = true) it "generates an inventory with all active machines" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(config.inventory_path).to be_nil expect(File.exists?(generated_inventory_file)).to be(true) inventory_content = File.read(generated_inventory_file) - 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") + _ssh = config.compatibility_mode == VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0 ? "" : "_ssh" + if with_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") + 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") }.and_return(default_execute_result) end it "sets as ansible inventory the directory containing the auto-generated inventory file" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| inventory_index = args.rindex("--inventory-file=#{generated_inventory_dir}") expect(inventory_index).to be > 0 expect(find_last_argument_after(inventory_index, args, /--inventory-file=\w+/)).to be(false) @@ -260,11 +271,12 @@ VF end describe "with default options" do + it_should_check_ansible_version it_should_set_arguments_and_environment_variables it_should_create_and_use_generated_inventory it "does not add any group section to the generated inventory" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) expect(inventory_content).to_not match(/^\s*\[^\\+\]\s*$/) }.and_return(default_execute_result) @@ -275,14 +287,139 @@ VF end end + describe "deprecated 'sudo' options are aliases for equivalent 'become' options" do + before do + # Filter the deprecation notices + allow($stdout).to receive(:puts) + + config.sudo = true + config.sudo_user = 'deployer' + config.ask_sudo_pass = true + end + + it_should_set_optional_arguments({"sudo" => "--sudo", + "sudo_user" => "--sudo-user=deployer", + "ask_sudo_pass" => "--ask-sudo-pass", + "become" => "--sudo", + "become_user" => "--sudo-user=deployer", + "ask_become_pass" => "--ask-sudo-pass"}) + end + + context "with no compatibility_mode defined" do + before do + config.compatibility_mode = nil + end + + valid_versions = { + "0.6": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8, + "1.9.4": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8, + "2.2.1.0": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0, + "4.3.2.1": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0, + } + valid_versions.each_pair do |ansible_version, mode| + describe "and ansible version #{ansible_version}" do + before do + allow(subject).to receive(:gather_ansible_version).and_return("ansible #{ansible_version}") + end + + it "detects the compatibility mode #{mode}" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| + expect(config.compatibility_mode).to eq(mode) + }.and_return(default_execute_result) + end + + it "warns about compatibility mode auto-detection being used" do + expect(machine.env.ui).to receive(:warn).with( + I18n.t("vagrant.provisioners.ansible.compatibility_mode_warning", + compatibility_mode: mode, ansible_version: "ansible #{ansible_version}") + + "\n") + end + end + end + + invalid_versions = [ + "ansible devel", + "ansible 2.x.y.z", + "2.9.2.1", + ] + invalid_versions.each do |unknown_ansible_version| + describe "and `ansible --version` returning '#{unknown_ansible_version}'" do + before do + allow(subject).to receive(:gather_ansible_version).and_return(unknown_ansible_version) + end + + it "applies the default compatibility mode ('#{VagrantPlugins::Ansible::DEFAULT_COMPATIBILITY_MODE}')" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| + expect(config.compatibility_mode).to eq(VagrantPlugins::Ansible::DEFAULT_COMPATIBILITY_MODE) + }.and_return(default_execute_result) + end + + it "warns about not being able to detect the best compatibility mode" do + expect(machine.env.ui).to receive(:warn).with( + I18n.t("vagrant.provisioners.ansible.compatibility_mode_not_detected", + compatibility_mode: VagrantPlugins::Ansible::DEFAULT_COMPATIBILITY_MODE, + gathered_version: unknown_ansible_version) + + "\n") + end + end + end + + end + + context "with compatibility_mode '#{VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8}'" do + before do + config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8 + end + + it_should_create_and_use_generated_inventory + + it "doesn't warn about compatibility mode auto-detection" do + expect(machine.env.ui).to_not receive(:warn) + end + end + + context "with compatibility_mode '#{VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0}'" do + before do + config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0 + end + + it_should_create_and_use_generated_inventory + + it "doesn't warn about compatibility mode auto-detection" do + expect(machine.env.ui).to_not receive(:warn) + end + + describe "deprecated 'sudo' options are aliases for equivalent 'become' options" do + before do + # Filter the deprecation notices + allow($stdout).to receive(:puts) + + config.sudo = true + config.sudo_user = 'deployer' + config.ask_sudo_pass = true + end + + it_should_set_optional_arguments({"sudo" => "--become", + "sudo_user" => "--become-user=deployer", + "ask_sudo_pass" => "--ask-become-pass", + "become" => "--become", + "become_user" => "--become-user=deployer", + "ask_become_pass" => "--ask-become-pass"}) + end + end + describe "with playbook_command option" do before do config.playbook_command = "custom-ansible-playbook" + + # set the compatibility mode to ensure that only ansible-playbook is excuted + config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0 end it "uses custom playbook_command to run playbooks" do expect(Vagrant::Util::Subprocess).to receive(:execute) .with("custom-ansible-playbook", any_args) + .and_return(default_execute_result) end end @@ -293,7 +430,7 @@ VF config.host_vars = { machine1: {"http_port" => 80, "comments" => "'some text with spaces'"} } - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 comments='some text with spaces'$") }.and_return(default_execute_result) @@ -303,7 +440,8 @@ VF config.host_vars = { machine1: ["http_port=80", "maxRequestsPerChild=808"] } - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { + + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 maxRequestsPerChild=808") }.and_return(default_execute_result) @@ -313,7 +451,8 @@ VF config.host_vars = { :machine1 => "http_port=80 maxRequestsPerChild=808" } - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { + + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 maxRequestsPerChild=808") }.and_return(default_execute_result) @@ -323,7 +462,8 @@ VF config.host_vars = { "machine1" => "http_port=80 maxRequestsPerChild=808" } - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { inventory_content = File.read(generated_inventory_file) expect(inventory_content).to match("^" + Regexp.quote(machine.name) + ".+http_port=80 maxRequestsPerChild=808") }.and_return(default_execute_result) @@ -345,7 +485,7 @@ VF "bar:children" => ["group1", "group2", "group3", "group5"], } - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| inventory_content = File.read(generated_inventory_file) # Accept String instead of Array for group member list @@ -383,7 +523,7 @@ VF "group3:vars" => "stringvar1=stringvalue1 stringvar2=stringvalue2", } - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| inventory_content = File.read(generated_inventory_file) # Hash syntax @@ -421,18 +561,18 @@ VF describe "with boolean (flag) options disabled" do before do - config.sudo = false - config.ask_sudo_pass = false + config.become = false + config.ask_become_pass = false config.ask_vault_pass = false - config.sudo_user = 'root' + config.become_user = 'root' end it_should_set_arguments_and_environment_variables 6 - it_should_set_optional_arguments({ "sudo_user" => "--sudo-user=root" }) + it_should_set_optional_arguments({ "become_user" => "--sudo-user=root" }) it "it does not set boolean flag when corresponding option is set to false" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args.index("--sudo")).to be_nil expect(args.index("--ask-sudo-pass")).to be_nil expect(args.index("--ask-vault-pass")).to be_nil @@ -442,7 +582,7 @@ VF describe "with raw_arguments option" do before do - config.sudo = false + config.become = false config.force_remote_user = false config.skip_tags = %w(foo bar) config.limit = "all" @@ -461,7 +601,7 @@ VF it_should_set_arguments_and_environment_variables 17, 4, false, "paramiko" it "sets all raw arguments" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| config.raw_arguments.each do |raw_arg| expect(args).to include(raw_arg) end @@ -469,7 +609,7 @@ VF end it "sets raw arguments after arguments related to supported options" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args.index("--user=lion")).to be > args.index("--user=testuser") expect(args.index("--inventory-file=/forget/it/my/friend")).to be > args.index("--inventory-file=#{generated_inventory_dir}") expect(args.index("--limit=bar")).to be > args.index("--limit=all") @@ -478,7 +618,7 @@ VF end it "sets boolean flag (e.g. --sudo) defined in raw_arguments, even if corresponding option is set to false" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include('--sudo') }.and_return(default_execute_result) end @@ -503,7 +643,7 @@ VF 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(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).not_to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") expect(args).to include("--user=#{machine.ssh_info[:username]}") }.and_return(default_execute_result) @@ -534,8 +674,7 @@ VF it_should_set_arguments_and_environment_variables it "generates an inventory with winrm connection settings" do - - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(config.inventory_path).to be_nil expect(File.exists?(generated_inventory_file)).to be(true) inventory_content = File.read(generated_inventory_file) @@ -550,7 +689,7 @@ VF end it "doesn't set the ansible remote user in inventory and use '--user' argument with the vagrant ssh username" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| inventory_content = File.read(generated_inventory_file) expect(inventory_content).to include("machine1 ansible_connection=winrm ansible_ssh_host=127.0.0.1 ansible_ssh_port=55986 ansible_ssh_pass='winword'\n") @@ -568,7 +707,7 @@ VF 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(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include("--inventory-file=#{existing_file}") expect(args).not_to include("--inventory-file=#{generated_inventory_file}") expect(File.exists?(generated_inventory_file)).to be(false) @@ -576,7 +715,7 @@ VF end it "uses an --extra-vars argument to force ansible_ssh_user parameter" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).not_to include("--user=#{machine.ssh_info[:username]}") expect(args).to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") }.and_return(default_execute_result) @@ -588,7 +727,7 @@ VF end it "uses a --user argument to set a default remote user" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).not_to include("--extra-vars=ansible_ssh_user='#{machine.ssh_info[:username]}'") expect(args).to include("--user=#{machine.ssh_info[:username]}") }.and_return(default_execute_result) @@ -602,7 +741,7 @@ VF end it "sets ANSIBLE_CONFIG environment variable" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]).to include("ANSIBLE_CONFIG") expect(cmd_opts[:env]['ANSIBLE_CONFIG']).to eql(existing_file) @@ -618,7 +757,7 @@ VF it_should_set_arguments_and_environment_variables 6 it "should ask the vault password" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include("--ask-vault-pass") }.and_return(default_execute_result) end @@ -632,7 +771,7 @@ VF it_should_set_arguments_and_environment_variables 6 it "uses the given vault password file" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include("--vault-password-file=#{existing_file}") }.and_return(default_execute_result) end @@ -647,7 +786,7 @@ VF it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "passes custom SSH options via ANSIBLE_SSH_ARGS with the highest priority" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last raw_opt_index = cmd_opts[:env]['ANSIBLE_SSH_ARGS'].index("-o ControlMaster=no") default_opt_index = cmd_opts[:env]['ANSIBLE_SSH_ARGS'].index("-o ControlMaster=auto") @@ -661,7 +800,7 @@ VF end it "sets '-o ForwardAgent=yes' via ANSIBLE_SSH_ARGS with higher priority than raw_ssh_args values" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last forwardAgentYes = cmd_opts[:env]['ANSIBLE_SSH_ARGS'].index("-o ForwardAgent=yes") forwardAgentNo = cmd_opts[:env]['ANSIBLE_SSH_ARGS'].index("-o ForwardAgent=no") @@ -681,7 +820,7 @@ VF it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "passes additional Identity Files via ANSIBLE_SSH_ARGS" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentityFile=/an/other/identity") expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentityFile=/yet/an/other/key") @@ -695,7 +834,7 @@ VF end it "replaces `%` with `%%`" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentityFile=/foo%%bar/key") expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o IdentityFile=/bar%%%%buz/key") @@ -712,7 +851,7 @@ VF it_should_explicitly_enable_ansible_ssh_control_persist_defaults it "enables SSH-Forwarding via ANSIBLE_SSH_ARGS" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ForwardAgent=yes") }.and_return(default_execute_result) @@ -725,7 +864,7 @@ VF end it "sets '-o ProxyCommand' via ANSIBLE_SSH_ARGS" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ProxyCommand='ssh -W %h:%p -q user@remote_libvirt_host'") }.and_return(default_execute_result) @@ -795,10 +934,12 @@ VF describe "without colorized output" do before do allow(machine.env).to receive(:ui).and_return(Vagrant::UI::Basic.new) + + allow(machine.env.ui).to receive(:warn).and_return("") # hide the breaking change warning end it "disables ansible-playbook colored output" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]).to_not include("ANSIBLE_FORCE_COLOR") expect(cmd_opts[:env]['ANSIBLE_NOCOLOR']).to eql("true") @@ -822,7 +963,11 @@ VF expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleCommandFailed) end - it "execute ansible-galaxy, and then ansible-playbook" do + it "execute three commands: ansible --version, ansible-galaxy, and ansible-playbook" do + expect(Vagrant::Util::Subprocess).to receive(:execute) + .once + .with('ansible', '--version', { :notify => [:stdout, :stderr] }) + .and_return(default_execute_result) expect(Vagrant::Util::Subprocess).to receive(:execute) .once .with('ansible-galaxy', any_args) @@ -856,7 +1001,7 @@ VF end it "sets ANSIBLE_ROLES_PATH with corresponding absolute path" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]).to include("ANSIBLE_ROLES_PATH") expect(cmd_opts[:env]['ANSIBLE_ROLES_PATH']).to eql(File.join(machine.env.root_path, "my-roles")) @@ -893,10 +1038,10 @@ VF # command line arguments config.galaxy_roles_path = "/up/to the stars" config.extra_vars = { var1: %Q(string with 'apo$trophe$', \\, " and =), var2: { x: 42 } } - config.sudo = true - config.sudo_user = 'deployer' + config.become = true + config.become_user = 'deployer' config.verbose = "vvv" - config.ask_sudo_pass = true + config.ask_become_pass = true config.ask_vault_pass = true config.vault_password_file = existing_file config.tags = %w(db www) @@ -914,10 +1059,10 @@ VF it_should_set_arguments_and_environment_variables 21, 6, true it_should_explicitly_enable_ansible_ssh_control_persist_defaults it_should_set_optional_arguments({ "extra_vars" => "--extra-vars={\"var1\":\"string with 'apo$trophe$', \\\\, \\\" and =\",\"var2\":{\"x\":42}}", - "sudo" => "--sudo", - "sudo_user" => "--sudo-user=deployer", + "become" => "--sudo", + "become_user" => "--sudo-user=deployer", "verbose" => "-vvv", - "ask_sudo_pass" => "--ask-sudo-pass", + "ask_become_pass" => "--ask-sudo-pass", "ask_vault_pass" => "--ask-vault-pass", "vault_password_file" => "--vault-password-file=#{File.expand_path(__FILE__)}", "tags" => "--tags=db,www", @@ -927,7 +1072,7 @@ VF }) it "also includes given raw arguments" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| expect(args).to include("--why-not") expect(args).to include("--su-user=foot") expect(args).to include("--ask-su-pass") @@ -967,7 +1112,7 @@ VF end it "uses an SSH ProxyCommand to reach the VM" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to include("-o ProxyCommand='ssh boot9docker@127.0.0.1 -p 2299 -i /path/to/docker/host/key -o Compression=yes -o ConnectTimeout=5 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no exec nc %h %p 2>/dev/null'") }.and_return(default_execute_result) @@ -982,11 +1127,14 @@ VF before do allow(Vagrant::Util::Platform).to receive(:windows?).and_return(true) allow(machine.ui).to receive(:warn) + + # Set the compatibility mode to only get the Windows warning + config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0 end it "warns that Windows is not officially supported for the Ansible control machine" do expect(machine.env.ui).to receive(:warn) - .with(I18n.t("vagrant.provisioners.ansible.windows_not_supported_for_control_machine")) + .with(I18n.t("vagrant.provisioners.ansible.windows_not_supported_for_control_machine") + "\n") end end @@ -996,7 +1144,7 @@ VF end it "does not set IdentitiesOnly=yes in ANSIBLE_SSH_ARGS" do - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to_not include("-o IdentitiesOnly=yes") }.and_return(default_execute_result) @@ -1006,7 +1154,7 @@ VF it "does not set ANSIBLE_SSH_ARGS environment variable" do config.host_key_checking = true - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]).to_not include('ANSIBLE_SSH_ARGS') }.and_return(Vagrant::Util::Subprocess::Result.new(0, "", "")) @@ -1019,7 +1167,7 @@ VF it 'does not set IdentitiesOnly=yes in ANSIBLE_SSH_ARGS' do ssh_info[:keys_only] = false - expect(Vagrant::Util::Subprocess).to receive(:execute).with(any_args) { |*args| + expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args) { |*args| cmd_opts = args.last expect(cmd_opts[:env]['ANSIBLE_SSH_ARGS']).to_not include("-o IdentitiesOnly=yes") }.and_return(default_execute_result) diff --git a/website/source/docs/provisioning/ansible.html.md b/website/source/docs/provisioning/ansible.html.md index 3dda49b31..ead77573c 100644 --- a/website/source/docs/provisioning/ansible.html.md +++ b/website/source/docs/provisioning/ansible.html.md @@ -53,10 +53,17 @@ end This section lists the _specific_ options for the Ansible (remote) provisioner. In addition to the options listed below, this provisioner supports the [**common options** for both Ansible provisioners](/docs/provisioning/ansible_common.html). -- `ask_sudo_pass` (boolean) - require Ansible to [prompt for a sudo password](https://docs.ansible.com/intro_getting_started.html#remote-connection-information). +- `ask_become_pass` (boolean) - require Ansible to [prompt for a password](https://docs.ansible.com/intro_getting_started.html#remote-connection-information) when switching to another user with the [become/sudo mechanism](http://docs.ansible.com/ansible/become.html). The default value is `false`. +- `ask_sudo_pass` (boolean) - Backwards compatible alias for the [ask_become_pass](#ask_become_pass) option. + +
+ Deprecation: + The `ask_sudo_pass` option is deprecated and will be removed in a future release. Please use the [**`ask_become_pass`**](#ask_become_pass) option instead. +
+ - `ask_vault_pass` (boolean) - require Ansible to [prompt for a vault password](https://docs.ansible.com/playbooks_vault.html#vault). The default value is `false`. diff --git a/website/source/docs/provisioning/ansible_common.html.md b/website/source/docs/provisioning/ansible_common.html.md index f6fb84f07..5d0536520 100644 --- a/website/source/docs/provisioning/ansible_common.html.md +++ b/website/source/docs/provisioning/ansible_common.html.md @@ -17,6 +17,33 @@ These options get passed to the `ansible-playbook` command that ships with Ansib Some of these options are for advanced usage only and should not be used unless you understand their purpose. +- `become` (boolean) - Cause Ansible to perform all the playbook tasks [as another user](http://docs.ansible.com/ansible/become.html), different from the one used to log into the guest system. + + The default value is `false`. + +- `become_user` (string) - Set the default username to be used by the Ansible `become` [privilege escalation](http://docs.ansible.com/ansible/become.html) mechanism. + + By default this option is not defined, and the Ansible default value (`root`) will be used. + +- `compatibility_mode` (string) - Set the **minimal** version of Ansible to be supported. Vagrant will use some parameters that are only compatible since the given version. + + Possible values: + + - `"1.8"` (Ansible versions prior to 1.8 should mostly work well, but some options might not be supported) + - `"2.0"` (The generated Ansible inventory will be incompatible with Ansible 1.x) + + By default this option is not set, and Vagrant will try to automatically set the optimal compatibilty mode by checking the Ansible version currently available. Note that Vagrant doesn't validate this option, and any unsupported value (e.g. "2.3") will also lead Vagrant to auto-detect the compatibility mode. + +
+ Compatibility Note: + This option was introduced in Vagrant 2.0. Previous Vagrant versions behave like if this option was set to `"1.8"`. +
+ +
+ Attention: + Vagrant doesn't perform any validation between the `compatibility_mode` value and the value of the ansible_local [`version`](/docs/provisioning/ansible_local.html#version) option. +
+ - `config_file` (string) - The path to an [Ansible Configuration file](https://docs.ansible.com/intro_configuration.html). By default, this option is not set, and Ansible will [search for a possible configuration file in some default locations](/docs/provisioning/ansible_intro.html#ANSIBLE_CONFIG). @@ -129,11 +156,19 @@ Some of these options are for advanced usage only and should not be used unless - `start_at_task` (string) - The task name where the [playbook execution will start](https://docs.ansible.com/playbooks_startnstep.html#start-at-task). -- `sudo` (boolean) - Cause Ansible to perform all the playbook tasks [using sudo](https://docs.ansible.com/glossary.html#sudo). +- `sudo` (boolean) - Backwards compatible alias for the [`become`](#become) option. - The default value is `false`. +
+ Deprecation: + The `sudo` option is deprecated and will be removed in a future release. Please use the [**`become`**](#become) option instead. +
-- `sudo_user` (string) - set the default username who should be used by the sudo command. +- `sudo_user` (string) - Backwards compatible alias for the [`become_user`](#become_user) option. + +
+ Deprecation: + The `sudo_user` option is deprecated and will be removed in a future release. Please use the [**`become_user`**](#become_user) option instead. +
- `tags` (string or array of strings) - Only plays, roles and tasks [tagged with these values will be executed](https://docs.ansible.com/playbooks_tags.html) .