diff --git a/plugins/provisioners/ansible/config/base.rb b/plugins/provisioners/ansible/config/base.rb index ae643591f..36fb5b903 100644 --- a/plugins/provisioners/ansible/config/base.rb +++ b/plugins/provisioners/ansible/config/base.rb @@ -6,6 +6,7 @@ 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 :config_file attr_accessor :extra_vars attr_accessor :galaxy_role_file attr_accessor :galaxy_roles_path @@ -26,6 +27,7 @@ module VagrantPlugins attr_accessor :verbose def initialize + @config_file = UNSET_VALUE @extra_vars = UNSET_VALUE @galaxy_role_file = UNSET_VALUE @galaxy_roles_path = UNSET_VALUE @@ -47,6 +49,7 @@ module VagrantPlugins end def finalize! + @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 @galaxy_roles_path = nil if @galaxy_roles_path == UNSET_VALUE diff --git a/plugins/provisioners/ansible/provisioner/base.rb b/plugins/provisioners/ansible/provisioner/base.rb index ed020b618..8e52d751f 100644 --- a/plugins/provisioners/ansible/provisioner/base.rb +++ b/plugins/provisioners/ansible/provisioner/base.rb @@ -29,20 +29,39 @@ module VagrantPlugins 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.config_file, :config_file) if config.config_file 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_file) if config.vault_password_file end - def ansible_playbook_command_for_shell_execution - shell_command = [] + def get_environment_variables_for_shell_execution + shell_env_vars = [] @environment_variables.each_pair do |k, v| - if k =~ /ANSIBLE_SSH_ARGS|ANSIBLE_ROLES_PATH/ - shell_command << "#{k}='#{v}'" + if k =~ /ANSIBLE_SSH_ARGS|ANSIBLE_ROLES_PATH|ANSIBLE_CONFIG/ + shell_env_vars << "#{k}='#{v}'" else - shell_command << "#{k}=#{v}" + shell_env_vars << "#{k}=#{v}" end end + shell_env_vars + end + + def ansible_galaxy_command_for_shell_execution + command_values = { + role_file: "'#{get_galaxy_role_file}'", + roles_path: "'#{get_galaxy_roles_path}'" + } + + shell_command = get_environment_variables_for_shell_execution + + shell_command << config.galaxy_command % command_values + + shell_command.flatten.join(' ') + end + + def ansible_playbook_command_for_shell_execution + shell_command = get_environment_variables_for_shell_execution shell_command << config.playbook_command @@ -102,6 +121,12 @@ module VagrantPlugins # Use ANSIBLE_ROLES_PATH to tell ansible-playbook where to look for roles # (there is no equivalent command line argument in ansible-playbook) @environment_variables["ANSIBLE_ROLES_PATH"] = get_galaxy_roles_path if config.galaxy_roles_path + + prepare_ansible_config_environment_variable + end + + def prepare_ansible_config_environment_variable + @environment_variables["ANSIBLE_CONFIG"] = config.config_file if config.config_file end # Auto-generate "safe" inventory file based on Vagrantfile, diff --git a/plugins/provisioners/ansible/provisioner/guest.rb b/plugins/provisioners/ansible/provisioner/guest.rb index 2cdbe3004..ef4287650 100644 --- a/plugins/provisioners/ansible/provisioner/guest.rb +++ b/plugins/provisioners/ansible/provisioner/guest.rb @@ -72,14 +72,9 @@ module VagrantPlugins end def execute_ansible_galaxy_on_guest - command_values = { - role_file: "'#{get_galaxy_role_file}'", - roles_path: "'#{get_galaxy_roles_path}'" - } + prepare_ansible_config_environment_variable - remote_command = config.galaxy_command % command_values - - execute_ansible_command_on_guest "galaxy", remote_command + execute_ansible_command_on_guest "galaxy", ansible_galaxy_command_for_shell_execution end def execute_ansible_playbook_on_guest diff --git a/plugins/provisioners/ansible/provisioner/host.rb b/plugins/provisioners/ansible/provisioner/host.rb index 697ad51a8..e0fc70f0b 100644 --- a/plugins/provisioners/ansible/provisioner/host.rb +++ b/plugins/provisioners/ansible/provisioner/host.rb @@ -20,6 +20,7 @@ module VagrantPlugins check_files_existence warn_for_unsupported_platform + execute_ansible_galaxy_from_host if config.galaxy_role_file execute_ansible_playbook_from_host end @@ -88,6 +89,8 @@ module VagrantPlugins end def execute_ansible_galaxy_from_host + prepare_ansible_config_environment_variable + command_values = { role_file: get_galaxy_role_file, roles_path: get_galaxy_roles_path @@ -97,20 +100,20 @@ module VagrantPlugins command = str_command.split(VAGRANT_ARG_SEPARATOR) command << { + env: @environment_variables, # Write stdout and stderr data, since it's the regular Ansible output notify: [:stdout, :stderr], workdir: @machine.env.root_path.to_s } - # FIXME: role_file and roles_path arguments should be quoted in the console output - ui_running_ansible_command "galaxy", str_command.gsub(VAGRANT_ARG_SEPARATOR, ' ') + ui_running_ansible_command "galaxy", ansible_galaxy_command_for_shell_execution execute_command_from_host command end def execute_ansible_playbook_from_host - prepare_command_arguments prepare_environment_variables + prepare_command_arguments # Assemble the full ansible-playbook command command = [config.playbook_command] << @command_arguments @@ -234,6 +237,7 @@ module VagrantPlugins proxy_cmd += " exec nc %h %p 2>/dev/null" ssh_options << "-o ProxyCommand='#{ proxy_cmd }'" + # TODO ssh_options << "-o ProxyCommand=\"#{ proxy_cmd }\"" end # Use an SSH ProxyCommand when corresponding Vagrant setting is defined diff --git a/test/unit/plugins/provisioners/ansible/config/guest_test.rb b/test/unit/plugins/provisioners/ansible/config/guest_test.rb index 1249bd834..a2b44a57b 100644 --- a/test/unit/plugins/provisioners/ansible/config/guest_test.rb +++ b/test/unit/plugins/provisioners/ansible/config/guest_test.rb @@ -16,7 +16,8 @@ describe VagrantPlugins::Ansible::Config::Guest do let(:existing_file) { "this/path/is/a/stub" } it "supports a list of options" do - supported_options = %w( extra_vars + supported_options = %w( config_file + extra_vars galaxy_command galaxy_role_file galaxy_roles_path diff --git a/test/unit/plugins/provisioners/ansible/config/host_test.rb b/test/unit/plugins/provisioners/ansible/config/host_test.rb index 6730ff0bc..e8d7bc27a 100644 --- a/test/unit/plugins/provisioners/ansible/config/host_test.rb +++ b/test/unit/plugins/provisioners/ansible/config/host_test.rb @@ -15,6 +15,7 @@ describe VagrantPlugins::Ansible::Config::Host, :skip_windows => true do it "supports a list of options" do supported_options = %w( ask_sudo_pass ask_vault_pass + config_file extra_vars force_remote_user galaxy_command diff --git a/test/unit/plugins/provisioners/ansible/config/shared.rb b/test/unit/plugins/provisioners/ansible/config/shared.rb index c59a31a94..92b2e0d90 100644 --- a/test/unit/plugins/provisioners/ansible/config/shared.rb +++ b/test/unit/plugins/provisioners/ansible/config/shared.rb @@ -3,6 +3,7 @@ shared_examples_for 'options shared by both Ansible provisioners' do it "assigns default values to unset common options" do subject.finalize! + 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") expect(subject.galaxy_role_file).to be_nil diff --git a/test/unit/plugins/provisioners/ansible/provisioner_test.rb b/test/unit/plugins/provisioners/ansible/provisioner_test.rb index 64d58d341..db562201d 100644 --- a/test/unit/plugins/provisioners/ansible/provisioner_test.rb +++ b/test/unit/plugins/provisioners/ansible/provisioner_test.rb @@ -59,8 +59,6 @@ VF stubbed_ui.stub(detail: "") machine.env.stub(ui: stubbed_ui) - subject.stub(:check_path) - config.playbook = 'playbook.yml' end @@ -195,7 +193,9 @@ VF before do unless example.metadata[:skip_before] config.finalize! + Vagrant::Util::Subprocess.stub(execute: Vagrant::Util::Subprocess::Result.new(0, "", "")) + subject.stub(:check_path) end end @@ -207,44 +207,46 @@ VF 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 + STUBBED_INVALID_PATH = "/test/239nfmd/invalid_path".freeze - 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") + it 'raises an error when the `playbook` file does not exist', 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: STUBBED_INVALID_PATH, + system: "host") - config.playbook = "/home/wip/test/invalid_path.yml" - config.finalize! + config.playbook = STUBBED_INVALID_PATH + 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") + expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleError, + "`playbook` does not exist on the host: #{STUBBED_INVALID_PATH}") + end + + %w(config_file extra_vars inventory_path galaxy_role_file vault_password_file).each do |option_name| + it "raises an error when the '#{option_name}' does not exist", skip_before: true, skip_after: true do + Vagrant::Util::Subprocess.stub(execute: Vagrant::Util::Subprocess::Result.new(0, "", "")) + + config.playbook = existing_file + config.send(option_name + '=', STUBBED_INVALID_PATH) + if option_name == 'extra_vars' + # little trick to auto-append the '@' prefix, which is a duty of the config validator... + config.validate(machine) + end + config.finalize! + + expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleError, + "`#{option_name}` does not exist on the host: #{STUBBED_INVALID_PATH}") 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 it "raises an error", skip_before: true, skip_after: true do config.finalize! + + subject.stub(:check_path) Vagrant::Util::Subprocess.stub(execute: Vagrant::Util::Subprocess::Result.new(1, "", "")) expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleCommandFailed) @@ -582,6 +584,20 @@ VF end end + context "with config_file option defined" do + before do + config.config_file = existing_file + end + + it "sets ANSIBLE_CONFIG environment variable" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with { |*args| + cmd_opts = args.last + expect(cmd_opts[:env]).to include("ANSIBLE_CONFIG") + expect(cmd_opts[:env]['ANSIBLE_CONFIG']).to eql(existing_file) + } + end + end + describe "with ask_vault_pass option" do before do config.ask_vault_pass = true @@ -777,6 +793,8 @@ VF it "raises an error when ansible-galaxy command fails", skip_before: true, skip_after: true do config.finalize! + + subject.stub(:check_path) Vagrant::Util::Subprocess.stub(execute: Vagrant::Util::Subprocess::Result.new(1, "", "")) expect {subject.provision}.to raise_error(VagrantPlugins::Ansible::Errors::AnsibleCommandFailed) @@ -852,11 +870,12 @@ VF config.raw_arguments = ["--why-not", "--su-user=foot", "--ask-su-pass", "--limit=all", "--private-key=./myself.key", "--extra-vars='{\"var3\":\"foo\"}'"] # environment variables + config.config_file = existing_file config.host_key_checking = true config.raw_ssh_args = ['-o ControlMaster=no'] end - it_should_set_arguments_and_environment_variables 21, 5, true + 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 'apostrophes', \\\\, \\\" and =\",\"var2\":{\"x\":42}}", "sudo" => "--sudo", @@ -883,7 +902,7 @@ VF it "shows the ansible-playbook command, with additional quotes when required" do expect(machine.env.ui).to receive(:detail).with { |full_command| - expect(full_command).to eq(%Q(PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ANSIBLE_ROLES_PATH='/up/to the stars' ANSIBLE_HOST_KEY_CHECKING=true ANSIBLE_SSH_ARGS='-o IdentitiesOnly=yes -i '/my/key1' -i '/my/key2' -o ForwardAgent=yes -o ControlMaster=no -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --connection=ssh --timeout=30 --ask-sudo-pass --ask-vault-pass --limit="machine*:&vagrant:!that_one" --inventory-file=#{generated_inventory_dir} --extra-vars="{\\"var1\\":\\"string with 'apostrophes', \\\\\\\\, \\\\\\" and =\\",\\"var2\\":{\\"x\\":42}}" --sudo --sudo-user=deployer -vvv --vault-password-file=#{File.expand_path(__FILE__)} --tags=db,www --skip-tags=foo,bar --start-at-task="joe's awesome task" --why-not --su-user=foot --ask-su-pass --limit=all --private-key=./myself.key --extra-vars='{\"var3\":\"foo\"}' playbook.yml)) + expect(full_command).to eq(%Q(PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ANSIBLE_ROLES_PATH='/up/to the stars' ANSIBLE_CONFIG='#{existing_file}' ANSIBLE_HOST_KEY_CHECKING=true ANSIBLE_SSH_ARGS='-o IdentitiesOnly=yes -i '/my/key1' -i '/my/key2' -o ForwardAgent=yes -o ControlMaster=no -o ControlMaster=auto -o ControlPersist=60s' ansible-playbook --connection=ssh --timeout=30 --ask-sudo-pass --ask-vault-pass --limit="machine*:&vagrant:!that_one" --inventory-file=#{generated_inventory_dir} --extra-vars="{\\"var1\\":\\"string with 'apostrophes', \\\\\\\\, \\\\\\" and =\\",\\"var2\\":{\\"x\\":42}}" --sudo --sudo-user=deployer -vvv --vault-password-file=#{existing_file} --tags=db,www --skip-tags=foo,bar --start-at-task="joe's awesome task" --why-not --su-user=foot --ask-su-pass --limit=all --private-key=./myself.key --extra-vars='{\"var3\":\"foo\"}' playbook.yml)) } end end diff --git a/website/source/docs/provisioning/ansible_common.html.md b/website/source/docs/provisioning/ansible_common.html.md index d76d6cfa0..2ed3b4e72 100644 --- a/website/source/docs/provisioning/ansible_common.html.md +++ b/website/source/docs/provisioning/ansible_common.html.md @@ -17,6 +17,10 @@ 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. +- `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). + - `extra_vars` (string or hash) - Pass additional variables (with highest priority) to the playbook. This parameter can be a path to a JSON or YAML file, or a hash. diff --git a/website/source/docs/provisioning/ansible_intro.html.md b/website/source/docs/provisioning/ansible_intro.html.md index de9c07697..be3ef1ec0 100644 --- a/website/source/docs/provisioning/ansible_intro.html.md +++ b/website/source/docs/provisioning/ansible_intro.html.md @@ -246,12 +246,11 @@ Certain settings in Ansible are (only) adjustable via a [configuration file](htt When shipping an Ansible configuration file it is good to know that: - - it is possible to reference an Ansible configuration file via `ANSIBLE_CONFIG` environment variable, if you want to be flexible about the location of this file. - - as of Ansible 1.5, the lookup order is the following: - - - `ANSIBLE_CONFIG` an environment variable - - `ansible.cfg` in the runtime working directory - - `.ansible.cfg` in the user home directory - - `/etc/ansible/ansible.cfg` - - - `ansible-playbook` doesn't look for a configuration file relative to the playbook file location (e.g. in the same directory) + - as of Ansible 1.5, the lookup order is the following: + - any path set as `ANSIBLE_CONFIG` environment variable + - `ansible.cfg` in the runtime working directory + - `.ansible.cfg` in the user home directory + - `/etc/ansible/ansible.cfg` + - Ansible commands don't look for a configuration file relative to the playbook file location (e.g. in the same directory) + - an `ansible.cfg` file located in the same directory as your `Vagrantfile` will be used by default. + - it is also possible to reference any other location with the [config_file](/docs/provisioning/ansible_common.html#config_file) provisioner option. In this case, Vagrant will set the `ANSIBLE_CONFIG` environment variable accordingly.