diff --git a/plugins/provisioners/ansible/provisioner/host.rb b/plugins/provisioners/ansible/provisioner/host.rb index 9f9dc5d0d59..6c989d5274e 100644 --- a/plugins/provisioners/ansible/provisioner/host.rb +++ b/plugins/provisioners/ansible/provisioner/host.rb @@ -302,7 +302,18 @@ def prepare_ansible_ssh_args # Multiple Private Keys unless !config.inventory_path && @ssh_info[:private_key_path].size == 1 @ssh_info[:private_key_path].each do |key| - ssh_options += ["-o", "IdentityFile=%s" % [key.gsub('%', '%%')]] + # Escape any '%' characters to avoid formatting issues when + # building the final command string for Ansible. If the path + # contains spaces, wrap it in double quotes so the OpenSSH + # client invoked by Ansible treats the whole path as a single + # argument (otherwise the path would be split on spaces and + # could be interpreted as a hostname or separate option). + escaped = key.gsub('%', '%%') + if escaped.include?(' ') + ssh_options += ["-o", "IdentityFile=\"%s\"" % [escaped]] + else + ssh_options += ["-o", "IdentityFile=%s" % [escaped]] + end end end diff --git a/test/unit/plugins/provisioners/ansible/provisioner_test.rb b/test/unit/plugins/provisioners/ansible/provisioner_test.rb index 4c8b77f4cd3..c96d2639a30 100644 --- a/test/unit/plugins/provisioners/ansible/provisioner_test.rb +++ b/test/unit/plugins/provisioners/ansible/provisioner_test.rb @@ -856,6 +856,23 @@ def ensure_that_config_is_valid end end + describe "with an identity file path containing spaces and a custom inventory_path" do + before do + ssh_info[:private_key_path] = ['/path/with a space/key'] + # When inventory_path is provided, the provisioner will add IdentityFile + # entries to ANSIBLE_SSH_ARGS even if there's a single key. Use a value + # containing spaces to reproduce the problematic scenario. + config.inventory_path = '/some inventory/with spaces/inv' + end + + it "wraps the IdentityFile path in double quotes inside ANSIBLE_SSH_ARGS" do + 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=\"/path/with a space/key\"") + }.and_return(default_execute_result) + end + end + describe "with an identity file containing `%`" do before do ssh_info[:private_key_path] = ['/foo%bar/key', '/bar%%buz/key']