AnsibleUnsafeText, use, and linting

We have a particular cron job that we run once a day on each host during business hours. Any particular host uses the same time every day, but across all the hosts those jobs are scheduled fairly evenly throughout the day. We accomplish this by picking the hour and minute for each host with the random() function, seeding each call to random() with a value specific and unchanging for each host.

   hour: "{{ 19 | random(seed=ansible_machine_id, start=7) }}" # hours between 7 and 18
   minute: '{{ 59 | random(seed=ansible_hostname) }}'  # minutes between 0 and 59

This has worked for years. (And the jobs are much more evenly spread than expected!)

Recently, ansible-lint has raised objections to this practice, complaining specifically that both ansible_machine_id and ansible_hostname are of type AnsibleUnsafeText, whereas the he only supported seed types are: None, int, float, str, bytes, and bytearray.

It’s trivially easy to work around. For example, this:

- name: Test some AnsibleUnsafeText re-typing methods
  ansible.builtin.debug:
    msg:
      - "(ansible_machine_id) is a {{ ansible_machine_id | type_debug }} : {{ ansible_machine_id }}"
      - "(ansible_machine_id | string) is a {{ ansible_machine_id | string | type_debug }} : {{ ansible_machine_id | string }}"
      - "(ansible_machine_id ~ '') is a {{ (ansible_machine_id ~ '') | type_debug }} : {{ (ansible_machine_id ~ '') }}"
      - "(ansible_machine_id | regex_replace('a', 'a')) is a {{ ansible_machine_id | regex_replace('a', 'a') | type_debug }} : {{ ansible_machine_id | regex_replace('a', 'a') }}"

produces

- '(ansible_machine_id) is a AnsibleUnsafeText : 245ebab998344152848322cf4c07ba6e'
- '(ansible_machine_id | string) is a NativeJinjaText : 245ebab998344152848322cf4c07ba6e'
- '(ansible_machine_id ~ '''') is a str : 245ebab998344152848322cf4c07ba6e'
- '(ansible_machine_id | regex_replace(''a'', ''a'')) is a str : 245ebab998344152848322cf4c07ba6e'

and ansible-lint seems happy with these techniques.

My question is: Are any of these methods the Right Way™ to deal with AnsibleUnsafeText? This feels highly reminiscent of Perl’s “tainted” values and the similar workarounds.

Is the point of AnsibleUnsafeText simply to make the user aware of possible issues? If so, it seems like it should take an explicit “unsafe_ok” filter to use such values. As it is, you can “unsafe_var_a ~ unsafe_var_b” and get a str. Lots of normal expressions can accidentally employ AnsibleUnsafeText with the user being none the wiser. Which leaves one wondering: What’s really the point?

2 Likes

None of those are necessary. AnsibleUnsafeText is a subclass of str, and that’s an acceptable type for the seed.

More information on your environment and what exact output you got would be helpful, because I cannot reproduce your result (I tried with a couple of different versions of Python, Ansible, and ansible-lint.)

I can get this error from Python if I call random.Random with an unacceptable type, but AnsibleUnsafeText is fine.

#!/usr/bin/env python3

from random import Random
from ansible.utils.unsafe_proxy import AnsibleUnsafeText

us = AnsibleUnsafeText('myseed')
r = Random(us)

r = Random(['notaseed'])
Traceback (most recent call last):
  File "/home/ezekielh/testing/./test.py", line 9, in <module>
    r = Random(['notaseed'])
        ^^^^^^^^^^^^^^^^^^^^
  File "/home/ezekielh/.pyenv/versions/3.11.2/lib/python3.11/random.py", line 125, in __init__
    self.seed(x)
  File "/home/ezekielh/.pyenv/versions/3.11.2/lib/python3.11/random.py", line 160, in seed
    raise TypeError('The only supported seed types are: None,\n'
TypeError: The only supported seed types are: None,
int, float, str, bytes, and bytearray.

AnsibleUnsafeText is an internal implementation detail of the feature that prevents Ansible from templating values that should not be templated; these can include strings that the user specifically marked as unsafe, the values returned by code that runs on the target (like facts), values from lookups, and the results of templating things that included unsafe strings. Users mostly do not have to care about this (and the implementation will change Real Soon Now™ anyway.)

- hosts: localhost
  vars:
    foo: !unsafe this is an {{ unsafe }} value
  tasks:
    - debug:
        msg:
          - "{{ foo | type_debug }}"
          - "{{ (foo ~ ' thing') | type_debug }}"

    - set_fact:
        bar: "{{ foo ~ ' thing' }}"

    - debug:
        msg:
          - "{{ bar }}"
          - "{{ bar | type_debug }}"
          - "{{ [0, 1, 2, 3] | random(seed=bar) }}"
TASK [debug] *******************************************************************
ok: [localhost] =>
    msg:
    - AnsibleUnsafeText
    - str

TASK [set_fact] ****************************************************************
ok: [localhost]

TASK [debug] *******************************************************************
ok: [localhost] =>
    msg:
    - this is an {{ unsafe }} value thing
    - AnsibleUnsafeText
    - '1'
3 Likes

Thanks for putting in the effort, @flowerysong. I always value your insights.

I swing between “change nothing ever again” and “change everything.” You’ve piqued my curiosity.
“I’m intrigued by your ideas and would like to subscript to your newsletter.” — Very early Internet meme, from USENET days.


To clarify: the code in question has run for years, and still does. It’s the linting where the problem shows up. Here’s a complete but minimal, trivial example. The random seed variable is a host fact, and as such is of type AnsibleUnsafeText. Behold:

---
# random-seed.yml
- name: Testing random seeds
  hosts: localhost
  gather_facts: true
  tasks:
    - name: Test some random seeds
      ansible.builtin.debug:
        msg:
          - "{{ runs_and_lints }}"
          - "{{ runs_but_does_not_lint }}"
      vars:
        runs_and_lints: "{{ 59 | random(seed=ansible_machine_id ~ '') }}"
        runs_but_does_not_lint: "{{ 59 | random(seed=ansible_machine_id) }}"

I get identical results from the two versions of ansible-lint most handy for me:

$ ansible-lint --version
ansible-lint 24.2.0 using ansible-core:2.16.8 ansible-compat:4.1.11 ruamel-yaml:0.18.5 ruamel-yaml-clib:0.2.7
---
$ ansible-lint --version
ansible-lint 24.7.0 using ansible-core:2.16.6 ansible-compat:24.5.1 ruamel-yaml:0.18.6 ruamel-yaml-clib:0.2.8

And that ansible-lint output is:

WARNING  Skipped installing old role dependencies due to running in offline mode.
WARNING  Listing 1 violation(s) that are fatal
jinja[invalid]: An unhandled exception occurred while templating '{{ 59 | random(seed=ansible_machine_id) }}'. Error was a <class 'ansible.errors.AnsibleError'>, original message: Unexpected templating type error occurred on ({{ 59 | random(seed=ansible_machine_id) }}): The only supported seed types are: None,
int, float, str, bytes, and bytearray.. The only supported seed types are: None,
int, float, str, bytes, and bytearray.
random-seed.yml:9 Task/Handler: Test some random seeds

(Note: that message-within-message is real; that’s not a copy-n-paste error. Nor is it of concern for this discussion.)

1 Like

Yeah, that’s pretty clearly an ansible-lint bug. This isn’t actually related to AnsibleUnsafeText, it’s that the variable isn’t defined when ansible-lint attempts to evaluate the template. That means that the filter gets… something (probably an instance of AnsibleUndefined) which is not an allowed type.

jinja[invalid]: An unhandled exception occurred while templating '{{ 59 | random(seed=sssssssssssss) }}'. Error was a <class 'ansible.errors.AnsibleError'>, original message: Unexpected templating type error occurred on ({{ 59 | random(seed=sssssssssssss) }}): The only supported seed types are: None,
int, float, str, bytes, and bytearray.. The only supported seed types are: None,
int, float, str, bytes, and bytearray.
3 Likes

To my understanding (with the caveat that I’m just a curious bystander) one of the things that will happen as part of the highly anticipated release of data tagging is that the model around “unsafe” data will be inverted, so that only data which is tagged as safe will be eligible for templating. Even before that decision, though, AnsibleUnsafeText was going to be replaced with the generic object proxies for tagged data. Users mostly shouldn’t have to care about this because it will once again be an internal implementation detail, but it should address some long-standing issues and edge cases.

Personally I think this feature is going to slip again and be pushed back to 2.19, but it is closer to becoming a reality than it’s been at any point in the past.

2 Likes

I just want to say I’ve been following this thread, and want to second this notion.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.