provisioners/ansible(both): fix ansible config files presence checks

With this change, the presence of Ansible configuration files (like
playbook file, inventory path, galaxy role file, etc.) is no longer
performed by the `config` classes, but by the `provisioner` classes
(at the beginning of the provision command).

This change fixes several issues:

- Resolve #6984 as `provision` method are only executed when remote
  (ssh) communication with the guest machine is possible.
- Resolve #6763 in a better way than 4e451c6 initially did.
- Improve the general provisioner speed since the `config` checks are
  actually triggered by many vagrant actions (e.g. `destroy`,...), and
  can also be triggered multiple times during a vagrant run (e.g. on
  callback request made by the machine provider).

Unlike the former `config`-based checks, the provision action won't
collect all the invalid options, but only report the first invalid
option found and abort the execution.

Some unit tests were not implemented yet to save my scarce "open source
contribution time" for other important issues, but they should be done
at last via GH-6633.
This commit is contained in:
Gilles Cornu 2016-06-01 00:30:07 +02:00
parent 228abf27a6
commit a7ee56459b
12 changed files with 133 additions and 285 deletions

View File

@ -109,10 +109,8 @@ BUG FIXES:
galaxy resources when running on a Windows host [GH-6740, GH-6757] galaxy resources when running on a Windows host [GH-6740, GH-6757]
- provisioners/ansible_local: Change the way to verify `ansible-galaxy` - provisioners/ansible_local: Change the way to verify `ansible-galaxy`
presence, to avoid a non-zero status code with Ansible 2.0 [GH-6793] presence, to avoid a non-zero status code with Ansible 2.0 [GH-6793]
- provisioners/ansible_local: The configuration sanity checks now only warn - provisioners/ansible(both provisioners): The Ansible configuration files
on missing files or directories, so that the requested vagrant command is detection is only executed by the `provision` action [GH-6763, GH-6984]
always executed (e.g. `vagrant destroy` is not aborted when the configured
playbook is not present on the guest) [GH-6763]
- provisioners/chef: Do not use double sudo when installing - provisioners/chef: Do not use double sudo when installing
[GGH-6805, GH-6804] [GGH-6805, GH-6804]
- provisioners/chef: Change the default channel to "stable" (previously it - provisioners/chef: Change the default channel to "stable" (previously it

View File

@ -74,35 +74,16 @@ module VagrantPlugins
@errors << I18n.t("vagrant.provisioners.ansible.errors.no_playbook") @errors << I18n.t("vagrant.provisioners.ansible.errors.no_playbook")
end end
if playbook # Validate that extra_vars is either a Hash or a String (for a file path)
check_path_is_a_file(machine, playbook, "vagrant.provisioners.ansible.errors.playbook_path_invalid")
end
if inventory_path
check_path_exists(machine, inventory_path, "vagrant.provisioners.ansible.errors.inventory_path_invalid")
end
if galaxy_role_file
check_path_is_a_file(machine, galaxy_role_file, "vagrant.provisioners.ansible.errors.galaxy_role_file_invalid")
end
if vault_password_file
check_path_is_a_file(machine, vault_password_file, "vagrant.provisioners.ansible.errors.vault_password_file_invalid")
end
# Validate that extra_vars is either a hash, or a path to an existing file
if extra_vars if extra_vars
extra_vars_is_valid = extra_vars.kind_of?(Hash) || extra_vars.kind_of?(String) extra_vars_is_valid = extra_vars.kind_of?(Hash) || extra_vars.kind_of?(String)
if extra_vars.kind_of?(String) if extra_vars.kind_of?(String)
# Accept the usage of '@' prefix in Vagrantfile (e.g. '@vars.yml' # Accept the usage of '@' prefix in Vagrantfile
# and 'vars.yml' are both supported) # (e.g. '@vars.yml' and 'vars.yml' are both supported)
match_data = /^@?(.+)$/.match(extra_vars) match_data = /^@?(.+)$/.match(extra_vars)
extra_vars_path = match_data[1].to_s extra_vars_path = match_data[1].to_s
extra_vars_is_valid = check_path_is_a_file(machine, extra_vars_path)
if extra_vars_is_valid
@extra_vars = '@' + extra_vars_path @extra_vars = '@' + extra_vars_path
end end
end
if !extra_vars_is_valid if !extra_vars_is_valid
@errors << I18n.t( @errors << I18n.t(

View File

@ -35,32 +35,6 @@ module VagrantPlugins
{ "ansible local provisioner" => @errors } { "ansible local provisioner" => @errors }
end end
protected
def check_path(machine, path, test_args, error_message_key = nil)
remote_path = Helpers::expand_path_in_unix_style(path, @provisioning_path)
if machine.communicate.ready? && !machine.communicate.test("test #{test_args} #{remote_path}")
if error_message_key
# only show warnings, as raising an error would abort the request
# vagrant action (e.g. prevent `destroy` to be executed)
machine.ui.warn(I18n.t(error_message_key, path: remote_path, system: "guest"))
end
return false
end
# when the machine is not ready for SSH communication,
# the check is "optimistically" bypassed.
true
end
def check_path_is_a_file(machine, path, error_message_key = nil)
check_path(machine, path, "-f", error_message_key)
end
def check_path_exists(machine, path, error_message_key = nil)
check_path(machine, path, "-e", error_message_key)
end
end end
end end
end end

View File

@ -48,27 +48,6 @@ module VagrantPlugins
{ "ansible remote provisioner" => @errors } { "ansible remote provisioner" => @errors }
end end
protected
def check_path(machine, path, path_test_method, error_message_key = nil)
expanded_path = Pathname.new(path).expand_path(machine.env.root_path)
if !expanded_path.public_send(path_test_method)
if error_message_key
@errors << I18n.t(error_message_key, path: expanded_path, system: "host")
end
return false
end
true
end
def check_path_is_a_file(machine, path, error_message_key = nil)
check_path(machine, path, "file?", error_message_key)
end
def check_path_exists(machine, path, error_message_key = nil)
check_path(machine, path, "exist?", error_message_key)
end
end end
end end
end end

View File

@ -25,6 +25,15 @@ module VagrantPlugins
@inventory_path = nil @inventory_path = nil
end end
def check_files_existence
check_path_is_a_file config.playbook, :playbook
check_path_exists config.inventory_path, :inventory_path if config.inventory_path
check_path_is_a_file config.extra_vars[1..-1], :extra_vars if has_an_extra_vars_file_argument
check_path_is_a_file config.galaxy_role_file, :galaxy_role_file if config.galaxy_role_file
check_path_is_a_file config.vault_password_file, :vault_password if config.vault_password_file
end
def ansible_playbook_command_for_shell_execution def ansible_playbook_command_for_shell_execution
shell_command = [] shell_command = []
@environment_variables.each_pair do |k, v| @environment_variables.each_pair do |k, v|
@ -198,8 +207,12 @@ module VagrantPlugins
return inventory_groups return inventory_groups
end end
def has_an_extra_vars_file_argument
config.extra_vars && config.extra_vars.kind_of?(String) && config.extra_vars =~ /^@.+$/
end
def extra_vars_argument def extra_vars_argument
if config.extra_vars.kind_of?(String) and config.extra_vars =~ /^@.+$/ if has_an_extra_vars_file_argument
# A JSON or YAML file is referenced. # A JSON or YAML file is referenced.
config.extra_vars config.extra_vars
else else

View File

@ -14,6 +14,7 @@ module VagrantPlugins
end end
def provision def provision
check_files_existence
check_and_install_ansible check_and_install_ansible
execute_ansible_galaxy_on_guest if config.galaxy_role_file execute_ansible_galaxy_on_guest if config.galaxy_role_file
execute_ansible_playbook_on_guest execute_ansible_playbook_on_guest
@ -145,6 +146,31 @@ module VagrantPlugins
end end
end end
def check_path(path, test_args, option_name)
# Checks for the existence of given file (or directory) on the guest system,
# and error if it doesn't exist.
remote_path = Helpers::expand_path_in_unix_style(path, config.provisioning_path)
command = "test #{test_args} #{remote_path}"
@machine.communicate.execute(
command,
error_class: Ansible::Errors::AnsibleError,
error_key: :config_file_not_found,
config_option: option_name,
path: remote_path,
system: "guest"
)
end
def check_path_is_a_file(path, error_message_key)
check_path(path, "-f", error_message_key)
end
def check_path_exists(path, error_message_key)
check_path(path, "-e", error_message_key)
end
end end
end end
end end

View File

@ -18,6 +18,7 @@ module VagrantPlugins
# At this stage, the SSH access is guaranteed to be ready # At this stage, the SSH access is guaranteed to be ready
@ssh_info = @machine.ssh_info @ssh_info = @machine.ssh_info
check_files_existence
warn_for_unsupported_platform warn_for_unsupported_platform
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
@ -257,6 +258,28 @@ module VagrantPlugins
ssh_options.join(' ') ssh_options.join(' ')
end end
def check_path(path, path_test_method, option_name)
# Checks for the existence of given file (or directory) on the host system,
# and error if it doesn't exist.
expanded_path = Pathname.new(path).expand_path(@machine.env.root_path)
if !expanded_path.public_send(path_test_method)
raise Ansible::Errors::AnsibleError,
_key: :config_file_not_found,
config_option: option_name,
path: expanded_path,
system: "host"
end
end
def check_path_is_a_file(path, option_name)
check_path(path, "file?", option_name)
end
def check_path_exists(path, option_name)
check_path(path, "exist?", option_name)
end
end end
end end
end end

View File

@ -2128,22 +2128,16 @@ en:
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_local.html See https://docs.vagrantup.com/v2/provisioning/ansible_local.html
for more information. for more information.
config_file_not_found: |-
`%{config_option}` does not exist on the %{system}: %{path}
extra_vars_invalid: |- extra_vars_invalid: |-
`extra_vars` must be a hash or a path to an existing file. Received: %{value} (as %{type}) `extra_vars` must be a hash or a path to an existing file. Received: %{value} (as %{type})
no_playbook: |-
`playbook` file path must be set.
raw_arguments_invalid: |- raw_arguments_invalid: |-
`raw_arguments` must be an array of strings. Received: %{value} (as %{type}) `raw_arguments` must be an array of strings. Received: %{value} (as %{type})
raw_ssh_args_invalid: |- raw_ssh_args_invalid: |-
`raw_ssh_args` must be an array of strings. Received: %{value} (as %{type}) `raw_ssh_args` must be an array of strings. Received: %{value} (as %{type})
galaxy_role_file_invalid: |-
`galaxy_role_file` does not exist on the %{system}: %{path}
inventory_path_invalid: |-
`inventory_path` does not exist on the %{system}: %{path}
no_playbook: |-
`playbook` file path must be set.
playbook_path_invalid: |-
`playbook` does not exist on the %{system}: %{path}
vault_password_file_invalid: |-
`vault_password_file` does not exist on the %{system}: %{path}
installing: "Installing Ansible..." installing: "Installing Ansible..."
running_galaxy: "Running ansible-galaxy..." running_galaxy: "Running ansible-galaxy..."
running_playbook: "Running ansible-playbook..." running_playbook: "Running ansible-playbook..."

View File

@ -14,7 +14,6 @@ describe VagrantPlugins::Ansible::Config::Guest do
let(:communicator) { double("communicator") } let(:communicator) { double("communicator") }
let(:existing_file) { "this/path/is/a/stub" } let(:existing_file) { "this/path/is/a/stub" }
let(:non_existing_file) { "this/path/does/not/exist" }
it "supports a list of options" do it "supports a list of options" do
supported_options = %w( extra_vars supported_options = %w( extra_vars
@ -57,107 +56,10 @@ describe VagrantPlugins::Ansible::Config::Guest do
describe "#validate" do describe "#validate" do
before do before do
machine.stub(communicate: communicator)
end
context "when the machine is not ready to communicate" do
before do
allow(communicator).to receive(:ready?).and_return(false)
end
it "cannot check the existence of remote file" do
subject.playbook = non_existing_file
subject.finalize!
result = subject.validate(machine)
expect(result["ansible local provisioner"]).to eql([])
# FIXME: commented out because this `communicator.ready?` stub is not working as expected
# expect(communicator).to receive(:ready?).ordered
# Note that communicator mock will fail if it receives an unexpected message,
# which is part of this spec.
end
end
context "when the machine is ready to communicate" do
before do
allow(communicator).to receive(:ready?).and_return(true)
allow(communicator).to receive(:test).and_return(false)
allow(communicator).to receive(:test) do |arg1|
arg1.include?("#{existing_file}")
end
stubbed_ui = Vagrant::UI::Colored.new
machine.stub(ui: stubbed_ui)
allow(machine.ui).to receive(:warn)
subject.playbook = existing_file subject.playbook = existing_file
end end
it_behaves_like "an Ansible provisioner", "/vagrant", "local" it_behaves_like "an Ansible provisioner", "/vagrant", "local"
it "only shows a warning if the playbook file does not exist" do
subject.playbook = non_existing_file
subject.finalize!
result = subject.validate(machine)
expect(result["ansible remote provisioner"]).to be_nil
# FIXME
# expect(machine).to receive(:ui).with { |warning_text|
# expect(warning_text).to eq("`playbook` does not exist on the guest: /vagrant/this/path/does/not/exist")
# }
end
it "only shows a warning if inventory_path is specified, but does not exist" do
subject.inventory_path = non_existing_file
subject.finalize!
result = subject.validate(machine)
expect(result["ansible remote provisioner"]).to be_nil
# FIXME
# expect(machine.ui).to receive(:warn).with { |warning_text|
# expect(warning_text).to eq("`inventory_path` does not exist on the guest: this/path/does/not/exist")
# }
end
it "only shows a warning if vault_password_file is specified, but does not exist" do
subject.vault_password_file = non_existing_file
subject.finalize!
result = subject.validate(machine)
expect(result["ansible remote provisioner"]).to be_nil
# FIXME
# expect(machine.ui).to receive(:warn).with { |warning_text|
# expect(warning_text).to eq("`inventory_path` does not exist on the guest: this/path/does/not/exist")
# }
end
it "it doesn't consider missing files on the remote system as errors" do
subject.playbook = non_existing_file
subject.inventory_path = non_existing_file
subject.extra_vars = non_existing_file
subject.finalize!
result = subject.validate(machine)
expect(result["ansible local provisioner"]).to include(
I18n.t("vagrant.provisioners.ansible.errors.extra_vars_invalid",
type: subject.extra_vars.class.to_s,
value: subject.extra_vars.to_s))
expect(result["ansible local provisioner"]).to_not include(
I18n.t("vagrant.provisioners.ansible.errors.playbook_path_invalid",
path: File.join("/vagrant", non_existing_file), system: "guest"))
expect(result["ansible local provisioner"]).to_not include(
I18n.t("vagrant.provisioners.ansible.errors.inventory_path_invalid",
path: File.join("/vagrant", non_existing_file), system: "guest"))
end
end
end end
end end

View File

@ -11,7 +11,6 @@ describe VagrantPlugins::Ansible::Config::Host, :skip_windows => true do
let(:machine) { double("machine", env: Vagrant::Environment.new) } let(:machine) { double("machine", env: Vagrant::Environment.new) }
let(:existing_file) { File.expand_path(__FILE__) } let(:existing_file) { File.expand_path(__FILE__) }
let(:non_existing_file) { "/this/does/not/exist" }
it "supports a list of options" do it "supports a list of options" do
supported_options = %w( ask_sudo_pass supported_options = %w( ask_sudo_pass
@ -74,50 +73,6 @@ describe VagrantPlugins::Ansible::Config::Host, :skip_windows => true do
it_behaves_like "an Ansible provisioner", "", "remote" it_behaves_like "an Ansible provisioner", "", "remote"
it "returns an error if the playbook file does not exist" do
subject.playbook = non_existing_file
subject.finalize!
result = subject.validate(machine)
expect(result["ansible remote provisioner"]).to eql([
I18n.t("vagrant.provisioners.ansible.errors.playbook_path_invalid",
path: non_existing_file, system: "host")
])
end
it "returns an error if galaxy_role_file is specified, but does not exist" do
subject.galaxy_role_file = non_existing_file
subject.finalize!
result = subject.validate(machine)
expect(result["ansible remote provisioner"]).to eql([
I18n.t("vagrant.provisioners.ansible.errors.galaxy_role_file_invalid",
path: non_existing_file, system: "host")
])
end
it "returns an error if inventory_path is specified, but does not exist" do
subject.inventory_path = non_existing_file
subject.finalize!
result = subject.validate(machine)
expect(result["ansible remote provisioner"]).to eql([
I18n.t("vagrant.provisioners.ansible.errors.inventory_path_invalid",
path: non_existing_file, system: "host")
])
end
it "returns an error if vault_password_file is specified, but does not exist" do
subject.vault_password_file = non_existing_file
subject.finalize!
result = subject.validate(machine)
expect(result["ansible remote provisioner"]).to eql([
I18n.t("vagrant.provisioners.ansible.errors.vault_password_file_invalid",
path: non_existing_file, system: "host")
])
end
it "returns an error if the raw_ssh_args is of the wrong data type" do it "returns an error if the raw_ssh_args is of the wrong data type" do
subject.raw_ssh_args = { arg1: 1, arg2: "foo" } subject.raw_ssh_args = { arg1: 1, arg2: "foo" }
subject.finalize! subject.finalize!
@ -138,25 +93,6 @@ describe VagrantPlugins::Ansible::Config::Host, :skip_windows => true do
expect(subject.raw_arguments).to eql(["-o ControlMaster=no"]) expect(subject.raw_arguments).to eql(["-o ControlMaster=no"])
end end
it "it collects and returns all detected errors" do
subject.playbook = non_existing_file
subject.inventory_path = non_existing_file
subject.extra_vars = non_existing_file
subject.finalize!
result = subject.validate(machine)
expect(result["ansible remote provisioner"]).to include(
I18n.t("vagrant.provisioners.ansible.errors.playbook_path_invalid",
path: non_existing_file, system: "host"))
expect(result["ansible remote provisioner"]).to include(
I18n.t("vagrant.provisioners.ansible.errors.extra_vars_invalid",
type: subject.extra_vars.class.to_s,
value: subject.extra_vars.to_s))
expect(result["ansible remote provisioner"]).to include(
I18n.t("vagrant.provisioners.ansible.errors.inventory_path_invalid",
path: non_existing_file, system: "host"))
end
end end
end end

View File

@ -29,13 +29,6 @@ shared_examples_for 'an Ansible provisioner' do | path_prefix, ansible_setup |
provisioner_label = "ansible #{ansible_setup} provisioner" provisioner_label = "ansible #{ansible_setup} provisioner"
provisioner_system = ansible_setup == "local" ? "guest" : "host" provisioner_system = ansible_setup == "local" ? "guest" : "host"
it "passes if the playbook option refers to an existing file" do
subject.finalize!
result = subject.validate(machine)
expect(result[provisioner_label]).to eql([])
end
it "returns an error if the playbook option is undefined" do it "returns an error if the playbook option is undefined" do
subject.playbook = nil subject.playbook = nil
subject.finalize! subject.finalize!
@ -46,14 +39,6 @@ shared_examples_for 'an Ansible provisioner' do | path_prefix, ansible_setup |
]) ])
end end
it "passes if the extra_vars option refers to an existing file" do
subject.extra_vars = existing_file
subject.finalize!
result = subject.validate(machine)
expect(result[provisioner_label]).to eql([])
end
it "passes if the extra_vars option is a hash" do it "passes if the extra_vars option is a hash" do
subject.extra_vars = { var1: 1, var2: "foo" } subject.extra_vars = { var1: 1, var2: "foo" }
subject.finalize! subject.finalize!
@ -62,21 +47,6 @@ shared_examples_for 'an Ansible provisioner' do | path_prefix, ansible_setup |
expect(result[provisioner_label]).to eql([]) expect(result[provisioner_label]).to eql([])
end end
# FIXME:
# The guest-based config should actually NOT return this error,
# but only display a warning (similarly to GH-6764 and 4e451c6)
it "returns an error if the extra_vars option refers to a file that does not exist" do
subject.extra_vars = non_existing_file
subject.finalize!
result = subject.validate(machine)
expect(result[provisioner_label]).to eql([
I18n.t("vagrant.provisioners.ansible.errors.extra_vars_invalid",
type: subject.extra_vars.class.to_s,
value: subject.extra_vars.to_s)
])
end
it "returns an error if the extra_vars option is of wrong data type" do it "returns an error if the extra_vars option is of wrong data type" do
subject.extra_vars = ["var1", 3, "var2", 5] subject.extra_vars = ["var1", 3, "var2", 5]
subject.finalize! subject.finalize!
@ -89,12 +59,12 @@ shared_examples_for 'an Ansible provisioner' do | path_prefix, ansible_setup |
]) ])
end end
it "passes if inventory_path refers to an existing location" do it "converts a raw_arguments option defined as a String into an Array" do
subject.inventory_path = existing_file subject.raw_arguments = "--foo=bar"
subject.finalize! subject.finalize!
result = subject.validate(machine) result = subject.validate(machine)
expect(result[provisioner_label]).to eql([]) expect(subject.raw_arguments).to eql(%w(--foo=bar))
end end
it "returns an error if the raw_arguments is of the wrong data type" do it "returns an error if the raw_arguments is of the wrong data type" do
@ -109,12 +79,25 @@ shared_examples_for 'an Ansible provisioner' do | path_prefix, ansible_setup |
]) ])
end end
it "converts a raw_arguments option defined as a String into an Array" do it "it collects and returns all detected errors" do
subject.raw_arguments = "--foo=bar" subject.playbook = nil
subject.extra_vars = ["var1", 3, "var2", 5]
subject.raw_arguments = { arg1: 1, arg2: "foo" }
subject.finalize! subject.finalize!
result = subject.validate(machine) result = subject.validate(machine)
expect(subject.raw_arguments).to eql(%w(--foo=bar))
expect(result[provisioner_label].size).to eql(3)
expect(result[provisioner_label]).to include(
I18n.t("vagrant.provisioners.ansible.errors.no_playbook"))
expect(result[provisioner_label]).to include(
I18n.t("vagrant.provisioners.ansible.errors.extra_vars_invalid",
type: subject.extra_vars.class.to_s,
value: subject.extra_vars.to_s))
expect(result[provisioner_label]).to include(
I18n.t("vagrant.provisioners.ansible.errors.raw_arguments_invalid",
type: subject.raw_arguments.class.to_s,
value: subject.raw_arguments.to_s))
end end
describe "sudo option" do describe "sudo option" do

View File

@ -59,6 +59,8 @@ VF
stubbed_ui.stub(detail: "") stubbed_ui.stub(detail: "")
machine.env.stub(ui: stubbed_ui) machine.env.stub(ui: stubbed_ui)
subject.stub(:check_path)
config.playbook = 'playbook.yml' config.playbook = 'playbook.yml'
end end
@ -203,6 +205,43 @@ VF
end end
end end
describe 'checking existence of Ansible configuration files' do
describe 'when the playbook file does not exist' do
it "raises an error", skip_before: true, skip_after: true do
subject.stub(:check_path).and_raise(VagrantPlugins::Ansible::Errors::AnsibleError,
_key: :config_file_not_found,
config_option: "playbook",
path: "/home/wip/test/invalid_path.yml",
system: "host")
config.playbook = "/home/wip/test/invalid_path.yml"
config.finalize!
expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleError,
"`playbook` does not exist on the host: /home/wip/test/invalid_path.yml")
end
end
describe 'when the inventory path does not exist' do
it "raises an error"
end
describe 'when the extra_vars file does not exist' do
it "raises an error"
end
describe 'when the galaxy_role_file does not exist' do
it "raises an error"
end
describe 'when the vault_password_file does not exist' do
it "raises an error"
end
end
describe 'when ansible-playbook fails' do describe 'when ansible-playbook fails' do
it "raises an error", skip_before: true, skip_after: true do it "raises an error", skip_before: true, skip_after: true do
config.finalize! config.finalize!