module-returned 'ansible_facts' precedence?

All,

Perhaps i’m being boneheaded, but I can’t seem to follow the exact order of application in updating the host facts that is performed by ansible on task completion. Particularly, I see the setup cache is updated with the content of “ansible_facts”, and in subsequent tasks I do see new values, but for facts which already existed in the namespace (specified by vars options) remain unchanged (as viewed by inject in subsequent tasks). My intuition is that the setup cache is lower in precedence than the vars specified, and so when inject is assembled for a subsequent task, it first injects the setup cache copy(which is correct), then inventory vars, etc. until whatever dict() is holding that initial value gets merged in, effectively overwriting the newer value. I’ve tried configuring hash_behaviour=merge, but this doesn’t seem to have the desired effect; only appears to apply to inventory hashes.
My question is this: How best might I feed back modified values of facts/vars such that they have the highest precedence, and ensure that said values are the ones accessed by all future tasks? Because the changes i want to make happen in a host-side action_module context, I have access to the Runner instance. Other than the ansible_facts return mechanism, is there a way to modify the host’s vars through an instance of runner? I’m okay with a hack back through runner, as I wouldn’t expect the architecture of runner to change too fast to keep up with. I’d prefer not to patch runner itself to achieve this, though that is the easier way to go (i think most of the logic is actually in module).

For a little bit of context: the action module I am writing is one which pre-validates variable content, specifying defaults for various task parameters, etc. One particular aspect is the ability to specify complex structure requirements for dict()/list(dict()) (e.g. NIC configurations). At issue here is the logic in said module which “fills out” missing fields with default values for each element in a list; since the var existed in the host namespace, we’re not creating a new fact, but modifying the existing one, and this modality doesn’t seem to be supported currently.

Thanks in advance,
Chris Wolfe

“How best might I feed back modified values of facts/vars such that they have the highest precedence, and ensure that said values are the ones accessed by all future tasks?”

Sounds like you need to be setting role defaults if you are seeing values in playbooks clobber facts and you do not want that behavior.

http://docs.ansible.com/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable

Some of your post i’m having some trouble understanding which is probably a sign you are using ansible in a “too complicated playbook” kind of way and may need to simplify, it’s a general trend from someone coming from some other tools, and might be trying to do something non-idiomatically.

Keeping variables in just one place, having only one source of truth, is the easiest way, and precedence never even comes up in those situations.

But yes, there’s much about your post I don’t understand. “modality”, etc.

Thanks Michael.

I am, in fact, consciously violating the “ansible” way of doing things, however, the purpose justifies the deviation. The plan s to have a potentially widely distributed playbook for non-technical consumers. The key need is to build slightly smarter playbooks at the expense of a little development effort such that feedback to end users is cogent, and to provide a little extra filling in the blanks. i.e. keeping the top-level configuration simpler at the expense of doing more work in the playbooks. Rationale aside, the particular need is to explicitly update VARS_CACHE, as opposed to the SETUP_CACHE. The following patch to ansible/playbook/init.py achieves the purpose: (using a new return variable ‘ansible_vars’)

— ./playbook/init.py 2014-05-23 15:08:11.000000000 -0400
+++ ./playbook/init.py.varsmod 2014-07-18 10:57:55.002591480 -0400
@@ -438,9 +438,13 @@
if type(res) == dict:
facts = res.get(‘ansible_facts’, {})
self.SETUP_CACHE[host].update(facts)

  • newvars = res.get(‘ansible_vars’, {})
  • self.VARS_CACHE[host].update(newvars)
    else:
    facts = result.get(‘ansible_facts’, {})
    self.SETUP_CACHE[host].update(facts)
  • newvars = result.get(‘ansible_vars’, {})
  • self.VARS_CACHE[host].update(newvars)
    if task.register:
    if ‘stdout’ in result and ‘stdout_lines’ not in result:
    result[‘stdout_lines’] = result[‘stdout’].splitlines()
    @@ -545,6 +549,7 @@
    for (host, result) in setup_ok.iteritems():
    self.SETUP_CACHE[host].update({‘module_setup’: True})
    self.SETUP_CACHE[host].update(result.get(‘ansible_facts’, {}))
  • self.VARS_CACHE[host].update(result.get(‘ansible_vars’,{}))
    return setup_results

*****************************************************

However, I was wondering if there was a way to achieve the same effect without resorting to patching playbook, and hence retain ‘distributability’. I’d be satisfied with any hack that allows me to disribute the required logic in-situ, so I was looking for a way to get back to the VARS_CACHE from the runner instance, but i wasn’t able to trace one out.

Forking ansible seems a bit extreme here. I would urge you not to do so and try to understand how the tool and project organization works a bit more first.

Plenty of people will have more complex setups than yours, and many have very user-friendly fronted playbooks.

The delegation features in Ansible tower, and the way it allows for prompting of variables, serve this purpose for a lot of Ansible users.

I’ve mentioned role defaults previously as opposed to role vars, which already have lower priority than inventory variables. Do you understand those?

I agree, modifying ansible is unreasonable, in general. And I am very familiar with the precedence relations between role/task/setup/inventory discovered vars/facts. But, that said, I still need to be able to modify facts/vars at higher precedence than user-supplied (inventory, group-vars, etc.). It is a corner-case, to be sure, but one that i would imagine others have encountered as well.

Consider the following toy example

[group1]
host1

<group_vars/group1>

I’m not sure you do.

When you say “modify facts”, can you give an example of what a fact is in this case?

I’m trying to see if what you are suggesting might not have to be a fact, and if there are nicer ways to do this.

I think there may be some ways to achieve things using the Jinja2 default operator in … fact

myvar: {{ factvar | default(some_other_var) }}

etc

The above patch achieves my intended goals. But… I would much rather move the hacky part into a custom module to be distributed with the playbook in question. This is only possible if i can “hop out” of the current paradigm’s access pattern and access the playbook object directly, and manually manipulate the VARS_CACHE. Of course, if there exists another mechanism to modify the VARS_CACHE without as much hackery, I’m game for that, too. From what i gather, looking at the code, that’s not the case.

Let’s ignore, for the moment, why i might want to do this and whether or not it’s a reasonable thing to want to do.

My question is a technical one and fairly straightforward, though perhaps I was less direct previously:

From the context of a runner-side action_plugin module
Note: (i.e. <playbook_root_dir>/lib/ansible/runner/action_plugins/mymodule.py)
given only access to self.runner,
Note: self is an ActionModule initialized with runner from <>
is there a way to retrieve an object reference to the currently executing playbook object,
Note: OR any object which owns the playbook object
such that
I can modify the playbook object’s self.VARS_CACHE[] member

For example:
No, it’s not possible; playbook exists outside the scope of runner
Yes, it’s possible; playbook is accessible via self.runner.x.y.z.potato.playbook
or maybe something in the global scope ansible_or_something.x.y.z.runner

The phrasing of my original question was, in hindsight, less specific than it needed to be. I was really just looking looking for a short answer. Because i don’t have the UML, and only minimal appreciation of the architecture of ansible core, I’m not sure whether playbook owns runner or vice versa, or if they even exist in the same process space, etc. If there’s a UML diagram floating around, then a glance would answer my question. In lieu of a direct answer, I’ll search the code. If the answer is no, that’s fine, I just figured the gurus here would be able to answer quickly whether it’s feasible.

Regards,
Chris

Solution (more or less):

From the action module, I can modify self.runner.vars_cache When the runner is constructed in _run_task_internal(…), the vars_cache is set to self.VARS_CACHE (the latter self being the playbook object); the same runner is then supplied to the action_module’s constructor, which modules then have access to.

This persists the changes for the current play. For me, this is good enough, and I would imagine the limited lifetime of the vars_cache is intentional ( or i could at least think of a bunch of justifications to keep it that way ).

In any event, I’m satisfied with my current hack, which doesn’t require forking runner. Thanks for the the help.

"Let’s ignore, for the moment, why i might want to do this and whether or not it’s a reasonable thing to want to do. "

If you’re not going to be discussing the use case and how to solve your problem in other ways, it’s going to be hard to have a discussion about how to do it better.

You’re still modifying code, which effectively means your Ansible is producing all sorts of side effects, so I’d at least ask that you not ask any ansible questions that can’t be reproduced on stock code.

We would be happy to discuss ways to manage your variable use cases, including role defaults.

I don’t object to finding other solutions, but felt i was stumbling trying to get the point across. The use case is being able to make task-by-contract guarantees:
A) the environment of the task is configured correctly and the task will always succeed,
B) the environment is misconfigured and the task fails before doing anything,
C) the environment is mostly configured, but the task knows how to fill in the blanks)
Each of these being checked/applied a priori with respect to task execution proper. always, in this context meaning if the task does fail, it won’t have been because of configuration. Particularly, some of the tasks require complex data structures, and those data structures need to be recursively validated with the same 3 goals.

At issue is goal C. For the case of scalars, the existing precedence rules function sufficiently; either the parameter has a user-supplied value, or it takes on any role-default if specified.
However, for complex data types, this is not the case.

If X is a dict(), having fields {x=a,y=b,z=None}, specified by user
and
If Y is a template for our expectation of X, having fields x=a’,y=b’,z=c’,
then
We would like the effective value of X, as seen by the play and its tasks, to be X’ {a=X.a, b=X.b, c=Y.c’}
thereby specifying piece-wise defaults for complex data X.

My method of achieving this is a custom module ‘requirements’ which provides all 3 guarantees. It particularly addresses the corner case of C for complex data by modifying the self.runner.vars_cache[host][‘X’] (in keeping with our example). The alternative, to return {ansible_facts:{X:{x:X.a,y:X.b,z:Y.c’}}}, would only update the setup cache, and subsequent tasks would not see the returned value because the user-supplied value in the vars_cache would take precedence when combining to create the environment for subsequent tasks. Therefore, X would have the original value, and X.z would still be None when accessed.

Regarding side-effects. This largely depends on the life time of the vars_cache. For my particular purpose, the ideal lifetime for these modifications to the environment is until end of play, including any included tasks. If the vars_cache outlives the play, then there could be unintended consequences, but, so far, that doesn’t appear to be the case. My rationale here is that any play invoking the ‘requirements’ module is imposing constraints for that play, and not the playbook as a whole. Were the piece-wise-defaults to live beyond the play, there might be issues, though the key to defending against that scenario is in rational variable naming (i.e. prefixes, etc.), which i do.

It is certainly possible to reconstruct the tasks to not require complex data, but generally impractical to do so, when tasks may require lists of dozens to hundreds of items (think manually unrolling a loop of 0-100 X_0_a X_0_b, X_0_c X_1_a, X_1_b … X_99_a … ). The alternative, in placing testing and defaulting logic throughout all of the various consumers of that data is inherently a bad idea; it detracts from the cohesiveness of the design. For the occasional parameter, it’s not a big deal, but structures that are used heavily in this mode will make the playbooks inherently un-maintainable, and tied in time to their origin.

As towards simplicity and doing too much. This feature actually allows for greater design simplicity for tasks, templates, etc. without having to re-verify or re-default a variable at each invocation, the logic becomes cleaner and visually comprehensible. Not only that, but communicates expectations in cogent and sensible way. By counterexample, having a key task fail during a rolling upgrade of 1000 machines because X.z was undefined when accessed in a template is not the optimum way to communicate mis-configuration to the end user, especially when it was known before machine 0 was touched that there would be a problem. More importantly, though, we may have something sensible to do to touch up X.z before execution (rule C). The important point is that, in order to hit all three bases, we need the ability to incrementally modify X such that X is neither the user-suplied value nor the default-value, but combination of the two. Insisting that X.z is defined up-front is unnecessarily harsh when a sensible default exists, Insisting that X is either entirely user-supplied or entirely default-supplied is unnecessarily inflexible. The middle ground lies squarely in the corner case of C.

I could also imagine a number of other scenarios where a task may want to override the user(inventory, etc.)-supplied values. Here is one:
In a large organization, with administrative and non-administrative users use ansible playbooks to accomplish certain tasks. The playbooks, designed by the administrator, includes some auditing tasks in certain plays. These tasks, for whatever reason, depend on parameters that the administrator does not want to allow the user to modify (credentials, logtime, etc.). Said user modifies the them anyway, inducing error into the audit trails. Admittedly, this example is contrived, and there are better ways to avoid such issues, but it nonetheless illustrates a viable scenario in which it may be desirable to have the ability to mandate the content of the effective environment for subsequent tasks.(i.e. don’t allow user J to modify “audit_username”)

So the question is not whether a particular case can be shoehorned into an ansible-idiomatic way of doing things, because that is almost always the case, modulo some hoop-jumping. The question is “how can we expand the ansible-idiomatic” way of doing things to reduce the hoop jumping. If the answer is six lines of code, then I find it hard to argue against the position (of course, there may be side effects and other concerns, but someone familiar with the code base could illuminate those).

In any event, sorry for the wall of text, and I really do appreciate your help.

Regards,
Chris

I have a bit of a problem with parsing text ambiguity, I’m a bit of a text learner.

So let me say this, and I’ll try not to be offensive, though it may sounds as such.

I see you have a vt.edu address. Great school.

We’ve got users on this list from Fortune 500 companies all over the place. They aren’t making their infrastructure that complicated. Many of them are making it self service.

I really feel you are overthinking this.

When I mean use case, I mean, things like “tell me what apps you are configuring and what some of the user inputs might be”.

And we can make suggestions from there.

But let’s step back and not talk code or theory for a moment, and pretend I’m a useless software executive. (Oh, wait…)

But yeah, it can’t be as hard as you are making it.

“Task by task contract guarantees” seems very abstract, almost like some PhD got struck my lightening and then had 10 cups of coffee.

There are several parts in the above though where I just couldn’t parse what you’re talking about, though, so I’m sorry this may just be super difficult to get on the same wavelength. This is one of the hardest posts for me to read all year.

I have a bit of a problem with parsing text ambiguity, I'm a bit of a text
learner.

typo: this should have been /visual/.

Arguably a few too many cups of coffee. :slight_smile: