{"id":7540,"date":"2023-10-04T15:33:00","date_gmt":"2023-10-04T21:33:00","guid":{"rendered":"https:\/\/gregsowell.com\/?p=7540"},"modified":"2023-10-05T11:43:14","modified_gmt":"2023-10-05T17:43:14","slug":"passing-ansible-variables-in-workflows-using-set_stats","status":"publish","type":"post","link":"https:\/\/gregsowell.com\/?p=7540","title":{"rendered":"Passing Ansible Variables In Workflows Using set_stats"},"content":{"rendered":"<p><a href=\"https:\/\/gregsowell.com\/wp-content\/uploads\/2023\/10\/thumbnail-template.jpg\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/gregsowell.com\/wp-content\/uploads\/2023\/10\/thumbnail-template-300x169.jpg\" alt=\"\" width=\"300\" height=\"169\" class=\"aligncenter size-medium wp-image-7541\" srcset=\"https:\/\/gregsowell.com\/wp-content\/uploads\/2023\/10\/thumbnail-template-300x169.jpg 300w, https:\/\/gregsowell.com\/wp-content\/uploads\/2023\/10\/thumbnail-template-768x432.jpg 768w, https:\/\/gregsowell.com\/wp-content\/uploads\/2023\/10\/thumbnail-template-1024x576.jpg 1024w, https:\/\/gregsowell.com\/wp-content\/uploads\/2023\/10\/thumbnail-template.jpg 1280w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><\/p>\n<p>Workflows allow AAP, AWX, and Ascender to visually connect playbooks together.  This works great\u2026until you try and pass variables from one job template(playbook and required parts in Ascender) to another.  That\u2019s where the <a href=\"https:\/\/docs.ansible.com\/ansible\/latest\/collections\/ansible\/builtin\/set_stats_module.html\">set_stats module <\/a>comes in.  It has the ability to pass between workflow jobs, but the documentation doesn\u2019t fully explain how it works, and some much needed functionality doesn\u2019t exist\u2026so this blog post will show you how to work around these limitations!<\/p>\n<h2>Video<\/h2>\n<p><iframe loading=\"lazy\" width=\"560\" height=\"315\" src=\"https:\/\/www.youtube.com\/embed\/VC8X-juxoLQ?si=udCsNoaAHxR07_JG\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" allowfullscreen><\/iframe><\/p>\n<h2>set_fact Vs set_stats<\/h2>\n<h1>set_fact<\/h1>\n<p>If you are to the point in your automation journey where you are playing with workflows I\u2019m going to go out on a limb and say you\u2019ve already used the <a href=\"https:\/\/docs.ansible.com\/ansible\/latest\/collections\/ansible\/builtin\/set_fact_module.html\">set_fact module<\/a>.  In short, it allows you to create and\/or set the contents of a variable.  Here\u2019s an example:<\/p>\n<pre class=\"gs-code\"><code class=\"language-plaintext\">- name: What am I having for lunch?\r\n  ansible.builtin.set_fact: \r\n    lunch: tacos<\/code><\/pre>\n<p>This works a treat <strong>unless <\/strong>you want to pass this variable to another job template in a workflow\u2026these variables are scoped to the individual job template only, but there is a workaround. <\/p>\n<h1>set_stats<\/h1>\n<p>The <a href=\"https:\/\/docs.ansible.com\/ansible\/latest\/collections\/ansible\/builtin\/set_stats_module.html\">set_stats module<\/a> looks pretty similar to set_fact\u2026so for example:<\/p>\n<pre class=\"gs-code\"><code class=\"language-plaintext\">- name: What am I having for lunch?\r\n  ansible.builtin.set_stats: \r\n    data:\r\n      lunch: tacos<\/code><\/pre>\n<p>The only real difference I see here beyond a different module name, is an option called \u201cdata\u201d and then my new variables.  So it should feel fairly familiar.  One of its key features is that set_stats variables(depending on how I create them) <strong>can be passed<\/strong> to another job template in a workflow!  Its operation, however, is quite different.<\/p>\n<h1>Functional Differences<\/h1>\n<p>By default set_facts will instantiate a &#8220;lunch&#8221; variable(lunch as in the variable name from the above example) for each host, and each host can have different contents of the &#8220;lunch&#8221; variable.<\/p>\n<p>As for set_stats, by default it <strong>does not do per host and the information is aggregated<\/strong>.  This means if I have 10 hosts, there is only 1 set_stats, and when I set new information in the variable it will just start smashing it all together.<\/p>\n<h2>Playbooks And Examples<\/h2>\n<p>You can find my playbooks here in my <a href=\"https:\/\/github.com\/gregsowell\/ansible-misc\/tree\/main\">ansbile-misc repository<\/a>.<\/p>\n<p>First is the playbook doing collection(<em>set-stats-test.yml<\/em>) and I\u2019ve got a second playbook that displays all of the information(<em>set-stats-test2.yml<\/em>).  They are in two playbooks so that I can build a workflow.  I\u2019m only going to cover the collection playbook, though.<\/p>\n<p>I\u2019ll show one or two tasks and then results from the second playbook to give you an idea of what each does.<\/p>\n<h1>Per Host Stats<\/h1>\n<pre class=\"gs-code\"><code class=\"language-plaintext\">  - name: per_host true and aggregate true\r\n    ansible.builtin.set_stats:\r\n      data:\r\n        dist_ver_per_host_true_agg_true: &quot;{{ ansible_facts.distribution_version }}&quot;\r\n      per_host: true\r\n\r\n  - name: per_host true and aggregate false\r\n    ansible.builtin.set_stats:\r\n      data:\r\n        dist_ver_per_host_true_agg_false: &quot;{{ ansible_facts.distribution_version }}&quot;\r\n      per_host: true\r\n      aggregate: false<\/code><\/pre>\n<p>These two tasks both set the per_host option to true, but one has aggregation enabled.  The results in the second job template for these variables are:<\/p>\n<pre class=\"gs-code\"><code class=\"language-plaintext\">    &quot;dist_ver_per_host_true_agg_true = not set&quot;,\r\n    &quot;dist_ver_per_host_true_agg_false = not set&quot;,<\/code><\/pre>\n<p>As you can see, neither option actually made it to the second job template\u2026odd, huh?  This is because <strong>workflows don\u2019t pass per host stats<\/strong>.  The stats info is passed as extra vars to the rest of the playbooks, which is why it won\u2019t do per host information.<\/p>\n<h1>Not Per Host<\/h1>\n<pre class=\"gs-code\"><code class=\"language-plaintext\">  - name: per_host false and aggregate true- default behavior\r\n    ansible.builtin.set_stats:\r\n      data:\r\n        dist_ver_per_host_false_agg_true: &quot;{{ ansible_facts.distribution_version }}&quot;\r\n      per_host: false\r\n\r\n  - name: per_host false and aggregate false\r\n    ansible.builtin.set_stats:\r\n      data:\r\n        dist_ver_per_host_false_agg_false: &quot;{{ ansible_facts.distribution_version }}&quot;\r\n      per_host: false\r\n      aggregate: false<\/code><\/pre>\n<p>These two tasks don\u2019t do per host, so their info should be passed.  One is using aggregation and one is not.  By default set_stats has per_host false and aggregate: true.  Here are the results:<\/p>\n<pre class=\"gs-code\"><code class=\"language-plaintext\">    &quot;dist_ver_per_host_false_agg_true = 8.68.8&quot;,\r\n    &quot;dist_ver_per_host_false_agg_false = 8.8&quot;<\/code><\/pre>\n<p>So the two hosts I\u2019m running this against return different info; one passes back 8.6 and the other passes 8.8.<br \/>\nAs you can see the default action will just start smashing together all of the returned information, which, generally, isn\u2019t that helpful, unless you are really just passing over simple information.<\/p>\n<p>The second task with aggregation turned off simply returns the value of the very last host processed\u2026which also isn\u2019t always that useful in certain situations. <\/p>\n<h1>Per Host Stats Workaround<\/h1>\n<p>There is a workaround for passing per host information!<\/p>\n<pre class=\"gs-code\"><code class=\"language-plaintext\">  - name: per host collection and aggregate false via a workaround\r\n    ansible.builtin.set_stats:\r\n      data:\r\n        dist_ver_per_host_agg_false: &quot;{{ dist_ver_per_host_agg_false | default({}) | combine({ inventory_hostname : { &#039;ver&#039; : ansible_facts.distribution_version}}) }}&quot;\r\n      aggregate: false\r\n\r\n  - name: per host collection and aggregate true via a workaround\r\n    ansible.builtin.set_stats:\r\n      data:\r\n        dist_ver_per_host_agg_true: &quot;{{ dist_ver_per_host_agg_true | default({}) | combine({ inventory_hostname : { &#039;ver&#039; : ansible_facts.distribution_version}}) }}&quot;\r\n      aggregate: true<\/code><\/pre>\n<p>Looking at both tasks, there is a lot going on in a single line\u2026let\u2019s break it down:<\/p>\n<pre class=\"gs-code\"><code class=\"language-plaintext\">        dist_ver_per_host_agg_false: &quot;{{ dist_ver_per_host_agg_false | default({}) | combine({ inventory_hostname : { &#039;ver&#039; : ansible_facts.distribution_version}}) }}&quot;<\/code><\/pre>\n<p>This is actually one long line(even if there is word wrap on it).<br \/>\nIt takes the existing variable and builds on it.<br \/>\ndefault({}) sets the variable to empty if it doesn\u2019t exist(it\u2019s really a way around causing an error).<br \/>\ncombine joins two dictionaries together(which is what we are creating).  This one builds the variable out with the additional information.<\/p>\n<p>Here are the results when run:<br \/>\n<strong>First task<\/strong><\/p>\n<pre class=\"gs-code\"><code class=\"language-plaintext\">  &quot;dist_ver_per_host_agg_false&quot;: {\r\n    &quot;greg-rocky86nonlts&quot;: {\r\n      &quot;ver&quot;: &quot;8.8&quot;<\/code><\/pre>\n<p><strong>Second task<\/strong><\/p>\n<pre class=\"gs-code\"><code class=\"language-plaintext\">  &quot;dist_ver_per_host_agg_true&quot;: {\r\n    &quot;greg-rocky86lts&quot;: {\r\n      &quot;ver&quot;: &quot;8.6&quot;\r\n    },\r\n    &quot;greg-rocky86nonlts&quot;: {\r\n      &quot;ver&quot;: &quot;8.8&quot;\r\n    }<\/code><\/pre>\n<p>You can see the first task has aggregate turned off, so it only shows whatever host was processed last\u2026which means it\u2019s not really per host!<\/p>\n<p>The second task returns information for all of my hosts!  This means that I can collect and pass per host information from one job template to another in a playbook!<\/p>\n<p>Just to bring it all home, here\u2019s the variables section for the second job template(shows variables passed as \u201cextra vars\u201d):<br \/>\n<a href=\"https:\/\/gregsowell.com\/wp-content\/uploads\/2023\/10\/Screenshot_1.jpg\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/gregsowell.com\/wp-content\/uploads\/2023\/10\/Screenshot_1-300x290.jpg\" alt=\"\" width=\"300\" height=\"290\" class=\"aligncenter size-medium wp-image-7550\" srcset=\"https:\/\/gregsowell.com\/wp-content\/uploads\/2023\/10\/Screenshot_1-300x290.jpg 300w, https:\/\/gregsowell.com\/wp-content\/uploads\/2023\/10\/Screenshot_1.jpg 576w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><\/p>\n<h2>Conclusion<\/h2>\n<p>While it\u2019s not always necessary, it is beyond useful to pass per host information in your workflows, and how you have a solid method to do so!  If you have any tweaks or tunes you\u2019d make to this, please let me know.<\/p>\n<p>Good luck, and happy automating!  <\/p>\n","protected":false},"excerpt":{"rendered":"<p>Workflows allow AAP, AWX, and Ascender to visually connect playbooks together. This works great\u2026until you try and pass variables from one job template(playbook and\u2026<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[49,59],"tags":[],"class_list":["post-7540","post","type-post","status-publish","format-standard","hentry","category-ansible","category-ascender"],"_links":{"self":[{"href":"https:\/\/gregsowell.com\/index.php?rest_route=\/wp\/v2\/posts\/7540","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/gregsowell.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/gregsowell.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/gregsowell.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/gregsowell.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=7540"}],"version-history":[{"count":9,"href":"https:\/\/gregsowell.com\/index.php?rest_route=\/wp\/v2\/posts\/7540\/revisions"}],"predecessor-version":[{"id":7551,"href":"https:\/\/gregsowell.com\/index.php?rest_route=\/wp\/v2\/posts\/7540\/revisions\/7551"}],"wp:attachment":[{"href":"https:\/\/gregsowell.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=7540"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/gregsowell.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=7540"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/gregsowell.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=7540"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}