<?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 foreman)</title><link>https://www.die-welt.net/</link><description></description><atom:link href="https://www.die-welt.net/category/foreman.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>Tue, 21 Jul 2026 05:25:29 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Validating cloud-init configs without being root</title><link>https://www.die-welt.net/2026/01/validating-cloud-init-configs-without-being-root/</link><dc:creator>evgeni</dc:creator><description>&lt;p&gt;Somehow this whole DevOps thing is all about generating the wildest things from some (usually equally wild) template.&lt;/p&gt;
&lt;p&gt;And today we're gonna generate &lt;a href="https://github.com/theforeman/foreman/blob/develop/app/views/unattended/provisioning_templates/cloud_init/cloud_init_default.erb"&gt;YAML from ERB&lt;/a&gt;, what could possibly go wrong?!&lt;/p&gt;
&lt;p&gt;Well, actually, &lt;a href="https://projects.theforeman.org/issues/38442"&gt;quite&lt;/a&gt; &lt;a href="https://projects.theforeman.org/issues/37433"&gt;a lot&lt;/a&gt;,
so one wants to validate the generated result before using it to break systems at scale.&lt;/p&gt;
&lt;p&gt;The YAML we generate is a cloud-init &lt;a href="https://cloudinit.readthedocs.io/en/latest/explanation/about-cloud-config.html"&gt;cloud-config&lt;/a&gt;,
and while checking that we generated a valid YAML document is easy (and we were already doing that),
it would be much better if we could check that cloud-init can actually use it.&lt;/p&gt;
&lt;p&gt;Enter &lt;a href="https://cloudinit.readthedocs.io/en/latest/howto/debug_user_data.html#check-user-data-cloud-config"&gt;&lt;code&gt;cloud-init schema&lt;/code&gt;&lt;/a&gt;, or so I thought.
Turns out &lt;a href="https://github.com/canonical/cloud-init/issues/6680"&gt;running &lt;code&gt;cloud-init schema&lt;/code&gt; is rather broken without root privileges&lt;/a&gt;,
as it tries to load &lt;a href="https://github.com/canonical/cloud-init/issues/6592"&gt;a ton of information from the running system&lt;/a&gt;.
This seems like a bug (or multiple), as the data should not be required for the validation of the schema itself.
I've not found a way to disable that behavior.&lt;/p&gt;
&lt;p&gt;Luckily, &lt;a href="https://xkcd.com/208/"&gt;I know Python&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Enter &lt;code&gt;evgeni-knows-better-and-can-write-python&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="ch"&gt;#!/usr/bin/env python3&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;cloudinit.config.schema&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;get_schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validate_cloudconfig_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SchemaValidationError&lt;/span&gt;

&lt;span class="k"&gt;try&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="n"&gt;validate_cloudconfig_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;get_schema&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Schema is not valid"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SchemaValidationError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;)&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="nb"&gt;print&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;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The canonical&lt;sup id="fnref:canonical"&gt;&lt;a class="footnote-ref" href="https://www.die-welt.net/2026/01/validating-cloud-init-configs-without-being-root/#fn:canonical"&gt;1&lt;/a&gt;&lt;/sup&gt; version if this &lt;a href="https://github.com/theforeman/foreman/blob/develop/script/cloud-init-validate"&gt;lives in the Foreman git repo&lt;/a&gt;, so go there if you think this will ever receive any updates.&lt;/p&gt;
&lt;p&gt;The hardest part was to understand the&lt;code&gt;validate_cloudconfig_file&lt;/code&gt; API,
as it will sometimes raise an &lt;code&gt;SchemaValidationError&lt;/code&gt;,
sometimes a &lt;code&gt;RuntimeError&lt;/code&gt; and sometimes just return &lt;code&gt;False&lt;/code&gt;.
No idea why.
But the above just turns it into a couple of printed lines and a non zero exit code,
unless of course there are no problems, then you get peaceful silence.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:canonical"&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Canonical"&gt;"canonical"&lt;/a&gt;, not &lt;a href="https://en.wikipedia.org/wiki/Canonical_(company)"&gt;"Canonical"&lt;/a&gt; &lt;a class="footnote-backref" href="https://www.die-welt.net/2026/01/validating-cloud-init-configs-without-being-root/#fnref:canonical" 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>english</category><category>foreman</category><category>linux</category><category>planet-debian</category><category>software</category><guid>https://www.die-welt.net/2026/01/validating-cloud-init-configs-without-being-root/</guid><pubDate>Wed, 21 Jan 2026 19:42:45 GMT</pubDate></item><item><title>The Mocking will continue, until CI improves</title><link>https://www.die-welt.net/2021/12/the-mocking-will-continue-until-ci-improves/</link><dc:creator>evgeni</dc:creator><description>&lt;p&gt;One might think, this blog is exclusively about &lt;a href="https://www.die-welt.net/2021/11/i-just-want-to-run-this-one-python-script/"&gt;weird&lt;/a&gt; &lt;a href="https://www.die-welt.net/2021/11/a-string-is-not-a-string-and-thats-groovy/"&gt;language behavior&lt;/a&gt; and yelling at computers… Well, welcome to another episode of Jackass!&lt;/p&gt;
&lt;p&gt;Today's opponent is &lt;a href="https://www.ruby-lang.org/en/"&gt;Ruby&lt;/a&gt;, or maybe &lt;a href="https://github.com/seattlerb/minitest"&gt;minitest&lt;/a&gt; , or maybe &lt;a href="https://github.com/freerange/mocha"&gt;Mocha&lt;/a&gt;. I'm not exactly sure, but it was a rather amusing exercise and I like to share my nightmares ;)&lt;/p&gt;
&lt;p&gt;It all started with the classical &lt;a href="https://projects.theforeman.org/issues/26968"&gt;"you're using old and unmaintained software, please switch to something new"&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/theforeman/foreman/pull/7997"&gt;first attempt&lt;/a&gt; was to switch from the &lt;code&gt;ci_reporter_minitest&lt;/code&gt; plugin to the &lt;code&gt;minitest-ci&lt;/code&gt; plugin. While the change worked great for Foreman itself, it broke the reporting in &lt;a href="https://github.com/Katello/Katello"&gt;Katello&lt;/a&gt; - the tests would run but no &lt;code&gt;junit.xml&lt;/code&gt; was generated and Jenkins rightfully complained that it got no test results.&lt;/p&gt;
&lt;p&gt;While investigating what the hell was wrong, we realized that Katello was already using a minitest reporting plugin: &lt;code&gt;minitest-reporters&lt;/code&gt;. Loading two different reporting plugins seemed like a good source for problems, so I tried &lt;a href="https://github.com/theforeman/foreman/pull/8960"&gt;using the same plugin for Foreman too&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Guess what? After a bit of massaging (mostly to disable the second &lt;code&gt;minitest-reporters&lt;/code&gt; initialization in Katello) reporting of test results from Katello started to work like a charm. But now the Foreman tests started to fail. Not fail to report, fail to actually run. WTH‽&lt;/p&gt;
&lt;p&gt;The failure was quite interesting too:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;test/unit/parameter_filter_test.rb:5:in `block in &amp;lt;class:ParameterFilterTest&amp;gt;':
  Mocha methods cannot be used outside the context of a test (Mocha::NotInitializedError)
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Yes, this is a single test file failing, all others were fine.&lt;/p&gt;
&lt;p&gt;The failing code doesn't look problematic on first glance:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'test_helper'&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;ParameterFilterTest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TestCase&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:klass&lt;/span&gt;&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="n"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Example'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tap&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="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stubs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Example'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&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="s1"&gt;'something'&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="n"&gt;something&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The failing line (5) is &lt;code&gt;mock('Example').tap …&lt;/code&gt; and for some reason Mocha thinks it's not initialized here.&lt;/p&gt;
&lt;p&gt;This &lt;em&gt;certainly&lt;/em&gt; has something to do with &lt;em&gt;how&lt;/em&gt; the various reporting plugins inject themselves, but I really didn't want to debug how to run two reporting plugins in parallel (which, as you remember, didn't expose this behavior). So the only real path forward was to debug what's happening here.&lt;/p&gt;
&lt;p&gt;Calling the test on its own, with one of the working reporter was the first step:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;bundle&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rake&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="nv"&gt;TEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;test/unit/parameter_filter_test.rb&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;TESTOPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-v
&lt;span class="go"&gt;…&lt;/span&gt;
&lt;span class="gp"&gt;#&lt;/span&gt;&amp;lt;Mocha::Mock:0x0000557bf1f22e30&amp;gt;#test_0001_permits&lt;span class="w"&gt; &lt;/span&gt;plugin-added&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;attribute&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="m"&gt;0&lt;/span&gt;.04&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;s&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="gp"&gt;#&lt;/span&gt;&amp;lt;Mocha::Mock:0x0000557bf12cf750&amp;gt;#test_0002_permits&lt;span class="w"&gt; &lt;/span&gt;plugin-added&lt;span class="w"&gt; &lt;/span&gt;attributes&lt;span class="w"&gt; &lt;/span&gt;from&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;blocks&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="m"&gt;0&lt;/span&gt;.49&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;s&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="go"&gt;…&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Wait, what? &lt;code&gt;#&amp;lt;Mocha::Mock:…&amp;gt;&lt;/code&gt;? Shouldn't this read more like &lt;code&gt;ParameterFilterTest::…&lt;/code&gt; as it happens for every single other test in our test suite? It definitely should! That's actually great, as it tells us that there is really something wrong with the &lt;em&gt;test&lt;/em&gt; and the change of the reporting plugin just makes it worse.&lt;/p&gt;
&lt;p&gt;What comes next is sheer luck. Well, that, and years of experience in yelling at computers.&lt;/p&gt;
&lt;p&gt;We use &lt;code&gt;let(:klass)&lt;/code&gt; to define an object called &lt;code&gt;klass&lt;/code&gt; and this object is a &lt;code&gt;Mocha::Mock&lt;/code&gt; that we'll use in our tests later. Now &lt;code&gt;klass&lt;/code&gt; is a very common term in Ruby when talking about classes and needing to store them — mostly because one can't use &lt;code&gt;class&lt;/code&gt; which is a keyword. Is something else in the stack using &lt;code&gt;klass&lt;/code&gt; and our &lt;code&gt;let&lt;/code&gt; is overriding that, making this whole thing explode?&lt;/p&gt;
&lt;p&gt;It was! The moment we &lt;a href="https://github.com/theforeman/foreman/pull/8963"&gt;replaced &lt;code&gt;klass&lt;/code&gt; with &lt;code&gt;klass1&lt;/code&gt;&lt;/a&gt; (silly, I know, but there also was a &lt;code&gt;klass2&lt;/code&gt; in that code, so it did fit), things started to work nicely.&lt;/p&gt;
&lt;p&gt;I really liked &lt;a href="https://github.com/tbrisker"&gt;Tomer&lt;/a&gt;'s comment in the PR: &lt;a href="https://github.com/theforeman/foreman/pull/8963#pullrequestreview-821333602"&gt;"no idea why, but I am not going to dig into mocha to figure that out."&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Turns out, I couldn't let (HAH!) the code rest and really wanted to understand what happened there.&lt;/p&gt;
&lt;p&gt;What I didn't want to do is to debug the whole Foreman test stack, because it is massive.&lt;/p&gt;
&lt;p&gt;So I started to write a minimal reproducer for the issue.&lt;/p&gt;
&lt;p&gt;All starts with a &lt;code&gt;Gemfile&lt;/code&gt;, as we need a few dependencies:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;gem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'rake'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'mocha'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'minitest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'~&amp;gt; 5.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt; 5.11'&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then a &lt;code&gt;Rakefile&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'rake/testtask'&lt;/span&gt;

&lt;span class="no"&gt;Rake&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TestTask&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:test&lt;/span&gt;&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="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'test'&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;test_files&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="no"&gt;FileList&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"test/**/*_test.rb"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:default&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="ss"&gt;:test&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And a test! I took the liberty to replace &lt;code&gt;ActiveSupport::TestCase&lt;/code&gt; with &lt;code&gt;Minitest::Test&lt;/code&gt;, as the test won't be using any Rails features and I wanted to keep my environment minimal.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'minitest/autorun'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'minitest/spec'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'mocha/minitest'&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;ParameterFilterTest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;Minitest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kp"&gt;extend&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;Minitest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Spec&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DSL&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:klass&lt;/span&gt;&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="n"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Example'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tap&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="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stubs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Example'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="w"&gt;  &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;test_lol&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;klass&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Well, damn, this passed! Is it Rails after all that breaks stuff? Let's add it to the &lt;code&gt;Gemfile&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;vim&lt;span class="w"&gt; &lt;/span&gt;Gemfile
&lt;span class="gp"&gt;$ &lt;/span&gt;bundle&lt;span class="w"&gt; &lt;/span&gt;install
&lt;span class="gp"&gt;$ &lt;/span&gt;bundle&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rake&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="nv"&gt;TESTOPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-v
&lt;span class="go"&gt;…&lt;/span&gt;
&lt;span class="gp"&gt;#&lt;/span&gt;&amp;lt;Mocha::Mock:0x0000564bbfe17e98&amp;gt;#test_lol&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="m"&gt;0&lt;/span&gt;.00&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;s&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;/pre&gt;&lt;/div&gt;

&lt;p&gt;Wait, I didn't change anything and it's already failing?! Fuck! I mean, cool!&lt;/p&gt;
&lt;p&gt;But the test isn't &lt;em&gt;minimal&lt;/em&gt; yet. What can we reduce? &lt;code&gt;let&lt;/code&gt; is just a fancy, lazy &lt;code&gt;def&lt;/code&gt;, right? So instead of &lt;code&gt;let(:klass)&lt;/code&gt; we should be able to write &lt;code&gt;def klass&lt;/code&gt; and achieve a similar outcome &lt;em&gt;and&lt;/em&gt; drop that &lt;code&gt;Minitest::Spec&lt;/code&gt;.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'minitest/autorun'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'mocha/minitest'&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;ParameterFilterTest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;Minitest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;
&lt;span class="w"&gt;  &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;klass&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;mock&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="w"&gt;  &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;test_lol&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;klass&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&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;bundle&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rake&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="nv"&gt;TESTOPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-v
&lt;span class="go"&gt;…&lt;/span&gt;
&lt;span class="go"&gt;/home/evgeni/Devel/minitest-wtf/test/parameter_filter_test.rb:5:in `klass': Mocha methods cannot be used outside the context of a test (Mocha::NotInitializedError)&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/railties-6.1.4.1/lib/rails/test_unit/reporter.rb:68:in `format_line'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/railties-6.1.4.1/lib/rails/test_unit/reporter.rb:15:in `record'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:682:in `block in record'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:681:in `each'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:681:in `record'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:324:in `run_one_method'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:311:in `block (2 levels) in run'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:310:in `each'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:310:in `block in run'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:350:in `on_signal'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:337:in `with_info_handler'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:309:in `run'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:159:in `block in __run'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:159:in `map'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:159:in `__run'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:136:in `run'&lt;/span&gt;
&lt;span class="go"&gt;    from /home/evgeni/Devel/minitest-wtf/vendor/bundle/ruby/3.0.0/gems/minitest-5.10.3/lib/minitest.rb:63:in `block in autorun'&lt;/span&gt;
&lt;span class="go"&gt;rake aborted!&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Oh nice, this is even better! Instead of the mangled class name, we now get the very same error the Foreman tests aborted with, &lt;em&gt;plus&lt;/em&gt; a nice stack trace! But wait, why is it pointing at &lt;code&gt;railties&lt;/code&gt;? We're not loading that! Anyways, lets look at &lt;code&gt;railties-6.1.4.1/lib/rails/test_unit/reporter.rb&lt;/code&gt;, line 68&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;format_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;klass&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="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;respond_to?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:klass&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;klass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"%s#%s = %.2f s = %s"&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="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;klass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result_code&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Heh, this is touching &lt;code&gt;result.klass&lt;/code&gt;, which we just messed up. Nice!&lt;/p&gt;
&lt;p&gt;But quickly back to &lt;code&gt;railties&lt;/code&gt;… What if we only add that to the &lt;code&gt;Gemfile&lt;/code&gt;, not full blown Rails?&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;gem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'railties'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'rake'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'mocha'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'minitest'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'~&amp;gt; 5.1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt; 5.11'&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Yepp, same failure. Also happens with &lt;code&gt;require =&amp;gt; false&lt;/code&gt; added to the line, so it seems &lt;code&gt;railties&lt;/code&gt; somehow injects itself into &lt;code&gt;rake&lt;/code&gt; even if nothing is using it?! "Cool"!&lt;/p&gt;
&lt;p&gt;By the way, why are we still pinning &lt;code&gt;minitest&lt;/code&gt; to &lt;code&gt;&amp;lt; 5.11&lt;/code&gt;? Oh right, this was the &lt;a href="https://projects.theforeman.org/issues/22110"&gt;&lt;em&gt;original&lt;/em&gt; reason to look into that whole topic&lt;/a&gt;. And, uh, it's pointing at &lt;code&gt;klass&lt;/code&gt; there already! 4 years ago!&lt;/p&gt;
&lt;p&gt;So &lt;a href="https://github.com/theforeman/foreman/pull/8969"&gt;lets remove that boundary&lt;/a&gt; and funny enough, &lt;em&gt;now&lt;/em&gt; tests are passing again, even if we use &lt;code&gt;klass&lt;/code&gt;!&lt;/p&gt;
&lt;p&gt;Minitest 5.11 changed how &lt;code&gt;Minitest::Test&lt;/code&gt; is structured, and seems not to rely on &lt;code&gt;klass&lt;/code&gt; at that point anymore. And I guess Rails also changed a bit since the original pin was put in place four years ago.&lt;/p&gt;
&lt;p&gt;I didn't want to go another rabbit hole, finding out what changed in Rails, but I did try with 5.0 (well, 5.0.7.2) to be precise, and the output with newer (&amp;gt;= 5.11) Minitest was interesting:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;bundle&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rake&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="nv"&gt;TESTOPTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-v
&lt;span class="go"&gt;…&lt;/span&gt;
&lt;span class="gp"&gt;Minitest::Result#&lt;/span&gt;&lt;span class="nv"&gt;test_lol&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="m"&gt;0&lt;/span&gt;.00&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;s&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;/pre&gt;&lt;/div&gt;

&lt;p&gt;It's leaking &lt;code&gt;Minitest::Result&lt;/code&gt; as &lt;code&gt;klass&lt;/code&gt; now, instead of &lt;code&gt;Mocha::Mock&lt;/code&gt;. So probably something along these lines was broken 4 years ago and triggered this pin.&lt;/p&gt;
&lt;p&gt;What do we learn from that?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;klass&lt;/code&gt; is cursed and shouldn't be used in places where inheritance and tooling might decide to use it for some reason&lt;/li&gt;
&lt;li&gt;inheritance is cursed - why the heck are implementation details of Minitest leaking inside my tests?!&lt;/li&gt;
&lt;li&gt;tooling is cursed - why is &lt;code&gt;railties&lt;/code&gt; injecting stuff when I didn't ask it to?!&lt;/li&gt;
&lt;li&gt;dependency pinning is cursed - at least if you pin to avoid an issue and then forget about said issue for four years&lt;/li&gt;
&lt;li&gt;I like cursed things!&lt;/li&gt;
&lt;/ul&gt;</description><category>english</category><category>foreman</category><category>linux</category><category>planet-debian</category><category>software</category><guid>https://www.die-welt.net/2021/12/the-mocking-will-continue-until-ci-improves/</guid><pubDate>Tue, 07 Dec 2021 19:39:41 GMT</pubDate></item><item><title>A String is not a String, and that's Groovy!</title><link>https://www.die-welt.net/2021/11/a-string-is-not-a-string-and-thats-groovy/</link><dc:creator>evgeni</dc:creator><description>&lt;p&gt;Halloween is over, but I still have some nightmares to share with you, so sit down, take some hot chocolate and enjoy :)&lt;/p&gt;
&lt;p&gt;When working with Jenkins, there is almost no way to avoid writing Groovy. Well, unless you only do old style jobs with shell scripts, but y'all know what I think about shell scripts…&lt;/p&gt;
&lt;p&gt;Anyways, &lt;a href="https://github.com/ehelms"&gt;Eric&lt;/a&gt; have been rewriting the jobs responsible for building Debian packages for &lt;a href="https://theforeman.org"&gt;Foreman&lt;/a&gt; to pipelines (and thus Groovy).&lt;/p&gt;
&lt;p&gt;Our build process for pull requests is rather simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Setup sources - get the orig tarball and adjust changelog to have an unique version for pull requests&lt;/li&gt;
&lt;li&gt;Call pbuilder&lt;/li&gt;
&lt;li&gt;Upload the built package to a staging archive for testing&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For merges, it's identical, minus the changelog adjustment.&lt;/p&gt;
&lt;p&gt;And if there are multiple packages changed in one go, it runs each step in parallel for each package.&lt;/p&gt;
&lt;p&gt;Now I've been doing mass changes to our plugin packages, to move them to a shared postinst helper instead of having the same code over and over in every package. This required changes to many packages and sometimes I'd end up building multiple at once. That should be fine, right?&lt;/p&gt;
&lt;p&gt;Well, yeah, it did &lt;em&gt;build&lt;/em&gt; fine, but the &lt;em&gt;upload&lt;/em&gt; only happened for the last package. This felt super weird, especially as I was absolutely sure we did test this scenario (multiple packages in one PR) and it worked just fine…&lt;/p&gt;
&lt;p&gt;So I went on a ride though the internals of the job, trying to understand why it didn't work.&lt;/p&gt;
&lt;p&gt;This requires a tad more information about the way we handle packages for Foreman:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the archive is handled by &lt;a href="https://github.com/freight-team/freight"&gt;freight&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;it has suites like &lt;code&gt;buster&lt;/code&gt;, &lt;code&gt;focal&lt;/code&gt; and &lt;code&gt;plugins&lt;/code&gt; (that one is a tad special)&lt;/li&gt;
&lt;li&gt;each suite has components that match Foreman releases, so &lt;code&gt;2.5&lt;/code&gt;, &lt;code&gt;3.0&lt;/code&gt;, &lt;code&gt;3.1&lt;/code&gt;, &lt;code&gt;nightly&lt;/code&gt; etc&lt;/li&gt;
&lt;li&gt;core packages (Foreman etc) are built for all supported distributions (right now: buster and focal)&lt;/li&gt;
&lt;li&gt;plugin packages are built only once and can be used on every distribution&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As generating the package index isn't exactly fast in freight, we tried not not run it too often. The idea was that when we build two packages for the same target (suite/version combination), we upload both at once and run import only once for both. That means that when we build Foreman for buster and focal, this results in two parallel builds and then two parallel uploads (as they end up in different suites). But if we build Foreman and Foreman Installer, we have four parallel builds, but only two parallel uploads, as we can batch upload Foreman and Installer per suite. Well, or so was the theory.&lt;/p&gt;
&lt;p&gt;The Groovy code, that was supposed to do this looked roughly like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kt"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;packages_to_build&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="n"&gt;find_changed_packages&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="kt"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;repos&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="o"&gt;[:]&lt;/span&gt;

&lt;span class="n"&gt;packages_to_build&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;each&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="n"&gt;pkg&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;suite&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;'buster'&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;component&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;'3.0'&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;target&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="s2"&gt;"${suite}-${component}"&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="n"&gt;repos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;containsKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&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="n"&gt;repos&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&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="o"&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="n"&gt;repos&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;].&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pkg&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;do_the_build&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;packages_to_build&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;do_the_upload&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repos&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's pretty straight forward, no? We create an empty &lt;a href="https://docs.groovy-lang.org/latest/html/groovy-jdk/java/util/Map.html"&gt;Map&lt;/a&gt;, loop over a list of packages and add them to an entry in the map which we pre-create as empty if it doesn't exist.&lt;/p&gt;
&lt;p&gt;Well, no, the resulting map always ended with only having one element in each target list. And this is also why our original tests always worked: we tested with a PR containing changes to Foreman and a plugin, and plugins go to this special target we have…&lt;/p&gt;
&lt;p&gt;So I started playing with the code (&lt;a href="https://groovyide.com/playground"&gt;https://groovyide.com/playground&lt;/a&gt; is really great for that!), trying to understand why the heck it erases previous data.&lt;/p&gt;
&lt;p&gt;The first finding was that it just always ended up jumping into the "if map entry not found" branch, even though the map very clearly had the correct entry &lt;em&gt;after&lt;/em&gt; the first package was added.&lt;/p&gt;
&lt;p&gt;The second one was weird. I was trying to minimize the reproducer code (IMHO always a good idea) and switched &lt;code&gt;target = "${suite}-${component}"&lt;/code&gt; to &lt;code&gt;target = "lol"&lt;/code&gt;. Two entries in the list, only one jump into the "map entry not found" branch. What?! 🧐&lt;/p&gt;
&lt;p&gt;So this is clearly related to the fact that we're using String interpolation here. But hey, that's a totally normal thing to do, isn't it?!&lt;/p&gt;
&lt;p&gt;Admittedly, at this point, I was lost. I knew &lt;em&gt;what&lt;/em&gt; breaks, but not &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Luckily, I knew exactly who to ask: &lt;a href="https://twitter.com/jbendisposto"&gt;Jens&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After a brief "well, that's interesting", Jens quickly found the source of our griefs: &lt;a href="https://groovy-lang.org/syntax.html#_double_quoted_string"&gt;Double-quoted strings are plain &lt;code&gt;java.lang.String&lt;/code&gt; if there’s no interpolated expression, but are &lt;code&gt;groovy.lang.GString&lt;/code&gt; instances if interpolation is present.&lt;/a&gt;. And when we do &lt;code&gt;repos[target]&lt;/code&gt; the GString &lt;code&gt;target&lt;/code&gt; gets converted to a String, but when we use &lt;code&gt;repos.containsKey()&lt;/code&gt; it remains a GString. This is because &lt;a href="https://groovy-lang.org/syntax.html#_interoperability_with_java"&gt;GStrings get converted to Strings, if the method wants one&lt;/a&gt;, but &lt;a href="https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Map.html#containsKey(java.lang.Object)"&gt;&lt;code&gt;containsKey&lt;/code&gt;&lt;/a&gt; takes any &lt;code&gt;Object&lt;/code&gt; while the &lt;code&gt;repos[target]&lt;/code&gt; notation for some reason converts it. Maybe this is because &lt;a href="https://groovy-lang.org/syntax.html#_gstring_and_string_hashcodes"&gt;using GString as Map keys should be avoided&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We can reproduce this with simpler code:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kt"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;map&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="o"&gt;[:]&lt;/span&gt;
&lt;span class="kt"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;something&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="s2"&gt;"something"&lt;/span&gt;
&lt;span class="kt"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;key&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="s2"&gt;"${something}"&lt;/span&gt;
&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&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="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClass&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;keySet&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;each&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClass&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;keySet&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;each&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;)}&lt;/span&gt;
&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;keySet&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;each&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="n"&gt;println&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="o"&gt;)}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Which results in the following output:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="n"&gt;org&lt;/span&gt;.&lt;span class="n"&gt;codehaus&lt;/span&gt;.&lt;span class="n"&gt;groovy&lt;/span&gt;.&lt;span class="n"&gt;runtime&lt;/span&gt;.&lt;span class="n"&gt;GStringImpl&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="n"&gt;java&lt;/span&gt;.&lt;span class="nb"&gt;lang&lt;/span&gt;.&lt;span class="n"&gt;String&lt;/span&gt;
&lt;span class="n"&gt;false&lt;/span&gt;
&lt;span class="n"&gt;true&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With that knowledge, the fix was to just use the same &lt;code&gt;repos[target]&lt;/code&gt; notation also for checking for existence — Groovy helpfully returns &lt;code&gt;null&lt;/code&gt; which is false-y when it can't find an entry in a Map absent.&lt;/p&gt;
&lt;p&gt;So yeah, a String is not always a String, and it'll bite you!&lt;/p&gt;</description><category>english</category><category>foreman</category><category>linux</category><category>planet-debian</category><category>software</category><guid>https://www.die-welt.net/2021/11/a-string-is-not-a-string-and-thats-groovy/</guid><pubDate>Fri, 19 Nov 2021 14:16:00 GMT</pubDate></item><item><title>It's not *always* DNS</title><link>https://www.die-welt.net/2021/07/its-not-always-dns/</link><dc:creator>evgeni</dc:creator><description>&lt;p&gt;Two weeks ago, I had the pleasure to play with &lt;a href="https://theforeman.org"&gt;Foreman&lt;/a&gt;s Kerberos integration and iron out a few long standing kinks.&lt;/p&gt;
&lt;p&gt;It all started with a user reminding us that &lt;a href="https://projects.theforeman.org/issues/32352"&gt;Kerberos authentication is broken when Foreman is deployed on CentOS 8&lt;/a&gt;, as there is no more &lt;a href="http://modauthkerb.sourceforge.net/"&gt;&lt;code&gt;mod_auth_kerb&lt;/code&gt;&lt;/a&gt; available. Given &lt;code&gt;mod_auth_kerb&lt;/code&gt; hasn't seen a release since 2013, this is quite understandable. Thankfully, there is a replacement available, &lt;a href="https://github.com/gssapi/mod_auth_gssapi"&gt;&lt;code&gt;mod_auth_gssapi&lt;/code&gt;&lt;/a&gt;. Even better, it's available in CentOS 7 and 8 and in Debian and Ubuntu too!&lt;/p&gt;
&lt;p&gt;So I quickly whipped up a &lt;a href="https://github.com/theforeman/puppet-foreman/pull/967"&gt;PR to completely replace &lt;code&gt;mod_auth_kerb&lt;/code&gt; with &lt;code&gt;mod_auth_gssapi&lt;/code&gt; in our installer&lt;/a&gt; and successfully tested that it still works in CentOS 7 (even if upgrading from a &lt;code&gt;mod_auth_kerb&lt;/code&gt; installation) and CentOS 8. &lt;/p&gt;
&lt;p&gt;Yay, the issue at hand seemed fixed. But just writing a post about that would've been boring, huh?&lt;/p&gt;
&lt;p&gt;Well, and then I dared to test the same on Debian…&lt;/p&gt;
&lt;p&gt;Turns out, our installer was using the &lt;a href="https://projects.theforeman.org/issues/32947"&gt;wrong path to the Apache configuration and the wrong username Apache runs under&lt;/a&gt; while trying to setup Kerberos, so it could not have ever worked. Luckily &lt;a href="https://github.com/ekohl"&gt;Ewoud&lt;/a&gt; and I were &lt;a href="https://github.com/theforeman/puppet-foreman/pull/968"&gt;able to fix that too&lt;/a&gt;. And yet the installer was still unable to fetch the keytab from my FreeIPA server 😿&lt;/p&gt;
&lt;p&gt;Let's dig deeper! To fetch the keytab, the installer does roughly this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;# &lt;/span&gt;kinit&lt;span class="w"&gt; &lt;/span&gt;-k
&lt;span class="gp"&gt;# &lt;/span&gt;ipa-getkeytab&lt;span class="w"&gt; &lt;/span&gt;-k&lt;span class="w"&gt; &lt;/span&gt;http.keytab&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;HTTP/foreman.example.com
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And if one executes that by hand to see the a actual error, you see:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;# &lt;/span&gt;kinit&lt;span class="w"&gt; &lt;/span&gt;-k
&lt;span class="go"&gt;kinit: Cannot determine realm for host (principal host/foreman@)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Well, yeah, the principal looks kinda weird (no realm) and the interwebs say for "kinit: Cannot determine realm for host":&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kerberos cannot determine the realm name for the host. (Well, duh, that's what it said?!)&lt;/li&gt;
&lt;li&gt;Make sure that there is a default realm name, or that the domain name mappings are set up in the Kerberos configuration file (krb5.conf)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And guess what, all of these are perfectly set by &lt;code&gt;ipa-client-install&lt;/code&gt; when joining the realm…&lt;/p&gt;
&lt;p&gt;But there must be something, right? Looking at the principal in the error, it's missing both the domain of the host and the realm.
I was pretty sure that my DNS and config was right, but what about &lt;a href="https://man7.org/linux/man-pages/man2/gethostname.2.html"&gt;&lt;code&gt;gethostname(2)&lt;/code&gt;&lt;/a&gt;?&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;# &lt;/span&gt;hostname
&lt;span class="go"&gt;foreman&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Bingo! Let's see what happens if we force that to be an FQDN?&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gp"&gt;# &lt;/span&gt;hostname&lt;span class="w"&gt; &lt;/span&gt;foreman.example.com
&lt;span class="gp"&gt;# &lt;/span&gt;kinit&lt;span class="w"&gt; &lt;/span&gt;-k
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;NO ERRORS! NICE!&lt;/p&gt;
&lt;p&gt;We're doing science here, right? And I still have the CentOS 8 box I had for the previous round of tests.
What happens if we set that to have a shortname? Nothing. It keeps working fine.
And what about CentOS 7? VMs are cheap. Well, that breaks like on Debian, if we force the hostname to be short. Interesting.&lt;/p&gt;
&lt;p&gt;Is it a version difference between the systems?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Debian 10 has krb5 1.17-3+deb10u1&lt;/li&gt;
&lt;li&gt;CentOS 7 has krb5 1.15.1-50.el7&lt;/li&gt;
&lt;li&gt;CentOS 8 has krb5 1.18.2-8.el8&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, something changed in 1.18?&lt;/p&gt;
&lt;p&gt;Looking at the &lt;a href="https://web.mit.edu/kerberos/krb5-1.18/"&gt;krb5 1.18 changelog&lt;/a&gt; the following entry jumps at one:
&lt;em&gt;Expand single-component hostnames in host-based principal names when DNS canonicalization is not used, adding the system's first DNS search path as a suffix.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Given Debian 11 has krb5 1.18.3-5 (well, testing has, so lets pretend bullseye will too), we can retry the experiment there, and it shows that it works with both, short and full hostname.
So yeah, it seems krb5 "does the right thing" since 1.18, and before that &lt;code&gt;gethostname(2)&lt;/code&gt; &lt;em&gt;must&lt;/em&gt; return an FQDN.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/theforeman/foreman-documentation/pull/594"&gt;I've documented that for our users and can now sleep a bit better&lt;/a&gt;. At least, it wasn't DNS, right?!&lt;/p&gt;
&lt;p&gt;Btw, &lt;a href="https://tracker.debian.org/pkg/freeipa"&gt;&lt;code&gt;freeipa&lt;/code&gt; won't be in bulsseye&lt;/a&gt;, which makes me a bit sad, as that means that Foreman won't be able to automatically join FreeIPA realms if deployed on Debian 11.&lt;/p&gt;</description><category>centos</category><category>debian</category><category>english</category><category>foreman</category><category>linux</category><category>planet-debian</category><category>software</category><guid>https://www.die-welt.net/2021/07/its-not-always-dns/</guid><pubDate>Fri, 23 Jul 2021 18:36:34 GMT</pubDate></item><item><title>systemd + SELinux = 🔥</title><link>https://www.die-welt.net/2020/12/systemd-%2B-selinux/</link><dc:creator>evgeni</dc:creator><description>&lt;p&gt;Okay, getting a title that will ensure clicks for this post was easy. Now comes the hard part: content!&lt;/p&gt;
&lt;p&gt;When you deploy &lt;a href="https://theforeman.org"&gt;The Foreman&lt;/a&gt;, you want a secure setup by default. That's why we ship (and enable) a SELinux policy which allows you to run the involved daemons in confined mode.&lt;/p&gt;
&lt;p&gt;We have recently switched our default Ruby application server from Passenger (running via &lt;code&gt;mod_passenger&lt;/code&gt; &lt;em&gt;inside&lt;/em&gt; Apache httpd) to Puma (running standalone and Apache just being a reverse proxy). While doing so, we initially deployed Puma listening on &lt;code&gt;localhost:3000&lt;/code&gt; and while &lt;code&gt;localhost&lt;/code&gt; is &lt;em&gt;pretty&lt;/em&gt; safe, a local user could still turn out evil and talk directly to Puma, pretending to be authenticated by Apache (think Kerberos or X.509 cert auth).&lt;/p&gt;
&lt;p&gt;Obviously, this is not optimal, so the &lt;a href="https://projects.theforeman.org/issues/30803"&gt;next task was to switch Puma to listen on an UNIX socket&lt;/a&gt; and only allow Apache to talk to said socket.&lt;/p&gt;
&lt;p&gt;This doesn't sound overly complicated, &lt;a href="https://github.com/theforeman/puppet-foreman/commit/247395005a170cf4a3b462b1532bf30745a332b5"&gt;and indeed it wasn't&lt;/a&gt;. The most time/thought was spent on doing that in a way that doesn't break existing setups and still allows binding to a TCP socket for setups where users explicitly want that.&lt;/p&gt;
&lt;p&gt;We also made a change to the SELinux policy to properly &lt;a href="https://github.com/theforeman/foreman-selinux/commit/7a8c5abc846c6f968f3cee6a94f787a285c48e2e"&gt;label the newly created socket and allow httpd to access it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The whole change was carefully tested on CentOS 7 and worked like a charm. So we merged it, and it broke. Only on CentOS 8, but broken is broken, right?&lt;/p&gt;
&lt;p&gt;This is the start of my Thanksgiving story "learn how to debug SELinux issues" ;)&lt;/p&gt;
&lt;p&gt;From the logs of our integration test I knew the issue was Apache not being able to talk to that new socket (we archive &lt;code&gt;sos&lt;/code&gt; reports as part of the tests, and those clearly had it in the &lt;code&gt;auditd&lt;/code&gt; logs). But I also knew we did prepare our policy for that change, so either our preparation was not sufficient or the policy wasn't properly loaded.&lt;/p&gt;
&lt;p&gt;The same &lt;code&gt;sos&lt;/code&gt; report also contained the output of &lt;code&gt;semanage fcontext --list&lt;/code&gt; which stated that all regular files called &lt;code&gt;/run/foreman.sock&lt;/code&gt; would get the &lt;code&gt;foreman_var_run_t&lt;/code&gt; type assigned. Wait a moment, all &lt;em&gt;regular&lt;/em&gt; files?! A socket is not a regular file! Let's quickly &lt;a href="https://github.com/theforeman/foreman-selinux/commit/a74b2481ce5f716a032741ceb17fa54336df5e6e"&gt;make that truly &lt;em&gt;all&lt;/em&gt; files&lt;/a&gt;. That clearly changed the &lt;code&gt;semanage fcontext --list&lt;/code&gt; output, but the socket was still created as &lt;code&gt;var_run_t&lt;/code&gt;?!&lt;/p&gt;
&lt;p&gt;It was time to actually boot a CentOS 8 VM and try more things out. Interestingly, you actually can't add a rule for &lt;code&gt;/run/something&lt;/code&gt;, as &lt;code&gt;/run&lt;/code&gt; is an alias (equivalency in SELinux speak) for &lt;code&gt;/var/run&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;semanage&lt;span class="w"&gt; &lt;/span&gt;fcontext&lt;span class="w"&gt; &lt;/span&gt;--add&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;foreman_var_run_t&lt;span class="w"&gt; &lt;/span&gt;/run/foreman.sock
&lt;span class="go"&gt;ValueError: File spec /run/foreman.sock conflicts with equivalency rule '/run /var/run'; Try adding '/var/run/foreman.sock' instead&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I have no idea how the list output in the report got that &lt;code&gt;/run&lt;/code&gt; rule, but &lt;a href="https://github.com/theforeman/foreman-selinux/commit/50d4abeb5c5b9ea423b804ee8dfc470d212b440a"&gt;okay, let's match &lt;code&gt;/var/run/foreman.sock&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Did that solve the issue? Of course not! And you knew it, as I didn't get to the juciest part of the headline yet: systemd!&lt;/p&gt;
&lt;p&gt;We use systemd to create the socket, as it is both convenient and useful (no more clients connecting before Rails has finished booting). But why is it wrongly labeling our freshly created socket?! A quick check with &lt;code&gt;touch&lt;/code&gt; shows that the policy is correct now, the &lt;code&gt;touch&lt;/code&gt;ed file gets the right type assigned. So it must be something with systemd…&lt;/p&gt;
&lt;p&gt;A bit of poking (and good guesswork based on prior experience with a similar issue in Puppet: &lt;a href="https://tickets.puppetlabs.com/browse/PUP-2169"&gt;PUP-2169&lt;/a&gt; and &lt;a href="https://tickets.puppetlabs.com/browse/PUP-10548"&gt;PUP-10548&lt;/a&gt;) led to the realization that a &lt;code&gt;systemctl daemon-reexec&lt;/code&gt; after adding the file context rule "fixes" the issue. Moving the poking to Google, you quickly end up at &lt;a href="https://github.com/systemd/systemd/issues/9997"&gt;systemd issue #9997&lt;/a&gt; which is fixed in v245, but that's in no EL release yet. And indeed, the issue seems fixed on my Fedora 33 with systemd 246, but I still need it to work on CentOS 7 and 8…&lt;/p&gt;
&lt;p&gt;Well, maybe that &lt;a href="https://github.com/theforeman/foreman-packaging/commit/92a8ffc2ee94bab801e39460c878e220ff2fe367"&gt;reexec isn't that bad after all&lt;/a&gt;? At least the socket is now properly labeled and httpd can connect to it on CentOS 8.&lt;/p&gt;
&lt;p&gt;Btw, no idea why the connection worked on CentOS 7, as there the socket was also wrongly labeled, but SELinux didn't deny httpd to open it.&lt;/p&gt;
&lt;p&gt;Big shout out to &lt;a href="https://lukas.zapletalovi.com/"&gt;lzap&lt;/a&gt; and &lt;a href="https://github.com/ekohl/"&gt;ewoud&lt;/a&gt; for helping me with this beast!&lt;/p&gt;</description><category>english</category><category>foreman</category><category>linux</category><category>planet-debian</category><category>selinux</category><category>software</category><category>systemd</category><guid>https://www.die-welt.net/2020/12/systemd-%2B-selinux/</guid><pubDate>Fri, 11 Dec 2020 18:11:30 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>