hashicorp/vagrant

Vagrant 1.8, ansible_local `extra_vars` being ignored

elsom25 opened this issue ยท 20 comments

Porting applications to use the lovely new ansible_local, and running into a few issues.

In my Vagrantfile, I have:

config.vm.provision "ansible_local" do |ansible|
    ansible.provisioning_path = "/my-app/playbooks"
    ansible.playbook = "site.yml"
    ansible.inventory_path = "development"
    ansible.extra_vars = {
      is_vagrant: true,
      should_seed: true,
      source: "/my-app"
    }
    ansible.verbose = "vvvv"
  end

Values have been scrubbed and simplified. Many of them actually inspect environment variables to determine there value, preventing me from moving them to my inventory group_var file.

default values defined in group_vars/all is is_vagrant=false and should_seed=false

which generates (what looks correctly):

cd /my-app/playbooks && PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ansible-playbook --limit='default' --inventory-file=development --extra-vars={"is_vagrant":true,"should_seed":true,"source":"/cirro-legacy"} -vvvv site.yml

But then I get (in a dummy task created to display variable output):

TASK: [common | display vars] *************************************************
<default> REMOTE_MODULE command echo "is_vagrant=False should_seed=False source=/cirro-legacy" #USE_SHELL

I can work around this temporarily, but something seems amiss.

Hi @elsom25, thanks for your problem report. This looks strange, but likely to be an Ansible (usage) issue.

Could you please log into the machine (vagrant ssh), and manually run the same command

cd /my-app/playbooks && PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ansible-playbook --limit='default' --inventory-file=development --extra-vars={"is_vagrant":true,"should_seed":true,"source":"/my-app"} -vvvv site.yml

I'd expect you get the same result...

Hey @gildegoma: as you suspect, it fails the same way.

Looking through ansible docs for --extra-vars, I can't find anything that suggests there's support for a raw hash, but rather, it seems it expects quoted json.

Testing this out by changing the command to:

cd /my-app/playbooks && PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ansible-playbook --limit='default' --inventory-file=development --extra-vars='{"is_vagrant":true,"should_seed":true,"source":"/my-app"}' -vvvv site.yml

Leads to the values being set correctly.

@elsom25 thanks for checking. It's indeed a bug in ansible_local handling of extra_vars in hash format. Sorry for that!

Until it's fixed, you can store these extra variables in a YAML or JSON file, and use something like ansible.extra_vars = '/my-app/vagrant-vars.yml'.

Note that the ansible provisioner is not affected (quoting is not required by command calls via childprocess library).

i was about to open a pull request... but since @gildegoma mentions it is not necessary for the ansible provisioner my quick fix might break something there.

since the ansible documentation uses single quotes around the extra_vars JSON (http://docs.ansible.com/ansible/playbooks_variables.html#passing-variables-on-the-command-line) i hacked string formatting into plugins/provisioners/ansible/provisioner/base.rb:

        def extra_vars_argument
          if config.extra_vars.kind_of?(String) and config.extra_vars =~ /^@.+$/
            # A JSON or YAML file is referenced.
            config.extra_vars
          else
            # Expected to be a Hash after config validation.
            "'#{config.extra_vars.to_json}'"
          end
        end

and then the ruby hash is properly passed on to ansible as a single quoted JSON string.

another workaround is "escaped double quoting":

    ansible.extra_vars = {
      "\"service_name\"": "\"djboot\""
    }

EDIT: that does not work when using more than one element in the hash. then bash interprets the {} as array.

the resulting --extra-vars={"\"service_name\"":"\"foo\""} then does not loose the escaped doublequotes. otherwise the shell (bash, sh) eats the doublequotes:

$ echo {"service_name":"foo"}
{service_name:foo}

I've just stumbled upon the same issue and dared to open a PR that fixes it.

@gildegoma looks like childprocess doesn't mind such quoting because I was able to provision my machine with both ansible and ansible-local. Could you take a look at #6820?

@kamazee thanks a lot for the PR, I'll check it asap (i.e. not right now ๐Ÿ˜‰)

+1 for this fix from me, I am trying to pass proxy settings through the ansible provisioner

ansible.extra_vars = {
  proxy_env: {
    http_proxy: ENV['http_proxy'] || "",
    https_proxy: ENV['http_proxy'] || ""
  }
}

but all what comes out in the command line arg is unquoted

    web: Running ansible-playbook...
PYTHONUNBUFFERED=1 \
ANSIBLE_FORCE_COLOR=true \
ANSIBLE_HOST_KEY_CHECKING=false \
ansible-playbook --connection=ssh --timeout=30 \
                 --extra-vars=ansible_ssh_user='vagrant' \
                 --limit='web.vagrant.dev' \
                 --inventory-file=./inventory \
                 --extra-vars={"proxy_env":{"http_proxy":"http://proxy:8888","https_proxy":"http://proxy:8888"}} \
                 --sudo site.yml

whilst --extra-vars must be quoted in single quotes to work

    web: Running ansible-playbook...
PYTHONUNBUFFERED=1 \
ANSIBLE_FORCE_COLOR=true \
ANSIBLE_HOST_KEY_CHECKING=false \
ansible-playbook --connection=ssh --timeout=30 \
                 --extra-vars=ansible_ssh_user='vagrant' \
                 --limit='web.vagrant.dev' \
                 --inventory-file=./inventory \
                 --extra-vars='{"proxy_env":{"http_proxy":"http://proxy:8888","https_proxy":"http://proxy:8888"}}' \
                 --sudo site.yml

@fdemmer I modified your approach slightly to also escape the quotes after the extra_vars hash is converted to JSON. Now it does work properly for me.

def extra_vars_argument
  if config.extra_vars.kind_of?(String) and config.extra_vars =~ /^@.+$/
    # A JSON or YAML file is referenced.
    config.extra_vars
  else
    # Expected to be a Hash after config validation.
    "'#{config.extra_vars.to_json.gsub("\"", %q(\\\"))}'"
  end
end

I've worked around this issue by using raw arguments.

This doesn't work:

ansible.extra_vars = {
  provider: "virtualbox"
}

This does:

ansible.raw_arguments = [
  "--extra-vars",
  "provider=virtualbox"
]

I hope it can be fixed before 1.8.2.

Is this issue holding up the 1.8.2 release? I noticed it's not in the 1.8.2 milestone, though one other ticket in that milestone mentioned this ticket as a dependency...

@geerlingguy no, there is no hold up ;-)

Of course, I hope this issue will also be fixed for the next Vagrant release ๐Ÿ˜ƒ

@kamazee I actually started to review #6820 some days ago, and found some problems (which I don't remember right now). I'll comment directly onto the PR (but no commitment about the deadline, sorry).

@gildegoma can you please double-check about those comments? i'd like to help, but don't see any comments in the code or conversation of pr #6820 besides the "needs-review" label and #6800 also only mentions "needs review", left the checkbox open, but still merged.

@fdemmer #6820 will be reviewed (I haven't started yet, sorry). If you want to help, you chan also check it out and bring your feedbacks on the PR thread. That would be great :)


For the records: I initially thought to address this issue via #6800. But I finally decided to merge this "growing beast" to get all these other fixes into master branch (I usually prefer to keep one PR focused on one Issue). I updated #6800 description, hoping to make things more clear...

Let's flag it for Milestone 1.8.2...

My workaround (Vagrant 1.8.1, ansible_local 1.9.5):

ansible.raw_arguments = [
    "--extra-vars",
    "\"a=b c=d\""
]

fixed with #7103 imho.

sorry, for being annoying, but is there an eta for 1.8.2? would help a fellow developer on windows a lot...

Below a simple test to verify the fix.

Vagrant version

Vagrant 1.8.1

Ansible version (also affects 1.9.x in exact same way)

ansible-playbook 2.0.1.0
config file =
configured module search path = Default w/o overrides

Host operating system

OSX 10.11.4

Guest operating system

Centos7 / any linux that can be targeted by ansible really

Vagrantfile

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure(2) do |config|

  config.vm.box = "centos/7"

  if Vagrant.has_plugin?("vagrant-vbguest")
    config.vbguest.auto_update = false
  end

  # provisioning ansible
  config.vm.provision "ansible" do |ansible|
    ansible.playbook = "play.yml"
    ansible.verbose = "vvvv"
    ansible.extra_vars = {
      # send a string of generated keycloak servers to the haproxy
      extra1: Array.new(3){ |n| "bla#{(1 + n).to_s.rjust(2,'0')}" },
      extra2: true
    }
  end # ansible provisioning

end

play.yml

---
- hosts: all
  # strategy: debug
  gather_facts: no
  tasks:
    - debug: var=extra1

    - fail: msg="extra1 arg not set"
      when: extra1 is undefined

Debug output

$ vagrant provision
==> default: Running provisioner: ansible...
    default: Running ansible-playbook...
PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true 
ANSIBLE_HOST_KEY_CHECKING=false 
ANSIBLE_SSH_ARGS='-o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o ControlMaster=auto -o ControlPersist=60s' 
ansible-playbook --connection=ssh --timeout=30 --limit='default' \
                           --inventory-file=/Users/fred/workspaces/extraargs/.vagrant/provisioners/ansible/inventory \
                           --extra-vars='{"extra1":["bla01","bla02","bla03"],"extra2":true}'\
                           -vvvv play.yml
No config file found; using defaults
Loaded callback default of type stdout, v2.0
1 plays in play.yml

PLAY ***************************************************************************

TASK [debug] *******************************************************************
task path: /Users/fred/workspaces/extraargs/play.yml:6
ok: [default] => {
    "extra1": "VARIABLE IS NOT DEFINED!"
}

PLAY RECAP *********************************************************************
default                    : ok=1    changed=0    unreachable=0    failed=1

Expected behavior

Extra args should be passed properly to the ansible playbook executable:

PYTHONUNBUFFERED=1 \
ANSIBLE_FORCE_COLOR=true \
ANSIBLE_HOST_KEY_CHECKING=false \
ANSIBLE_SSH_ARGS='-o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o ControlMaster=auto -o ControlPersist=60s' \
ansible-playbook --connection=ssh \
                 --timeout=30 \
                 --limit='default' \
                 --inventory-file=./.vagrant/provisioners/ansible/inventory \
                 --extra-vars='{"extra1":["bla01","bla02","bla03"],"extra2":true}' \
                 -vvvv play.yml

No config file found; using defaults
Loaded callback default of type stdout, v2.0
1 plays in play.yml

PLAY ***************************************************************************

TASK [debug] *******************************************************************
task path: /Users/fred/workspaces/extraargs/play.yml:6
ok: [default] => {
    "extra1": [
        "bla01", 
        "bla02", 
        "bla03"
    ]
}

PLAY RECAP *********************************************************************
default                    : ok=1    changed=0    unreachable=0    failed=0

Actual behavior

What actually happened?

extra_args are not passed properly by vagrant to the ansible-playbook process

Steps to reproduce

  1. copy Vagrantfile from above to empty folder
  2. copy play.yml file from above into same folder
  3. run vagrant up and watch ansible provisioning step fail

@bertramn thank you very much for your test case. I really appreciate to get more people involved in the "quality assurance" process ๐Ÿ‘ ๐Ÿ˜„

From your report, I think that you are testing it with a wrong fix (maybe #6820), as your output is single quoted, not double quoted. If you run it with master codebase (where #7103 has been merged), you should get a successful ansible run, like illustrated below:

with ansible remote provisioner:

==> machine1: Running provisioner: ansible...
    machine1: Running ansible-playbook...
PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true \
ANSIBLE_HOST_KEY_CHECKING=false ANSIBLE_SSH_ARGS='-o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o ControlMaster=auto -o ControlPersist=60s' \
ansible-playbook --connection=ssh --timeout=30 --limit="machine1" \
--inventory-file=/.../.vagrant/provisioners/ansible/inventory \
--extra-vars="{\"extra1\":[\"bla01\",\"bla02\",\"bla03\"],\"extra2\":true}" \
-vvvv check.yml

PLAY [all] ******************************************************************** 

TASK: [debug var=extra1] ****************************************************** 
<127.0.0.1> ESTABLISH CONNECTION FOR USER: vagrant
ok: [machine1] => {
    "var": {
        "extra1": [
            "bla01", 
            "bla02", 
            "bla03"
        ]
    }
}

TASK: [fail msg="extra1 arg not set"] ***************************************** 
skipping: [machine1]

PLAY RECAP ******************************************************************** 
machine1                   : ok=1    changed=0    unreachable=0    failed=0  

with ansible_local provisioner:

==> machine1: Running provisioner: ansible_local...
    machine1: Running ansible-playbook...
cd /vagrant && PYTHONUNBUFFERED=1 ANSIBLE_FORCE_COLOR=true ansible-playbook  \
   --limit="machine1" --inventory-file=/tmp/vagrant-ansible/inventory  \
   --extra-vars="{\"extra1\":[\"bla01\",\"bla02\",\"bla03\"],\"extra2\":true}"  \
   -vvvv check.yml
Using /vagrant/ansible.cfg as config file
Loaded callback default of type stdout, v2.0

PLAYBOOK: check.yml ************************************************************
1 plays in check.yml

PLAY [all] *********************************************************************

TASK [debug] *******************************************************************
task path: /vagrant/check.yml:5
ok: [machine1] => {
    "extra1": [
        "bla01", 
        "bla02", 
        "bla03"
    ]
}

TASK [fail] ********************************************************************
task path: /vagrant/check.yml:7
skipping: [machine1] => {"changed": false, "skip_reason": "Conditional check failed", "skipped": true}

PLAY RECAP *********************************************************************
machine1                   : ok=1    changed=0    unreachable=0    failed=0 

I also must stress out that I am very surprised that you have produce such an error when running the ansible provisioner, since this bug only affects ansible_local (only the verbose output of the ansible provisioner is wrong and displays an invalid ansible-playbook command, but the effective command executed by Vagrant 1.8.1 under the hood is correct).

Can you please give it a try with git master ? Thanks again!

Hi @gildegoma, glad to help out. This test case was actually executed against vagrant 1.8.1 release. I did have problems with ansible extra-vars on both OSX and windows. The ansible documentation suggests to single quote --extra-vars arguments when they are passed as json arguments. I think they do some magic to work out if its a comma separated list of vars, a yaml file to load or a JSON structure.

As of Ansible 1.2, you can also pass in extra vars as quoted JSON, like so:
--extra-vars '{"pacman":"mrs","ghosts":["inky","pinky","clyde","sue"]}'

I am not that familiar with ruby and have not been able to run the latest master (will have to setup a rbenv first before breaking my system ruby install ).

However I did look through the ansible provisioner code while tracking the same problem on a windows machine and it looks like the "command" is properly constructed but the subprocess utility does mingle it up and constructs the os process incorrectly (as in stripping out characters required by ansible). Its all good until about here:
plugins/provisioners/ansible/provisioner/host.rb line 76 but then goes wrong in Vagrant::Util::Subprocess.execute(*command). I could see it during debug with a little wrapper I wrote to get vagrant and ansible to work on windows (hack) https://github.com/bertramn/ansible-win-wrapper and the exact same problem also exists on OSX (as per above test). Reading through all the other issues on --extra-args no one mentioned Vagrant::Util::Subprocess as a possible culprit which I suspect causes this failure. Hope this gives you guys some ideas.

@bertramn Thanks for the additional information. Now it is very clear that the issue you're reporting is different, and shouldn't not be discussed here.

I filed it again as #7255, where I invite you to bring more information because I am not able to reproduce the described problem.