Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integers passed around using jinja variable references are converted to strings #9362

Closed
feanil opened this issue Oct 16, 2014 · 29 comments
Closed
Labels
bug This issue/PR relates to a bug.

Comments

@feanil
Copy link
Contributor

feanil commented Oct 16, 2014

Issue Type: Bug Report
Ansible Version: 1.6.9
Environment: Ubuntu 12.04
Summary:

In Ansible 1.6.9 a change was made that changed how the behavior of 'to_nice_json'. Previously numbers being passed around as jinja variable references would be converted to numbers in the json output but now the numbers are returned as quoted.

Steps To Reproduce:

test.yml

- hosts: all
  gather_facts: False
  vars_files:
    - test_vars.yml
  tasks:
    - debug: msg="{{ Y }}"
    - debug: msg="{{ Y | to_json }}"

test_vars.yml

X: 4
Y:
  Z: "{{ X }}"

Command: ansible-playbook test.yml -i localhost, -c local -vvv

Expected Results:

Output from 1.6.8


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

TASK: [debug msg="{'Z': u'4'}"] *********************************************** 
ok: [localhost] => {
    "msg": "{'Z': u'4'}"
}

TASK: [debug msg="{"Z": "4"}"] ************************************************ 
ok: [localhost] => {
    "msg": "{Z: 4}"
}

PLAY RECAP ******************************************************************** 
localhost                  : ok=2    changed=0    unreachable=0    failed=0   

Actual Results:

Output from 1.6.9


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

TASK: [debug msg="{'Z': u'4'}"] *********************************************** 
ok: [localhost] => {
    "msg": "{'Z': u'4'}"
}

TASK: [debug msg="{"Z": "4"}"] ************************************************ 
ok: [localhost] => {
    "msg": "{\"Z\": \"4\"}"
}

PLAY RECAP ******************************************************************** 
localhost                  : ok=2    changed=0    unreachable=0    failed=0   

feanil added a commit to openedx/configuration that referenced this issue Oct 16, 2014
@jimi-c jimi-c added P3 labels Oct 20, 2014
feanil added a commit to openedx/configuration that referenced this issue Oct 20, 2014
@feanil
Copy link
Contributor Author

feanil commented Oct 22, 2014

The same is true for Booleans I discovered and that is broken in 1.6.0 I would love to see this get a higher priority since it is preventing us from upgrading past 1.5.5.

@feanil
Copy link
Contributor Author

feanil commented Nov 3, 2014

Ok, thanks, in the case of your suggestion for the change above, it doesn't work because Y is actually a complex data structure which has an attribute that is an integer.

@jimi-c
Copy link
Member

jimi-c commented Nov 3, 2014

@feanil you can still do that: somevar.whatever.should_be_an_integer|int

@feanil
Copy link
Contributor Author

feanil commented Nov 4, 2014

Yes but the whole point of searializing the data structure as is that we don't have template out all of its content. We can just add new vars to defaults and start using them during the ansible run and for the data that the application also needs, we serialize that to disk for it to reference.

@memelet
Copy link

memelet commented Jun 1, 2015

So this is it? We can't create complex data (eg, marathon deploy) that contains non-string values? No workaround? This is pretty limiting.

@memelet
Copy link

memelet commented Jun 1, 2015

Strange, it only applies to variables

  marathon_app:
    id: /svc/auth
    instances: 2

Yields an int value for instances after to_json.

  marathon_app:
    id: /svc/auth
    instances: "{{vi_instances | int}}"

But here instances is a string after to_json.

@memelet
Copy link

memelet commented Jun 1, 2015

So this means for something line

- role: marathon_app
  marathon_app:
    id: /svc/auth
    cpus: 0.5
    mem: 64.0
    instances: "{{vi_instances | int}}"
    uris:
      - "{{dockercfg_uri}}"
    container:
      type: DOCKER
      docker:
        image: "systeminsights/ms-security-subjects:{{vi_version}}"
        network: BRIDGE
        dns: "{{consul_dns_ip}}"
        portMappings:
          - {containerPort: 3000, hostPort: 0}
        volumes:
          - {containerPath: /var/log/vimana, hostPath: /var/log/vimana}

Every value that needs to be an int needs to be hard coded.

@sdwr98
Copy link

sdwr98 commented Jun 1, 2015

@memelet We ran into the exact same issue. I worked around it by breaking out my JSON information into a separate file and using the template command + file lookup to create JSON request bodies. That mechanism allowed us to use variables for those integer/numeric values.

I wrote about it more detail here: https://sdwr98.wordpress.com/2015/06/01/using-ansible-for-marathonchronos-deployments/

@memelet
Copy link

memelet commented Jun 2, 2015

@sdwr98 Thanks Scott. Nice workaround. Did you try using lookup('template') and do away with the temporary file?

@sdwr98
Copy link

sdwr98 commented Jun 2, 2015

@memelet Cool! I didn't even see the template lookup. That makes it even simpler.

@feanil
Copy link
Contributor Author

feanil commented Jun 2, 2015

A fix for this was merged in to devel last month. It should be available in 2.0

#10465

There is a related PR for dealing with passing around nulls which has not been reviewed or merged yet.

#10957

@feanil feanil closed this as completed Jun 2, 2015
@vcastellm
Copy link
Contributor

This is broken for me in 2.1.0.0

@simplesteph
Copy link
Contributor

Any news on this or workarounds?

@shaharmor
Copy link

Still broken

@pablobm
Copy link

pablobm commented Dec 2, 2016

As I understand it, the mentioned PR (#10465) doesn't solve the problem at the root, but only one of its manifestations. It detects the case of a jinja2 template that only contains a single variable name, but this is not enough when the code is more complex and expected to evaluate as a non-string.

For those of you still having problems with this, I think it's the following you may be seeing: #18722

@alikins
Copy link
Contributor

alikins commented Jan 5, 2017

(Mentioning #17992 here to keep track of related issues)

@pzhuk
Copy link

pzhuk commented Jan 24, 2017

still broken for me in 2.2.0.0

@alikins
Copy link
Contributor

alikins commented Jan 24, 2017

@simplesteph @shaharmor @pzhuk

Post the playbooks that fail and the verbose output from the failed ansible-playbook invocation.
Aside from 'debug:' output, I'm curious what use case this causes failures on.

@pzhuk
Copy link

pzhuk commented Jan 24, 2017

@alikins , my usecases are all about making a config dictionary in yaml, and than deploy them as json. So I found that some int vars are getting exposed in json with quotes like strings...

Test1 (typecasting string to int during variable exposure)

Playbook:

  vars:
    foo: '123'
    bar: 123
    config:
      foo: "{{ foo | int }}"
      bar: "{{ bar }}"

  tasks:
    - debug: msg="{{ config | to_json }}"

Result (variable foo, which was typecasted as int, still was exposed quoted like string. While actual int var called bar - exposed properly):

TASK [debug] *******************************************************************
ok: [localhost] => {
    "msg": "{\"foo\": \"123\", \"bar\": 123}"
}

Test2 (variables which are pointing to int variables became strings!)

Playbook:

  vars:
    foo: 123
    bar: "{{ foo }}"
    config:
      bar_raw: "{{ bar }}"
      bar_int: "{{ bar|int }}"

  tasks:
    - debug: msg="{{ config | to_json }}"

Result (no way to expose bar variable as int, even though it's just pointing to a int var):

TASK [debug] *******************************************************************
ok: [localhost] => {
    "msg": "{\"bar_int\": \"123\", \"bar_raw\": \"123\"}"
}

@ryanwalls
Copy link
Contributor

ryanwalls commented Feb 2, 2017

I would vote for reopening this until it is fixed, or a workaround is presented.

Update: Best workaround is from @sdwr98 above. See #9362 (comment)

@opennomad
Copy link

Just to embarrass myself I found that for my use case i can just brute force it with a regex_replace:

{{ my_var | to_json | regex_replace('"([0-9]+)"','\1') }}

This appears to be working sufficiently for me when passing the output to other JSON aware tools that check on the type of things (awscli).

@alikins
Copy link
Contributor

alikins commented Feb 16, 2017

Some examples showing current behavior and what is desired (some of which currently doesnt work)

---
- hosts: localhost
  gather_facts: false
  vars:
    foo_string: '123'
    bar_int: 123
    config:
      foo2: "{{ foo_string | int }}"
      bar2: "{{ bar_int }}"

    foo_int: 123
    bar_string: "{{ foo_int }}"
    config2:
      bar_raw: "{{ bar_string }}"
      bar_int: "{{ bar_string|int }}"


  tasks:
    - name: debug foo_string type and value
      debug:
        msg: "foo_string value={{foo_string}} type={{foo_string | type_debug}}"

    - name: debug foo_string pipe int type and value
      debug:
        msg: "foo_string|int value={{foo_string|int}} type={{ foo_string| int| type_debug }}"

    - name: debug bar_int type and value
      debug:
        msg: "bar_int value={{bar_int }} type={{ bar_int|type_debug}}"

    - name: debug config.foo2 type and value
      debug:
        msg: "config.foo2 value={{config.foo2}} type={{config.foo2 | type_debug}}"

    - name: debug config.foo2 pipe int type and value
      debug:
        msg: "config.foo2|int value={{ config.foo2 | int }} type={{ config.foo2 | int | type_debug}}"

    - name: debug config.bar2 type and value
      debug:
        msg: "config.bar2 value={{ config.bar2 }} type={{ config.bar2 | type_debug }}"

    - name: config debug var
      debug:
        var: config

    - name: debug config to json
      debug:
        msg: "{{ config | to_nice_json }}"

    - name: config2 debug var
      debug:
        var: config2

    - name: debug config2 to json
      debug:
        msg: "{{ config2 | to_nice_json }}"

    - name: debug config2 to json and from json
      debug:
        msg: "{{ config2 | to_nice_json | from_json}}"

    - name: assert config.bar2 is an int 123 and not a string "123"
      assert:
        that:
            - config.bar2 == 123
      ignore_errors: true

    - name: assert config.bar2 is not a string "123"
      assert:
        that:
            - config.bar2 != "123"

    - name: assert config.foo2 is an int 123 and not a string "123"
      assert:
        that:
            - config.foo2 == 123
      ignore_errors: true

    - name: assert config.foo2 is not a string "123"
      assert:
        that:
            - config.foo2 != "123"
      ignore_errors: true

    - name: odd assert config.foo2 pipe int is an int 123 and not a string "123"
      assert:
        that:
            - '{{ config.foo2 | int }} == 123'
            - '{{ config.foo2 | int }} != 134323'
            - '"123" != {{ config.foo2 | int}}'
            - '"{{config.foo2 | int | type_debug}}" == "int"'

    - name: odd assert config2.bar_int is an int 123 and not a string "123"
      assert:
        that:
            - '{{ config2.bar_int| int }} == 123'
            - '"123" != {{ config2.bar_int | int}}'

    - name: assert config2.bar_int is an int 123 and not a string "123"
      assert:
        that:
            - config2.bar_int == 123
      ignore_errors: true

    - name: assert config2.bar_int is not a string "123"
      assert:
        that:
            - config2.bar_int != "123"
      ignore_errors: true

    - name: assert config2.bar_raw ais an int 123 and not a string "123"
      assert:
        that:
            - config2.bar_raw == 123
      ignore_errors: true

    - name: assert config2.bar_raw is not a string "123"
      assert:
        that:
            - config2.bar_raw != "123"
      ignore_errors: true

current output

[fedora-25:ansible (handle_future_builtins % u=)]$ ansible-playbook -vvv repro/jinja2_ints_as_strings_9362/test.yml
Using /home/adrian/.ansible.cfg as config file
 [WARNING]: provided hosts list is empty, only localhost is available


PLAYBOOK: test.yml *******************************************************************************************************************************************************************************************************************************************
1 plays in repro/jinja2_ints_as_strings_9362/test.yml

PLAY [localhost] *********************************************************************************************************************************************************************************************************************************************
META: ran handlers

TASK [debug foo_string type and value] ***********************************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:19
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "msg": "foo_string value=123 type=AnsibleUnicode"
}

TASK [debug foo_string pipe int type and value] **************************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:23
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "msg": "foo_string|int value=123 type=int"
}

TASK [debug bar_int type and value] **************************************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:27
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "msg": "bar_int value=123 type=int"
}

TASK [debug config.foo2 type and value] **********************************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:31
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "msg": "config.foo2 value=123 type=unicode"
}

TASK [debug config.foo2 pipe int type and value] *************************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:35
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "msg": "config.foo2|int value=123 type=int"
}

TASK [debug config.bar2 type and value] **********************************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:39
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "msg": "config.bar2 value=123 type=int"
}

TASK [config debug var] **************************************************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:43
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "config": {
        "bar2": 123, 
        "foo2": "123"
    }
}

TASK [debug config to json] **********************************************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:47
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "msg": "{\n    \"bar2\": 123, \n    \"foo2\": \"123\"\n}"
}

TASK [config2 debug var] *************************************************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:51
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "config2": {
        "bar_int": "123", 
        "bar_raw": "123"
    }
}

TASK [debug config2 to json] *********************************************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:55
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "msg": "{\n    \"bar_int\": \"123\", \n    \"bar_raw\": \"123\"\n}"
}

TASK [debug config2 to json and from json] *******************************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:59
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "msg": {
        "bar_int": "123", 
        "bar_raw": "123"
    }
}

TASK [assert config.bar2 is an int 123 and not a string "123"] ***********************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:63
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "msg": "All assertions passed"
}

TASK [assert config.bar2 is not a string "123"] **************************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:69
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "msg": "All assertions passed"
}

TASK [assert config.foo2 is an int 123 and not a string "123"] ***********************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:74
fatal: [localhost]: FAILED! => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "assertion": "config.foo2 == 123", 
    "changed": false, 
    "evaluated_to": false, 
    "failed": true
}
...ignoring

TASK [assert config.foo2 is not a string "123"] **************************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:80
fatal: [localhost]: FAILED! => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "assertion": "config.foo2 != \"123\"", 
    "changed": false, 
    "evaluated_to": false, 
    "failed": true
}
...ignoring

TASK [odd assert config.foo2 pipe int is an int 123 and not a string "123"] **********************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:86
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "msg": "All assertions passed"
}

TASK [odd assert config2.bar_int is an int 123 and not a string "123"] ***************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:94
ok: [localhost] => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "changed": false, 
    "msg": "All assertions passed"
}

TASK [assert config2.bar_int is an int 123 and not a string "123"] *******************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:100
fatal: [localhost]: FAILED! => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "assertion": "config2.bar_int == 123", 
    "changed": false, 
    "evaluated_to": false, 
    "failed": true
}
...ignoring

TASK [assert config2.bar_int is not a string "123"] **********************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:106
fatal: [localhost]: FAILED! => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "assertion": "config2.bar_int != \"123\"", 
    "changed": false, 
    "evaluated_to": false, 
    "failed": true
}
...ignoring

TASK [assert config2.bar_raw ais an int 123 and not a string "123"] ******************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:112
fatal: [localhost]: FAILED! => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "assertion": "config2.bar_raw == 123", 
    "changed": false, 
    "evaluated_to": false, 
    "failed": true
}
...ignoring

TASK [assert config2.bar_raw is not a string "123"] **********************************************************************************************************************************************************************************************************
task path: /home/adrian/src/ansible/repro/jinja2_ints_as_strings_9362/test.yml:118
fatal: [localhost]: FAILED! => {
    "_ansible_no_log": false, 
    "_ansible_verbose_always": true, 
    "assertion": "config2.bar_raw != \"123\"", 
    "changed": false, 
    "evaluated_to": false, 
    "failed": true
}
...ignoring
META: ran handlers
META: ran handlers

PLAY RECAP ***************************************************************************************************************************************************************************************************************************************************
localhost                  : ok=21   changed=0    unreachable=0    failed=0   

@micahlmartin
Copy link

micahlmartin commented Mar 13, 2017

This is still broken in 2.2.1.0:

ips: ['127.0.0.1']
options:
  server_count: "{{ ips | length }}"

And the in a template file:

{{ options |  to_nice_json }}

Results in:

{
    server_count: "1"
}

But I need it to be:

{
    server_count: 1
}

Also noticed that booleans work just fine whatever reason.

@xkrt
Copy link

xkrt commented Apr 6, 2017

Still broken in 2.3.0rc3, why not reopen this issue?

@memelet
Copy link

memelet commented Apr 27, 2017

Yes ansible team, are you declaring that this is really fixed?

@willthames
Copy link
Contributor

Still happening in released 2.3.0 and in latest devel branch

@abadger
Copy link
Contributor

abadger commented May 18, 2017

Unfortunately, jinja2 is a templating language and it always returns strings at the end of its run so there isn't much that we can do about it in Ansible code. feanil's work allowed us to turn the strings back into their given types in certain restricted circumstances where we knew what the type was supposed to be but it cannot handle more complex statements. For instance, with a filter involved, Ansible would know something like this:

- ping:
    msg: "{{ 9 | add_one }}"

Ansible knows that we start with an integer (9) but it does not know the return value of the add_one filter. The jinja2 templating engine which handles the substitution of the variables internally knows the results of add_one but it transforms the result into a string before it passes it back to us so the information is lost before we get it.

One of our developers has worked on an enhancement to jinja2 that would allow it to return python values instead of strings so that we had enough information but it is currently being evaluated by upstream. It's hard to say, at this point, whether upstream will accept the change or not. They've been very nice to us in looking over the change but it falls outside of jinja2's primary use case. As a templating language, it's bread and butter is to turn templates with values into text; returning python types is an additional feature that's not directly applicable to this core pattern. We're waiting to see what the Jinja2 upstream devs decide as the information Ansible code would need to output the user's expected type in this case is simply lacking without more information from jinja2.

@abadger
Copy link
Contributor

abadger commented May 18, 2017

I'm going to lock this issue. The reasons are:

  • The original issue was fixed by feanil's PR. The new issues here are either new bugs (If simple values without use of filters, etc are being transformed) or an additional feature (if the issue is with complex variables, variables going through filters, etc).
  • This is something we need to have fixed in jinja2's code, not in ansible's code.
  • Closed issues are not part of our normal workflow so comments and questions here unfortunately won't be seen or responded to (willthames thankfully mentioned this one to me on IRC so I knew to take a look). If you'd like to follow a ticket to see what we have in the works, you can look at this ticket: [WIP] force jinja to preserve native types  #23943 It's a sample implementation of the code that was eventually sent to the upstream jinja project. As I said there's no guarantee that this will go upstream or what we'll decide to do if it is rejected there. We'll have to re-evaluate the complexity of maintaining it once we see what upstream decides.

@ansible ansible locked and limited conversation to collaborators May 18, 2017
@dagwieers
Copy link
Member

There is a light at the end of the tunnel. We made a change to Jinja2 so we don't see all variable types changed into strings. See: pallets/jinja#708

@ansibot ansibot added bug This issue/PR relates to a bug. and removed bug_report labels Mar 6, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug This issue/PR relates to a bug.
Projects
None yet
Development

No branches or pull requests