# A simple approach to locally scoped facts; asking for feedback

**URL:** <https://forum.ansible.com/t/a-simple-approach-to-locally-scoped-facts-asking-for-feedback/10797>\
**Category:** Get Help\
**Tags:** variables\
**Created:** [November 9, 2024, 11:36am UTC](https://forum.ansible.com/t/a-simple-approach-to-locally-scoped-facts-asking-for-feedback/10797 "2024-11-09T11:36:20Z")\
**Posts on this page:** 9\
**Page:** 1

<div class="post-metadata">

**Author:** ![pieterjanv](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/pieterjanv/32/5401_2.png) [@pieterjanv](https://forum.ansible.com/u/pieterjanv)\
**Post date:** [November 9, 2024, 11:36am UTC](https://forum.ansible.com/t/a-simple-approach-to-locally-scoped-facts-asking-for-feedback/10797/1 "2024-11-09T11:36:20Z")

</div>

For the last few weeks I’ve been using Ansible quite extensively, and I realized for the first time that facts set in roles are not locally scoped. It so happens I like to name my variables as generically as the context permits, so instead of opting for prefixing all my role variables with company and role name, I tried something else.

I’ve documented a [proof of concept on GitHub](https://github.com/pieterjanv/ansible-local-facts) that aims to provide roles with a locally scoped fact named `local`, allowing one to create simply named facts without worry of them being overwritten by including roles.

I’ve been looking around for similar approaches, but did not find them, which leaves me to wonder whether I’m missing something about Ansible that makes the proposal unnecessary.

My question is basically whether the proposal is viable as a solution, or, alternatively, there are important reasons not to go down this path.

Edit: I realized that it might be very desirable to allow local scope without changing the way the role is consumed, so I added a set of roles that demonstrate this.

---

<div class="post-metadata">

**Author:** ![felixfontein](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/felixfontein/32/7530_2.png) [@felixfontein](https://forum.ansible.com/u/felixfontein)\
**Post date:** [November 9, 2024, 1:14pm UTC](https://forum.ansible.com/t/a-simple-approach-to-locally-scoped-facts-asking-for-feedback/10797/2 "2024-11-09T13:14:59Z")

</div>

This doesn’t help directly, and also has the disadvantage that the caller needs to enable it (by setting the config, or using the new `public` parameter), but it might still be interesting - in case you haven’t already seen it: [Ansible Configuration Settings — Ansible Community Documentation](https://docs.ansible.com/ansible/latest/reference_appendices/config.html#default-private-role-vars)

There’s also the Roles 2.0 proposal by @bcoca: [Roles 2.0 · Issue #191 · ansible/proposals · GitHub](https://github.com/ansible/proposals/issues/191)

---

<div class="post-metadata">

**Author:** ![pieterjanv](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/pieterjanv/32/5401_2.png) [@pieterjanv](https://forum.ansible.com/u/pieterjanv)\
**Post date:** [November 9, 2024, 1:28pm UTC](https://forum.ansible.com/t/a-simple-approach-to-locally-scoped-facts-asking-for-feedback/10797/3 "2024-11-09T13:28:04Z")

</div>

Thanks. I did notice the `DEFAULT_PRIVATE_ROLE_VARS` setting, but my test pointed out that it does nothing to intermediate values set using `set_facts`.

Is the proposal by bcoca stil alive? Either way, it does not seem to touch on facts set during execution either.

The problem I’m trying to solve concerns the scope of facts set using `set_facts` / `register`, specifically.

---

<div class="post-metadata">

**Author:** ![pieterjanv](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/pieterjanv/32/5401_2.png) [@pieterjanv](https://forum.ansible.com/u/pieterjanv)\
**Post date:** [November 10, 2024, 6:58pm UTC](https://forum.ansible.com/t/a-simple-approach-to-locally-scoped-facts-asking-for-feedback/10797/4 "2024-11-10T18:58:09Z")

</div>

I turned it into a collection so that it’s easier for people to try out; it’s available at the GitHub repo link in the original post.

It enables usage like the following:

```yaml
- name: Set some locally scoped facts
  pieterjanv.localscope.set:
    updates:
      some_key: some value

- name: Setup call to nested role
  pieterjanv.localscope.set:
    updates:
      call_args:
        name: some_one.some_collection.some_role
        tasks_from: main

# Now call the role through the `call` role
- name: Include a nested role
  ansible.builtin.include_role:
    name: pieterjanv.localscope.call

- name: Use the locally scoped fact knowing it has not been overwritten
  debug:
    var: local.some_key

```

---

<div class="post-metadata">

**Author:** ![bcoca](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/bcoca/32/9269_2.png) [@bcoca](https://forum.ansible.com/u/bcoca)\
**Post date:** [November 11, 2024, 2:24pm UTC](https://forum.ansible.com/t/a-simple-approach-to-locally-scoped-facts-asking-for-feedback/10797/5 "2024-11-11T14:24:59Z")

</div>

You have 2 main scopes in Ansible, one associated to the playbook objects, that works as you expect (mostly) and one associated to the host, set\_fact sets variables in ‘host scope’ and is independent of the playbook objects. If you want to set variables to stay local to the role use `vars:`.

The role 2.0 proposal is a wish item, not currently planned, I also have another to revamp variables themselves [redesign variable interface · Issue #127 · ansible/proposals · GitHub](https://github.com/ansible/proposals/issues/127) in the same state, which might be more appropriate to this discussion.

---

<div class="post-metadata">

**Author:** ![pieterjanv](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/pieterjanv/32/5401_2.png) [@pieterjanv](https://forum.ansible.com/u/pieterjanv)\
**Post date:** [November 13, 2024, 9:55am UTC](https://forum.ansible.com/t/a-simple-approach-to-locally-scoped-facts-asking-for-feedback/10797/6 "2024-11-13T09:55:50Z")

</div>

#### vars are visible to included modules

`vars:` take you a long way, but given they are visible to all included roles, they are not local in the sense that variables defined in a function in typical programming languages are. This is a hindrance to naming things generically, as there seems to be no way to distinguish between vars passed directly to your role, and vars by the same name that happened to be set higher up the inclusion chain, and the more generic your name, the likelier someone used it in a conflicting context.

#### `undef()` can help

I did just find out there is a cumbersome and error-prone way to work around this particular issue - the [`undef()`](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_templating_undef.html) function - that the caller can use to unset any settable vars if he wishes to not pass them. I actually found a use for it improve the api of my collection.

#### other vars limitations

I think I found a somewhat clean api to accomplish setting local facts that behave as I expect, but I don’t see a way to do this for vars:

- I see no way to automatically `undef()` all vars; I can’t iterate over them.
- Vars are evaluated lazily, so by the time they are evaluated, vars defined in terms of other vars may have values assigned to them in a conflicting context.

#### Only facts can be made local at the moment

I think the proposal you linked covers these issues, as you mention locally scoped vars and eager evaluation. I hope at least those parts can be integrated!

Until then, I see no other option for local definitions besides setting facts.

---

<div class="post-metadata">

**Author:** ![pieterjanv](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/pieterjanv/32/5401_2.png) [@pieterjanv](https://forum.ansible.com/u/pieterjanv)\
**Post date:** [November 16, 2024, 10:22pm UTC](https://forum.ansible.com/t/a-simple-approach-to-locally-scoped-facts-asking-for-feedback/10797/8 "2024-11-16T22:22:21Z")

</div>

I think I found a way to do eager evaluation of role arguments. @bcoca, I would love your thoughts on the way I went about wrapping `ansible.builtin.include_role` as it feels quite uncomfortable, but I saw no other way:

When calling [`pieterjanv.localscope.include_role`](https://github.com/pieterjanv/ansible-local-facts/blob/v1.2.0/plugins/action/include_role.py) in a play, the plugin creates a new play using the Ansible Python api that involves some tasks before and after calling `ansible.builtin.include_role`, and executes it using a new `TaskQueueManager`.

It was the only way I could find of wrapping the built-in `include_role`. I find it unnerving as I’m unsure whether nested plays break Ansible.

The api has now changed to:

```yaml
--- # Calling a role

- name: Include some role with eagerly evaled args and local facts
  pieterjanv.localscope.include_role:
    name: some_one.some_collection.some_role
    input: # all members will be available as plain variables
      some_var: some value
      eagerly_evaluated: "{{ my_var }}"
  # will default to a dictionary with only the `changed` and `failed` keys
  register: some_role_output

--- # using local facts

- name: Set local fact
  pieterjanv.localscope.set:
    updates:
      my:
        key: value
    recursive: yes

- name: Include a nested role
  pieterjanv.localscope.include_role: "{{ ... }}"

- name: Use local fact
  debug:
    var: local.my.key # unchanged

- name: Return a value
  pieterjanv.localscope.return:
    key1: value1
    key2: value2

```

---

<div class="post-metadata">

**Author:** ![bcoca](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/bcoca/32/9269_2.png) [@bcoca](https://forum.ansible.com/u/bcoca)\
**Post date:** [November 29, 2024, 5:37pm UTC](https://forum.ansible.com/t/a-simple-approach-to-locally-scoped-facts-asking-for-feedback/10797/9 "2024-11-29T17:37:27Z")

</div>

Yes, while `vars:` declared on a playbook object are scooped ‘local’ to that object, there is no isolation to vars declared more globally. This is the first time I see the request to have a role ONLY receive specific/explicit variables passed in vs it having access to all the variables and overriding specific ones for the role, though my variables proposal could do that since all variables are ‘namespaced’ at that point, a ‘current role/tree’ namespace(s) could be added.

Your custom modules might work now, but they are injecting themselves into very complex code in Ansible’s core engine, I would expect many unplanned side effects. Also the code you interface with can change at any time in very incompatible ways, it has done so many times in the past and I know at least 2-3 planned changes in the future.

---

<div class="post-metadata">

**Author:** ![pieterjanv](https://sea1.discourse-cdn.com/ansible/user_avatar/forum.ansible.com/pieterjanv/32/5401_2.png) [@pieterjanv](https://forum.ansible.com/u/pieterjanv)\
**Post date:** [November 30, 2024, 10:43am UTC](https://forum.ansible.com/t/a-simple-approach-to-locally-scoped-facts-asking-for-feedback/10797/10 "2024-11-30T10:43:45Z")

</div>

#### Regarding explicit role input

I think explicit role input reduces cognitive overhead to a minimum at the cost of having to pass everything in. I suppose it’s not unintuitive to expose non-local scope. I think most languages offer non-local scope as an option in the form of a closure.

But, supposing I have a role var and this variable is defined upon checking inside my role, I cannot know whether it was actually passed as an input, or just happens to have been set in some enclosing scope without intending it as a role var.

#### Features for more robust plugins

Indeed, I realize my module is not future proof, making use of variables that start with an underscore, which I take are not part of any public api, and using the nested play structure mentioned before.

I think the following features would allow for a lot of flexibility for plugins:

- Public access to a `Templar` object to allow plugins to eagerly evaluate vars; I used `ActionBase#_templar`, which I take is private.
- A public api for injection of task data at the point of execution. Something similar to [`Play#load()`](https://github.com/ansible/ansible/blob/devel/lib/ansible/playbook/play.py#L142) that looks up any internal dependencies from the task context would allow plugins to use the api as used in plays, which is unlikely to break unexpectedly. It could look something like:

```yaml
--- # plugins/action/some_action.py

self.insert_tasks([
  {
    'name': 'task1',
    'ansible.builtin.set_fact': {
      'my_fact': 'foo',
    },
  },
  {
    'name': 'task2',
    ...
  },
]);

```

- A public api for registering output to a fact from within a module. After some digging I found setting `Task#_register` works, but this is far from obvious from the code.
