provisioners/ansible: Check compatibility conflicts

Vagrant will verify that the current Ansible version does support the
requested compatibility mode (only applicable if not "auto", of course).

As mentioned in the documentation, there is no sanity checks between
`version` option and `compatibility_mode` option.

With this change, the host-based provisioner is also improved to
execute only once the "ansible" command (and store the gathered
information for multiple usages like version requirement and
compatibility checks). On the other hand, the guest-based provisioner
can still potentially execute "ansible" twice (once in the
AnsibleInstalled cap, and via "gather_ansible_version" function via
Base::set_compatibility_mode).
This commit is contained in:
Gilles Cornu 2017-09-01 08:05:50 +02:00
parent 36616fb208
commit dc3b6341e2
No known key found for this signature in database
GPG Key ID: F6BC2CF7E1FE8FFF
7 changed files with 124 additions and 62 deletions

View File

@ -26,6 +26,10 @@ module VagrantPlugins
class AnsibleVersionMismatch < AnsibleError class AnsibleVersionMismatch < AnsibleError
error_key(:ansible_version_mismatch) error_key(:ansible_version_mismatch)
end end
class AnsibleCompatibilityModeConflict < AnsibleError
error_key(:ansible_compatibility_mode_conflict)
end
end end
end end
end end

View File

@ -46,45 +46,31 @@ module VagrantPlugins
@environment_variables = {} @environment_variables = {}
@inventory_machines = {} @inventory_machines = {}
@inventory_path = nil @inventory_path = nil
@gathered_version_stdout = nil
@gathered_version_major = nil
@gathered_version = nil
end end
def set_compatibility_mode def set_compatibility_mode
if config.compatibility_mode == Ansible::COMPATIBILITY_MODE_AUTO
detect_compatibility_mode(gather_ansible_version)
end
unless Ansible::COMPATIBILITY_MODES.slice(1..-1).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 != Ansible::COMPATIBILITY_MODE_AUTO
raise "Programming Error: detect_compatibility_mode() shouldn't have been called."
end
begin begin
first_line = ansible_version_stdoutput.lines[0] set_gathered_ansible_version(gather_ansible_version)
full_version = first_line.match(/ansible (\d)(\.\d+){1,}/) rescue Exception => e
# Nothing to do here, as the fallback on safe compatibility_mode is done below
@logger.error("Error while gathering the ansible version: #{e.to_s}")
end
if full_version if @gathered_version_major
major_version, _ = full_version.captures if config.compatibility_mode == Ansible::COMPATIBILITY_MODE_AUTO
detect_compatibility_mode
if major_version.to_i <= 1 elsif @gathered_version_major.to_i < 2 && config.compatibility_mode == Ansible::COMPATIBILITY_MODE_V2_0
config.compatibility_mode = Ansible::COMPATIBILITY_MODE_V1_8 # A better version comparator will be needed
else # when more compatibility modes come... but so far let's keep it simple!
config.compatibility_mode = Ansible::COMPATIBILITY_MODE_V2_0 raise Ansible::Errors::AnsibleCompatibilityModeConflict,
end ansible_version: @gathered_version,
system: @control_machine,
@machine.env.ui.warn(I18n.t("vagrant.provisioners.ansible.compatibility_mode_warning", compatibility_mode: config.compatibility_mode
compatibility_mode: config.compatibility_mode,
ansible_version: full_version) +
"\n")
end end
rescue
# Nothing to do here, the fallback to default compatibility_mode is done below
end end
if config.compatibility_mode == Ansible::COMPATIBILITY_MODE_AUTO if config.compatibility_mode == Ansible::COMPATIBILITY_MODE_AUTO
@ -92,9 +78,15 @@ module VagrantPlugins
@machine.env.ui.warn(I18n.t("vagrant.provisioners.ansible.compatibility_mode_not_detected", @machine.env.ui.warn(I18n.t("vagrant.provisioners.ansible.compatibility_mode_not_detected",
compatibility_mode: config.compatibility_mode, compatibility_mode: config.compatibility_mode,
gathered_version: ansible_version_stdoutput) + gathered_version: @gathered_version_stdout) +
"\n") "\n")
end end
unless Ansible::COMPATIBILITY_MODES.slice(1..-1).include?(config.compatibility_mode)
raise "Programming Error: compatibility_mode must correctly set at this stage!"
end
@lexicon = ANSIBLE_PARAMETER_NAMES[config.compatibility_mode]
end end
def check_files_existence def check_files_existence
@ -357,6 +349,39 @@ module VagrantPlugins
end end
end end
private
def detect_compatibility_mode
if !@gathered_version_major || config.compatibility_mode != Ansible::COMPATIBILITY_MODE_AUTO
raise "Programming Error: detect_compatibility_mode() shouldn't have been called."
end
if @gathered_version_major.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: @gathered_version) +
"\n")
end
def set_gathered_ansible_version(stdout_output)
@gathered_version_stdout = stdout_output
if !@gathered_version_stdout.empty?
first_line = @gathered_version_stdout.lines[0]
ansible_version_pattern = first_line.match(/(^ansible\s+)(.+)$/)
if ansible_version_pattern
_, @gathered_version, _ = ansible_version_pattern.captures
if @gathered_version
@gathered_version_major = @gathered_version.match(/^(\d)\..+$/).captures[0].to_i
end
end
end
end
end end
end end
end end

View File

@ -66,19 +66,22 @@ module VagrantPlugins
if (!config.version.empty? && if (!config.version.empty? &&
config.version.to_s.to_sym != :latest && config.version.to_s.to_sym != :latest &&
!@machine.guest.capability(:ansible_installed, config.version)) !@machine.guest.capability(:ansible_installed, config.version))
raise Ansible::Errors::AnsibleVersionMismatch, system: @control_machine, required_version: config.version.to_s raise Ansible::Errors::AnsibleVersionMismatch,
system: @control_machine,
required_version: config.version,
current_version: @gathered_version
end end
end end
def gather_ansible_version def gather_ansible_version
raw_output = nil raw_output = ""
result = @machine.communicate.execute("ansible --version", error_check: false) do |type, output| result = @machine.communicate.execute("ansible --version", error_check: false) do |type, output|
if type == :stdout && output.lines[0] if type == :stdout && output.lines[0]
raw_output = output.lines[0] raw_output = output.lines[0]
end end
end end
if result != 0 if result != 0
raw_output = nil raw_output = ""
end end
raw_output raw_output
end end

View File

@ -20,9 +20,9 @@ module VagrantPlugins
@ssh_info = @machine.ssh_info @ssh_info = @machine.ssh_info
warn_for_unsupported_platform warn_for_unsupported_platform
check_required_ansible_version unless config.version.empty?
check_files_existence check_files_existence
set_compatibility_mode set_compatibility_mode
check_required_ansible_version
execute_ansible_galaxy_from_host if config.galaxy_role_file execute_ansible_galaxy_from_host if config.galaxy_role_file
execute_ansible_playbook_from_host execute_ansible_playbook_from_host
@ -39,15 +39,16 @@ module VagrantPlugins
end end
def check_required_ansible_version def check_required_ansible_version
if config.version.to_s.to_sym == :latest # Skip this check when not required, nor possible
@logger.debug("The :latest version requirement is not supported (yet) by the host-based provisioner") if !@gathered_version || config.version.empty? || config.version.to_s.to_sym == :latest
return return
end end
@logger.info("Checking for Ansible version on Vagrant host...") if config.version != @gathered_version
found_version = gather_ansible_version raise Ansible::Errors::AnsibleVersionMismatch,
if (!found_version || "ansible #{config.version}\n" != found_version.lines[0]) system: @control_machine,
raise Ansible::Errors::AnsibleVersionMismatch, system: @control_machine, required_version: config.version.to_s required_version: config.version,
current_version: @gathered_version
end end
end end
@ -105,7 +106,7 @@ module VagrantPlugins
end end
def gather_ansible_version def gather_ansible_version
raw_output = nil raw_output = ""
command = %w(ansible --version) command = %w(ansible --version)
command << { command << {
@ -119,7 +120,7 @@ module VagrantPlugins
end end
end end
if result.exit_code != 0 if result.exit_code != 0
raw_output = nil raw_output = ""
end end
rescue Vagrant::Errors::CommandUnavailable rescue Vagrant::Errors::CommandUnavailable
raise Ansible::Errors::AnsibleNotFoundOnHost raise Ansible::Errors::AnsibleNotFoundOnHost

View File

@ -2366,10 +2366,15 @@ en:
https://github.com/mitchellh/vagrant https://github.com/mitchellh/vagrant
ansible_version_mismatch: |- ansible_version_mismatch: |-
The requested Ansible version (%{required_version}) was not found on the %{system}. The requested Ansible version (%{required_version}) was not found on the %{system}.
Please check the Ansible installation on your Vagrant %{system} system, Please check the Ansible installation on your Vagrant %{system} system (currently: %{current_version}),
or adapt the `version` option of this provisioner in your Vagrantfile. or adapt the `version` option of this provisioner in your Vagrantfile.
See https://docs.vagrantup.com/v2/provisioning/ansible_common.html#version See https://docs.vagrantup.com/v2/provisioning/ansible_common.html#version
for more information. for more information.
ansible_compatibility_mode_conflict: |-
The requested Ansible compatibility mode (%{compatibility_mode}) is in conflict with
the Ansible installation on your Vagrant %{system} system (currently: %{ansible_version}).
See https://docs.vagrantup.com/v2/provisioning/ansible_common.html#compatibility_mode
for more information.
config_file_not_found: |- config_file_not_found: |-
`%{config_option}` does not exist on the %{system}: %{path} `%{config_option}` does not exist on the %{system}: %{path}
extra_vars_invalid: |- extra_vars_invalid: |-

View File

@ -313,7 +313,8 @@ VF
valid_versions = { valid_versions = {
"0.6": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8, "0.6": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8,
"1.9.4": 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, "2.5.0.0-rc1": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0,
"2.x.y.z": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0,
"4.3.2.1": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0, "4.3.2.1": VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0,
} }
valid_versions.each_pair do |ansible_version, mode| valid_versions.each_pair do |ansible_version, mode|
@ -331,7 +332,7 @@ VF
it "warns about compatibility mode auto-detection being used" do it "warns about compatibility mode auto-detection being used" do
expect(machine.env.ui).to receive(:warn).with( expect(machine.env.ui).to receive(:warn).with(
I18n.t("vagrant.provisioners.ansible.compatibility_mode_warning", I18n.t("vagrant.provisioners.ansible.compatibility_mode_warning",
compatibility_mode: mode, ansible_version: "ansible #{ansible_version}") + compatibility_mode: mode, ansible_version: ansible_version) +
"\n") "\n")
end end
end end
@ -339,7 +340,7 @@ VF
invalid_versions = [ invalid_versions = [
"ansible devel", "ansible devel",
"ansible 2.x.y.z\n...\n", "anything 1.2",
"2.9.2.1", "2.9.2.1",
] ]
invalid_versions.each do |unknown_ansible_version| invalid_versions.each do |unknown_ansible_version|
@ -371,6 +372,7 @@ VF
config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8 config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8
end end
it_should_check_ansible_version
it_should_create_and_use_generated_inventory it_should_create_and_use_generated_inventory
it "doesn't warn about compatibility mode auto-detection" do it "doesn't warn about compatibility mode auto-detection" do
@ -381,6 +383,7 @@ VF
context "with compatibility_mode '#{VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0}'" do context "with compatibility_mode '#{VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0}'" do
before do before do
config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0 config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0
allow(subject).to receive(:gather_ansible_version).and_return("ansible 2.3.0.0\n...\n")
end end
it_should_create_and_use_generated_inventory it_should_create_and_use_generated_inventory
@ -389,6 +392,16 @@ VF
expect(machine.env.ui).to_not receive(:warn) expect(machine.env.ui).to_not receive(:warn)
end end
describe "and an incompatible ansible version" do
before do
allow(subject).to receive(:gather_ansible_version).and_return("ansible 1.9.3\n...\n")
end
it "raises a compatibility conflict error", skip_before: false, skip_after: true do
expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleCompatibilityModeConflict)
end
end
describe "deprecated 'sudo' options are aliases for equivalent 'become' options" do describe "deprecated 'sudo' options are aliases for equivalent 'become' options" do
before do before do
# Filter the deprecation notices # Filter the deprecation notices
@ -413,7 +426,7 @@ VF
config.playbook_command = "custom-ansible-playbook" config.playbook_command = "custom-ansible-playbook"
# set the compatibility mode to ensure that only ansible-playbook is excuted # set the compatibility mode to ensure that only ansible-playbook is excuted
config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0 config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8
end end
it "uses custom playbook_command to run playbooks" do it "uses custom playbook_command to run playbooks" do
@ -968,20 +981,29 @@ VF
allow(subject).to receive(:gather_ansible_version).and_return("ansible 1.9.6\n...\n") allow(subject).to receive(:gather_ansible_version).and_return("ansible 1.9.6\n...\n")
end end
it "raises an error about the ansible version mismatch", skip_before: true, skip_after: true do it "raises an error about the ansible version mismatch", skip_before: false, skip_after: true do
config.finalize!
expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleVersionMismatch) expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleVersionMismatch)
end end
end end
describe "and the installed ansible version cannot be detected" do describe "and the installed ansible version cannot be detected" do
before do before do
allow(subject).to receive(:gather_ansible_version).and_return(nil) allow(subject).to receive(:gather_ansible_version).and_return("")
end end
it "raises an error about the ansible version mismatch", skip_before: true, skip_after: true do it "skips the ansible version check and executes ansible-playbook command" do
config.finalize! expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args).and_return(default_execute_result)
expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleVersionMismatch) end
end
describe "with special value: 'latest'" do
before do
config.version = :latest
allow(subject).to receive(:gather_ansible_version).and_return("ansible 2.2.0.1\n...\n")
end
it "skips the ansible version check and executes ansible-playbook command" do
expect(Vagrant::Util::Subprocess).to receive(:execute).with('ansible-playbook', any_args).and_return(default_execute_result)
end end
end end
end end
@ -1168,7 +1190,7 @@ VF
allow(machine.ui).to receive(:warn) allow(machine.ui).to receive(:warn)
# Set the compatibility mode to only get the Windows warning # Set the compatibility mode to only get the Windows warning
config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V2_0 config.compatibility_mode = VagrantPlugins::Ansible::COMPATIBILITY_MODE_V1_8
end end
it "warns that Windows is not officially supported for the Ansible control machine" do it "warns that Windows is not officially supported for the Ansible control machine" do

View File

@ -35,16 +35,18 @@ Some of these options are for advanced usage only and should not be used unless
By default this option is set to `"auto"`. If Vagrant is not able to detect any supported Ansible version, it will falls back on the compatibility mode `"1.8"` with a warning. By default this option is set to `"auto"`. If Vagrant is not able to detect any supported Ansible version, it will falls back on the compatibility mode `"1.8"` with a warning.
<div class="alert alert-info"> Vagrant will error if the specified compatibility mode is incompatible with the current Ansible version.
<strong>Compatibility Note:</strong>
This option was introduced in Vagrant 2.0. Previous Vagrant versions behave like if this option was set to `"1.8"`.
</div>
<div class="alert alert-warning"> <div class="alert alert-warning">
<strong>Attention:</strong> <strong>Attention:</strong>
Vagrant doesn't perform any validation between the `compatibility_mode` value and the value of the [`version`](#version) option. Vagrant doesn't perform any validation between the `compatibility_mode` value and the value of the [`version`](#version) option.
</div> </div>
<div class="alert alert-info">
<strong>Compatibility Note:</strong>
This option was introduced in Vagrant 2.0. Previous Vagrant versions behave like if this option was set to `"1.8"`.
</div>
- `config_file` (string) - The path to an [Ansible Configuration file](https://docs.ansible.com/intro_configuration.html). - `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). 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).