<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title>ibizaman's Blog</title>
    <link href="https://blog.tiserbox.com//atom.xml" rel="self" />
    <link href="https://blog.tiserbox.com/" />
    <id>https://blog.tiserbox.com//atom.xml</id>
    <author>
        <name>ibizaman</name>
        <email>blog@pierre.tiserbox.com</email>
    </author>
    <updated>2026-09-01T00:00:00Z</updated>
    <entry>
    <title>Connect Home Assistant to Nextcloud Calendar</title>
    <link href="https://blog.tiserbox.com//posts/2026-09-01-connect-home-assistant-to-nextcloud-calendar.html" />
    <id>https://blog.tiserbox.com//posts/2026-09-01-connect-home-assistant-to-nextcloud-calendar.html</id>
    <published>2026-09-01T00:00:00Z</published>
    <updated>2026-09-01T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on September  1, 2026
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;home-assistant&#39;." href="/tags/home-assistant.html">home-assistant</a>, <a title="All pages tagged &#39;nextcloud&#39;." href="/tags/nextcloud.html">nextcloud</a>
	  
	</section>
    <section>
        <p>Home-Assistant can connect to “subscription calendars” and you can then use those events to trigger automations.</p>
<p>I wanted to connect HA to my Nextcloud server so I could use my personal calendar this way too.
But it wasn’t that obvious how to do it.
So here are the step by step instructions.</p>
<h2 id="locate-calendar-in-nextcloud">Locate calendar in Nextcloud</h2>
<p>First, pick one calendar to share and click on its “Edit and share calendar”:</p>
<figure>
<img src="/images/2026-09-01-connect-home-assistant-to-nextcloud-calendar/Edit%20link.png" alt="Edit and share calendar button is focused for the calendar named Personal" />
<figcaption aria-hidden="true">Edit and share calendar button is focused for the calendar named Personal</figcaption>
</figure>
<p>Then click on the three dots next to the “Share link” row
and click on “Copy subscription link”:</p>
<figure>
<img src="/images/2026-09-01-connect-home-assistant-to-nextcloud-calendar/Share%20modal.png" alt="Copy subscription link is focused for the calendar named Personal" />
<figcaption aria-hidden="true">Copy subscription link is focused for the calendar named Personal</figcaption>
</figure>
<h2 id="add-calendar-to-home-assistant">Add calendar to Home Assistant</h2>
<p>On the Home Assistant side, add the Remote Calendar integration:</p>
<figure>
<img src="/images/2026-09-01-connect-home-assistant-to-nextcloud-calendar/HA%20add%20integration.png" alt="We add the Remote Calendar integration to Home Assistant" />
<figcaption aria-hidden="true">We add the Remote Calendar integration to Home Assistant</figcaption>
</figure>
<p>Give a name to the calendar and copy the subscription link. Then click done.</p>
<figure>
<img src="/images/2026-09-01-connect-home-assistant-to-nextcloud-calendar/HA%20add%20calendar.png" alt="We add the calendar named Personal to Home Assistant" />
<figcaption aria-hidden="true">We add the calendar named Personal to Home Assistant</figcaption>
</figure>
<h2 id="conclusion">Conclusion</h2>
<p>And that’s it! Now you can see your Nextcloud calendar from Home Assistant.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Testing LDAP and SSO Integrations in NixOS with Playwright</title>
    <link href="https://blog.tiserbox.com//posts/2026-08-22-testing-ldap-and-sso-integrations-in-nix-os-with-playwright.html" />
    <id>https://blog.tiserbox.com//posts/2026-08-22-testing-ldap-and-sso-integrations-in-nix-os-with-playwright.html</id>
    <published>2026-08-22T00:00:00Z</published>
    <updated>2026-08-22T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on August 22, 2026
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>, <a title="All pages tagged &#39;tests&#39;." href="/tags/tests.html">tests</a>, <a title="All pages tagged &#39;playwright&#39;." href="/tags/playwright.html">playwright</a>, <a title="All pages tagged &#39;ldap&#39;." href="/tags/ldap.html">ldap</a>, <a title="All pages tagged &#39;sso&#39;." href="/tags/sso.html">sso</a>, <a title="All pages tagged &#39;selfhostblocks&#39;." href="/tags/selfhostblocks.html">selfhostblocks</a>
	  
	</section>
    <section>
        <p>There is one thing I hate the most in the DevOps world. It’s having the ground shift under my feet.</p>
<p>I take a while to implement some integration but it’s done and I’m happy. Then 6 months later, something changes, some default is different, I don’t notice it and I deploy. This breaks my carefully crafted integration and I only realize that 2 weeks later or if a user notices it. And now I need to spelunk and figure what changed caused this and fix it, all that in a hurry.</p>
<p><strong>This blog article is about how I pushed NixOS VM tests to the max to fix this once and for all.</strong></p>
<h2 id="automated-updates">Automated Updates</h2>
<p>In my project SelfHostBlocks, I add a layer on top of services like Nextcloud, Home Assistant, Jellyfin, Immich, etc. that sets up a lot of features declaratively. One particularly rare feature I’ve seen in other projects is integrating those services with an LDAP provider (LLDAP) and an SSO provider (Authelia) completely self-hosted and declaratively.</p>
<p>I also wanted to provide automated updates. A cron job would open a PR after running <code>nix flake update nixpkgs</code> and if the tests succeed, it would be merged automatically. But… it was hard to get the integrations right, it’s quite the balancing act. Automating the updates like this would always lead to issues cropping up in unexpected ways. Even just manually updating was letting some slip through!</p>
<p>After updating <code>nixpkgs</code>, there are 4 possible scenarios:</p>
<ol type="1">
<li>Everything works fine. Yeah 🎉</li>
<li>The evaluation is failing. This usually means a NixOS module option changed. The fix is to update how I use it.</li>
<li>The build is failing. This means something in the package built or its dependencies broke. The fix is usually to pick another <code>nixpkgs</code> commit. I attempted to fix this previously, see my <a href="https://blog.tiserbox.com/posts/2026-05-12-bisect-experiment-at-the-pr-level.html">other blog post</a>.</li>
<li>The integration fails subtly. The fix is… to test the integration. So let’s see what we can do.</li>
</ol>
<h2 id="nixos-vm-tests">NixOS VM Tests</h2>
<p>NixOS VM tests are a wonderful piece of technology. You can spin up one or more VMs that talk together, write some NixOS config to start whatever service you want in those, just like you would deploy on your server, and then write some python code to exercise some properties you want to test.</p>
<p>First, let’s see a minimal NixOS VM test.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span> <span class="va">pkgs</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>:</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>pkgs.testers.runNixOSTest <span class="op">{</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">name</span> <span class="op">=</span> <span class="st">&quot;jellyfin&quot;</span><span class="op">;</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>  <span class="va">nodes</span>.<span class="va">machine</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>    <span class="va">services</span>.<span class="va">jellyfin</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>      <span class="va">enable</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>  <span class="va">testScript</span> <span class="op">=</span> <span class="st">&#39;&#39;</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a><span class="st">    machine.start()</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="st">    machine.wait_for_unit(&quot;jellyfin.service&quot;)</span></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a><span class="st">    machine.wait_for_open_port(9096)</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a><span class="st">    machine.wait_until_succed(&quot;curl --fail http://localhost:9096/login&quot;)</span></span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a><span class="st">  &#39;&#39;</span><span class="op">;</span></span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>This test is named <code>jellyfin</code>, it creates one VM called <code>machine</code> and launches the <code>jellyfin</code> service with <code>services.jellyfin.enable=true</code>. The <code>testScript</code> is Python code, here it starts the machine, waits for the <code>jellyfin</code> systemd service to start and the port <code>9096</code> to be open and finally.</p>
<blockquote>
<p>For more info, you can check the <a href="https://search.nixos.org/options?channel=unstable&amp;include_modular_service_options=1&amp;include_nixos_options=1&amp;sort=alpha_asc&amp;query=services.jellyfin"><code>services.jellyfin</code> NixOS module options</a> and the <a href="https://nixos.org/manual/nixos/stable/#sec-nixos-tests">NixOS VM test manual</a> which is a complete reference on what you can do and what Python function you can use. Or just look at all the existing tests in <a href="https://github.com/NixOS/nixpkgs/tree/master/nixos/tests">the nixpkgs repository</a>.</p>
</blockquote>
<p>This looks quite simple in the end but it’s extremely powerful because it does three things:</p>
<ol type="1">
<li>It builds jellyfin (or pulls from the cache) which makes sure the package can be built.</li>
<li>It evaluates the jellyfin NixOS module, making sure the options are still used correctly.</li>
<li>It runs the jellyfin service and runs some smoke tests making sure it starts successfully.</li>
</ol>
<p>Isn’t this magically solving all the problems we had with automated updates? Well yes it is!</p>
<p>Where this minimal test is too minimal is it does not test the LDAP or SSO integration. So let’s add that.</p>
<h2 id="playwright-tests">Playwright Tests</h2>
<p>One way of testing the LDAP or SSO integration would be to make some <code>curl</code> requests here and there and poke around in the background. I tried this at first but stopped quickly because discovering those <code>curl</code> requests was hard and anyway, I wasn’t really testing the integrations this way. What I needed was to do some black box testing by opening up a browser, navigating to the service URL, filling out the login form and then see I was correctly logged in the web UI.</p>
<p>That’s where Playwright comes in. It’s a program that can automate actions in a web UI. Setting it up in the NixOS VM test was not obvious, but had to be done only once. In the following snippet, we create a NixOS module that a NixOS VM test can import.</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="co"># clientLoginModule.nix</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="op">{</span> <span class="va">config</span><span class="op">,</span> <span class="va">pkgs</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>:</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">cfg</span> <span class="op">=</span> config.test.login<span class="op">;</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="kw">in</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>  <span class="va">options</span>.<span class="va">test</span>.<span class="va">login</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>    <span class="va">browser</span> <span class="op">=</span> mkOption <span class="op">{</span></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a>      <span class="va">type</span> <span class="op">=</span> enum <span class="op">[</span></span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;firefox&quot;</span></span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;chromium&quot;</span></span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;webkit&quot;</span></span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a>      <span class="op">];</span></span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a>      <span class="va">default</span> <span class="op">=</span> <span class="st">&quot;firefox&quot;</span><span class="op">;</span></span>
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb2-17"><a href="#cb2-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-18"><a href="#cb2-18" aria-hidden="true" tabindex="-1"></a>  <span class="va">config</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb2-19"><a href="#cb2-19" aria-hidden="true" tabindex="-1"></a>    <span class="va">environment</span>.<span class="va">variables</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb2-20"><a href="#cb2-20" aria-hidden="true" tabindex="-1"></a>      <span class="va">PLAYWRIGHT_BROWSERS_PATH</span> <span class="op">=</span> pkgs.playwright<span class="op">-</span>driver.browsers<span class="op">;</span></span>
<span id="cb2-21"><a href="#cb2-21" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb2-22"><a href="#cb2-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-23"><a href="#cb2-23" aria-hidden="true" tabindex="-1"></a>    <span class="va">environment</span>.<span class="va">systemPackages</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb2-24"><a href="#cb2-24" aria-hidden="true" tabindex="-1"></a>      <span class="op">(</span>pkgs.writers.writePython3Bin <span class="st">&quot;login_playwright&quot;</span></span>
<span id="cb2-25"><a href="#cb2-25" aria-hidden="true" tabindex="-1"></a>        <span class="op">{</span></span>
<span id="cb2-26"><a href="#cb2-26" aria-hidden="true" tabindex="-1"></a>          <span class="va">libraries</span> <span class="op">=</span> <span class="op">[</span> pkgs.python3Packages.playwright <span class="op">];</span></span>
<span id="cb2-27"><a href="#cb2-27" aria-hidden="true" tabindex="-1"></a>          <span class="va">flakeIgnore</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb2-28"><a href="#cb2-28" aria-hidden="true" tabindex="-1"></a>            <span class="st">&quot;F401&quot;</span></span>
<span id="cb2-29"><a href="#cb2-29" aria-hidden="true" tabindex="-1"></a>            <span class="st">&quot;E501&quot;</span></span>
<span id="cb2-30"><a href="#cb2-30" aria-hidden="true" tabindex="-1"></a>          <span class="op">];</span></span>
<span id="cb2-31"><a href="#cb2-31" aria-hidden="true" tabindex="-1"></a>        <span class="op">}</span></span>
<span id="cb2-32"><a href="#cb2-32" aria-hidden="true" tabindex="-1"></a>        <span class="op">(</span></span>
<span id="cb2-33"><a href="#cb2-33" aria-hidden="true" tabindex="-1"></a>          <span class="kw">let</span></span>
<span id="cb2-34"><a href="#cb2-34" aria-hidden="true" tabindex="-1"></a>            <span class="va">testCfg</span> <span class="op">=</span> pkgs.writeText <span class="st">&quot;users.json&quot;</span> <span class="op">(</span><span class="bu">builtins</span>.toJSON cfg<span class="op">);</span></span>
<span id="cb2-35"><a href="#cb2-35" aria-hidden="true" tabindex="-1"></a>          <span class="kw">in</span></span>
<span id="cb2-36"><a href="#cb2-36" aria-hidden="true" tabindex="-1"></a>          <span class="st">&#39;&#39;</span></span>
<span id="cb2-37"><a href="#cb2-37" aria-hidden="true" tabindex="-1"></a><span class="st">            import json</span></span>
<span id="cb2-38"><a href="#cb2-38" aria-hidden="true" tabindex="-1"></a><span class="st">            from playwright.sync_api import sync_playwright</span></span>
<span id="cb2-39"><a href="#cb2-39" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-40"><a href="#cb2-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-41"><a href="#cb2-41" aria-hidden="true" tabindex="-1"></a><span class="st">            def test(page):</span></span>
<span id="cb2-42"><a href="#cb2-42" aria-hidden="true" tabindex="-1"></a><span class="st">                # Actual test goes here</span></span>
<span id="cb2-43"><a href="#cb2-43" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-44"><a href="#cb2-44" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-45"><a href="#cb2-45" aria-hidden="true" tabindex="-1"></a><span class="st">            browsers = {</span></span>
<span id="cb2-46"><a href="#cb2-46" aria-hidden="true" tabindex="-1"></a><span class="st">                &quot;chromium&quot;: {&#39;args&#39;: [&quot;--headless&quot;, &quot;--disable-gpu&quot;], &#39;channel&#39;: &#39;chromium&#39;},</span></span>
<span id="cb2-47"><a href="#cb2-47" aria-hidden="true" tabindex="-1"></a><span class="st">                &quot;firefox&quot;: {&#39;args&#39;: [&quot;--reporter&quot;, &quot;html&quot;]},</span></span>
<span id="cb2-48"><a href="#cb2-48" aria-hidden="true" tabindex="-1"></a><span class="st">                &quot;webkit&quot;: {},</span></span>
<span id="cb2-49"><a href="#cb2-49" aria-hidden="true" tabindex="-1"></a><span class="st">            }</span></span>
<span id="cb2-50"><a href="#cb2-50" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-51"><a href="#cb2-51" aria-hidden="true" tabindex="-1"></a><span class="st">            with open(&quot;</span><span class="sc">${</span>testCfg<span class="sc">}</span><span class="st">&quot;) as f:</span></span>
<span id="cb2-52"><a href="#cb2-52" aria-hidden="true" tabindex="-1"></a><span class="st">                testCfg = json.load(f)</span></span>
<span id="cb2-53"><a href="#cb2-53" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-54"><a href="#cb2-54" aria-hidden="true" tabindex="-1"></a><span class="st">            browser_name = testCfg[&#39;browser&#39;]</span></span>
<span id="cb2-55"><a href="#cb2-55" aria-hidden="true" tabindex="-1"></a><span class="st">            browser_args = browsers.get(browser_name)</span></span>
<span id="cb2-56"><a href="#cb2-56" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-57"><a href="#cb2-57" aria-hidden="true" tabindex="-1"></a><span class="st">            with sync_playwright() as p:</span></span>
<span id="cb2-58"><a href="#cb2-58" aria-hidden="true" tabindex="-1"></a><span class="st">                browser = getattr(p, browser_name).launch(**browser_args)</span></span>
<span id="cb2-59"><a href="#cb2-59" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-60"><a href="#cb2-60" aria-hidden="true" tabindex="-1"></a><span class="st">                context = browser.new_context(ignore_https_errors=True)</span></span>
<span id="cb2-61"><a href="#cb2-61" aria-hidden="true" tabindex="-1"></a><span class="st">                context.set_default_navigation_timeout(2 * 60 * 1000)</span></span>
<span id="cb2-62"><a href="#cb2-62" aria-hidden="true" tabindex="-1"></a><span class="st">                context.tracing.start(screenshots=True, snapshots=True, sources=True)</span></span>
<span id="cb2-63"><a href="#cb2-63" aria-hidden="true" tabindex="-1"></a><span class="st">                try:</span></span>
<span id="cb2-64"><a href="#cb2-64" aria-hidden="true" tabindex="-1"></a><span class="st">                    page = context.new_page()</span></span>
<span id="cb2-65"><a href="#cb2-65" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-66"><a href="#cb2-66" aria-hidden="true" tabindex="-1"></a><span class="st">                    test(page)</span></span>
<span id="cb2-67"><a href="#cb2-67" aria-hidden="true" tabindex="-1"></a><span class="st">                finally:</span></span>
<span id="cb2-68"><a href="#cb2-68" aria-hidden="true" tabindex="-1"></a><span class="st">                    context.tracing.stop(path=f&quot;trace/{i}.zip&quot;)</span></span>
<span id="cb2-69"><a href="#cb2-69" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-70"><a href="#cb2-70" aria-hidden="true" tabindex="-1"></a><span class="st">                browser.close()</span></span>
<span id="cb2-71"><a href="#cb2-71" aria-hidden="true" tabindex="-1"></a><span class="st">          &#39;&#39;</span></span>
<span id="cb2-72"><a href="#cb2-72" aria-hidden="true" tabindex="-1"></a>        <span class="op">)</span></span>
<span id="cb2-73"><a href="#cb2-73" aria-hidden="true" tabindex="-1"></a>      <span class="op">)</span></span>
<span id="cb2-74"><a href="#cb2-74" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb2-75"><a href="#cb2-75" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb2-76"><a href="#cb2-76" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p><a name="test-function-snippet"></a>
This big snippet only sets up the scaffolding. The actual test would go in <code># Test goes here</code>. A few things to note:</p>
<ul>
<li>We create a <code>test.login.browser</code> option to choose which browser to test with.</li>
<li>We set <code>PLAYWRIGHT_BROWSERS_PATH = pkgs.playwright-driver.browsers;</code> so the <code>playwright</code> executable can find NixOS provided browsers.</li>
<li>We ignore some python linting issues. I explicitly allowed for long lines because dealing with those for a test is annoying.</li>
<li>We create a <code>login_playwright</code> Python executable that launches the Playwright test. It will be called from the test’s <code>testScript</code>.</li>
<li>We pass the whole <code>test.login</code> config to the Python script by the intermediary of a json file. This makes it easy to pass any kind of values and to add options later on.</li>
<li>We enable taking a full trace and write that trace. It will then be available in the build output.</li>
</ul>
<p>Using the module looks like so:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span> <span class="va">pkgs</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>:</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">clientLoginModule</span> <span class="op">=</span> <span class="bu">import</span> clientLoginModule.nix<span class="op">;</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="kw">in</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>pkgs.testers.runNixOSTest <span class="op">{</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>  <span class="va">name</span> <span class="op">=</span> <span class="st">&quot;jellyfin&quot;</span><span class="op">;</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>  <span class="va">nodes</span>.<span class="va">server</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>    <span class="va">services</span>.<span class="va">jellyfin</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>      <span class="va">enable</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>  <span class="va">node</span>.<span class="va">client</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>    <span class="va">imports</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>      clientLoginModule</span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a>  <span class="va">testScript</span> <span class="op">=</span> <span class="st">&#39;&#39;</span></span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a><span class="st">    machine.start()</span></span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a><span class="st">    machine.wait_for_unit(&quot;jellyinf.service&quot;)</span></span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a><span class="st">    machine.wait_for_open_port(9096)</span></span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a><span class="st">    machine.wait_until_succed(&quot;curl --fail http://localhost:9096/login&quot;)</span></span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a><span class="st">    with subtest(&quot;Login from client&quot;):</span></span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a><span class="st">        code, logs = client.execute(&quot;login_playwright&quot;)</span></span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a><span class="st">        print(logs)</span></span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a><span class="st">        try:</span></span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a><span class="st">            client.succeed(&quot;&quot;&quot;</span></span>
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a><span class="st">              mkdir -p /tmp/shared/</span></span>
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a><span class="st">              cp -r trace /tmp/shared/</span></span>
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a><span class="st">            &quot;&quot;&quot;)</span></span>
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a><span class="st">            client.copy_from_machine(&quot;trace&quot;)</span></span>
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a><span class="st">        except:</span></span>
<span id="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a><span class="st">            print(&quot;No trace found on client&quot;)</span></span>
<span id="cb3-37"><a href="#cb3-37" aria-hidden="true" tabindex="-1"></a><span class="st">        if code != 0:</span></span>
<span id="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a><span class="st">            raise Exception(&quot;login_playwright did not succeed&quot;)</span></span>
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a><span class="st">  &#39;&#39;</span><span class="op">;</span></span>
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>Here we create two nodes, two VMs, one called server with the jellyfin service and the second called client from which we will exercise the browser tests. The idea is to make the test as accurate as possible by making sure a remote node can access the service. Also, we take care of copying the traces from the client VM to the build output.</p>
<h2 id="declarative-ldap">Declarative LDAP</h2>
<p>Before being able to write the test, we must figure out how to create the LDAP and SSO config declaratively.</p>
<p>For the LDAP provider, I chose <a href="https://github.com/lldap/lldap">LLDAP</a> for its nice web UI. It did provide a <a href="https://github.com/lldap/lldap/blob/main/scripts/bootstrap.sh">bootstrap script</a> but this one was not wired into the NixOS module system. After adding support for that in my project (<a href="https://shb.skarabox.com/blocks-lldap.html#blocks-lldap-manage-groups">link to manual</a>) and upstreaming a few fixes (<a href="https://github.com/lldap/lldap/pulls?q=is%3Apr+author%3Aibizaman">link to PRs</a> if you’re curious) we can now enable those services in our test and create some users declaratively.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="co"># ldapModule.nix</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">networking</span>.<span class="va">hosts</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;</span>127.0.0.1<span class="st">&quot;</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;</span><span class="sc">${</span>config.shb.lldap.subdomain<span class="sc">}</span><span class="st">.</span><span class="sc">${</span>config.shb.lldap.domain<span class="sc">}</span><span class="st">&quot;</span> <span class="op">];</span></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>  <span class="va">shb</span>.<span class="va">lldap</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>    <span class="va">enable</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a>    <span class="va">domain</span> <span class="op">=</span> <span class="st">&quot;example.com&quot;</span><span class="op">;</span></span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a>    <span class="va">subdomain</span> <span class="op">=</span> <span class="st">&quot;ldap&quot;</span><span class="op">;</span></span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a>    <span class="va">ldapPort</span> <span class="op">=</span> <span class="dv">3890</span><span class="op">;</span></span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a>    <span class="va">webUIListenPort</span> <span class="op">=</span> <span class="dv">17170</span><span class="op">;</span></span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a>    <span class="va">dcdomain</span> <span class="op">=</span> <span class="st">&quot;dc=example,dc=com&quot;</span><span class="op">;</span></span>
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a>    <span class="va">ldapUserPassword</span>.<span class="va">result</span> <span class="op">=</span> config.shb.hardcodedsecret.ldapAdminPassword.result<span class="op">;</span></span>
<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a>    <span class="va">jwtSecret</span>.<span class="va">result</span> <span class="op">=</span> config.shb.hardcodedsecret.jwtSecret.result<span class="op">;</span></span>
<span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-18"><a href="#cb4-18" aria-hidden="true" tabindex="-1"></a>  <span class="va">shb</span>.<span class="va">hardcodedsecret</span>.<span class="va">ldapAdminPassword</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb4-19"><a href="#cb4-19" aria-hidden="true" tabindex="-1"></a>    <span class="va">request</span> <span class="op">=</span> config.shb.lldap.ldapUserPassword.request<span class="op">;</span></span>
<span id="cb4-20"><a href="#cb4-20" aria-hidden="true" tabindex="-1"></a>    <span class="va">settings</span>.<span class="va">content</span> <span class="op">=</span> <span class="st">&quot;ldapAdminPassword&quot;</span><span class="op">;</span></span>
<span id="cb4-21"><a href="#cb4-21" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb4-22"><a href="#cb4-22" aria-hidden="true" tabindex="-1"></a>  <span class="va">shb</span>.<span class="va">hardcodedsecret</span>.<span class="va">jwtSecret</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb4-23"><a href="#cb4-23" aria-hidden="true" tabindex="-1"></a>    <span class="va">request</span> <span class="op">=</span> config.shb.lldap.jwtSecret.request<span class="op">;</span></span>
<span id="cb4-24"><a href="#cb4-24" aria-hidden="true" tabindex="-1"></a>    <span class="va">settings</span>.<span class="va">content</span> <span class="op">=</span> <span class="st">&quot;jwtSecret&quot;</span><span class="op">;</span></span>
<span id="cb4-25"><a href="#cb4-25" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb4-26"><a href="#cb4-26" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-27"><a href="#cb4-27" aria-hidden="true" tabindex="-1"></a>  <span class="va">shb</span>.<span class="va">lldap</span>.<span class="va">ensureUsers</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb4-28"><a href="#cb4-28" aria-hidden="true" tabindex="-1"></a>    <span class="va">alice</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb4-29"><a href="#cb4-29" aria-hidden="true" tabindex="-1"></a>      <span class="va">email</span> <span class="op">=</span> <span class="st">&quot;alice@example.com&quot;</span><span class="op">;</span></span>
<span id="cb4-30"><a href="#cb4-30" aria-hidden="true" tabindex="-1"></a>      <span class="va">displayName</span> <span class="op">=</span> <span class="st">&quot;Alice Alice&quot;</span><span class="op">;</span></span>
<span id="cb4-31"><a href="#cb4-31" aria-hidden="true" tabindex="-1"></a>      <span class="va">groups</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;user_group&quot;</span> <span class="op">];</span></span>
<span id="cb4-32"><a href="#cb4-32" aria-hidden="true" tabindex="-1"></a>      <span class="va">password</span>.<span class="va">result</span> <span class="op">=</span> config.shb.hardcodedsecret.alice.result<span class="op">;</span></span>
<span id="cb4-33"><a href="#cb4-33" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb4-34"><a href="#cb4-34" aria-hidden="true" tabindex="-1"></a>    <span class="va">bob</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb4-35"><a href="#cb4-35" aria-hidden="true" tabindex="-1"></a>      <span class="va">email</span> <span class="op">=</span> <span class="st">&quot;bob@example.com&quot;</span><span class="op">;</span></span>
<span id="cb4-36"><a href="#cb4-36" aria-hidden="true" tabindex="-1"></a>      <span class="va">displayName</span> <span class="op">=</span> <span class="st">&quot;Bob Bob&quot;</span><span class="op">;</span></span>
<span id="cb4-37"><a href="#cb4-37" aria-hidden="true" tabindex="-1"></a>      <span class="va">groups</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;admin_group&quot;</span> <span class="op">];</span></span>
<span id="cb4-38"><a href="#cb4-38" aria-hidden="true" tabindex="-1"></a>      <span class="va">password</span>.<span class="va">result</span> <span class="op">=</span> config.shb.hardcodedsecret.bob.result<span class="op">;</span></span>
<span id="cb4-39"><a href="#cb4-39" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb4-40"><a href="#cb4-40" aria-hidden="true" tabindex="-1"></a>    <span class="va">charlie</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb4-41"><a href="#cb4-41" aria-hidden="true" tabindex="-1"></a>      <span class="va">email</span> <span class="op">=</span> <span class="st">&quot;charlie@example.com&quot;</span><span class="op">;</span></span>
<span id="cb4-42"><a href="#cb4-42" aria-hidden="true" tabindex="-1"></a>      <span class="va">displayName</span> <span class="op">=</span> <span class="st">&quot;Charlie Charlie&quot;</span><span class="op">;</span></span>
<span id="cb4-43"><a href="#cb4-43" aria-hidden="true" tabindex="-1"></a>      <span class="va">groups</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;other_group&quot;</span> <span class="op">];</span></span>
<span id="cb4-44"><a href="#cb4-44" aria-hidden="true" tabindex="-1"></a>      <span class="va">password</span>.<span class="va">result</span> <span class="op">=</span> config.shb.hardcodedsecret.charlie.result<span class="op">;</span></span>
<span id="cb4-45"><a href="#cb4-45" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb4-46"><a href="#cb4-46" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb4-47"><a href="#cb4-47" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-48"><a href="#cb4-48" aria-hidden="true" tabindex="-1"></a>  <span class="va">shb</span>.<span class="va">hardcodedsecret</span>.<span class="va">alice</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb4-49"><a href="#cb4-49" aria-hidden="true" tabindex="-1"></a>    <span class="va">request</span> <span class="op">=</span> config.shb.lldap.ensureUsers.alice.password.request<span class="op">;</span></span>
<span id="cb4-50"><a href="#cb4-50" aria-hidden="true" tabindex="-1"></a>    <span class="va">settings</span>.<span class="va">content</span> <span class="op">=</span> <span class="st">&quot;AlicePassword&quot;</span><span class="op">;</span></span>
<span id="cb4-51"><a href="#cb4-51" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb4-52"><a href="#cb4-52" aria-hidden="true" tabindex="-1"></a>  <span class="va">shb</span>.<span class="va">hardcodedsecret</span>.<span class="va">bob</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb4-53"><a href="#cb4-53" aria-hidden="true" tabindex="-1"></a>    <span class="va">request</span> <span class="op">=</span> config.shb.lldap.ensureUsers.bob.password.request<span class="op">;</span></span>
<span id="cb4-54"><a href="#cb4-54" aria-hidden="true" tabindex="-1"></a>    <span class="va">settings</span>.<span class="va">content</span> <span class="op">=</span> <span class="st">&quot;BobPassword&quot;</span><span class="op">;</span></span>
<span id="cb4-55"><a href="#cb4-55" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb4-56"><a href="#cb4-56" aria-hidden="true" tabindex="-1"></a>  <span class="va">shb</span>.<span class="va">hardcodedsecret</span>.<span class="va">charlie</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb4-57"><a href="#cb4-57" aria-hidden="true" tabindex="-1"></a>    <span class="va">request</span> <span class="op">=</span> config.shb.lldap.ensureUsers.charlie.password.request<span class="op">;</span></span>
<span id="cb4-58"><a href="#cb4-58" aria-hidden="true" tabindex="-1"></a>    <span class="va">settings</span>.<span class="va">content</span> <span class="op">=</span> <span class="st">&quot;CharliePassword&quot;</span><span class="op">;</span></span>
<span id="cb4-59"><a href="#cb4-59" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb4-60"><a href="#cb4-60" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-61"><a href="#cb4-61" aria-hidden="true" tabindex="-1"></a>  <span class="va">shb</span>.<span class="va">lldap</span>.<span class="va">ensureGroups</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb4-62"><a href="#cb4-62" aria-hidden="true" tabindex="-1"></a>    <span class="va">user_group</span> <span class="op">=</span> <span class="op">{</span> <span class="op">};</span></span>
<span id="cb4-63"><a href="#cb4-63" aria-hidden="true" tabindex="-1"></a>    <span class="va">admin_group</span> <span class="op">=</span> <span class="op">{</span> <span class="op">};</span></span>
<span id="cb4-64"><a href="#cb4-64" aria-hidden="true" tabindex="-1"></a>    <span class="va">other_group</span> <span class="op">=</span> <span class="op">{</span> <span class="op">};</span></span>
<span id="cb4-65"><a href="#cb4-65" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb4-66"><a href="#cb4-66" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<blockquote>
<p>We do use hard-coded secrets here in the tests as that is good enough for testing. The <code>shb.hardcoded</code> module makes sure the file containing the secret has the correct Unix user, group and mode thanks to the <code>request</code> option. So although it hard-codes the secret, it at least makes sure permissions are correct.</p>
</blockquote>
<p>The service will be accessible at <code>ldap.example.com</code> and we make it so requests to that fqdn goes to <code>127.0.0.1</code>. We also create 3 users:</p>
<ul>
<li>Alice has password <code>AlicePassword</code> and belongs to the LDAP <code>user_group</code> group which grants normal access to the service we will test.</li>
<li>Bob has password <code>BobPassword</code> and belongs to the LDAP <code>admin_group</code> group which grants admin privileges to the service we will test.</li>
<li>Charlie has password <code>CharliePassword</code> and belongs to some other group that do not grant access to the service we will test.</li>
</ul>
<p>Those 3 users allow us to test most of what the upstream services allow with their LDAP and SSO integration. We also verify with Charlie that a user not part of the authorized groups cannot login into the service.</p>
<h2 id="declarative-sso">Declarative SSO</h2>
<p>I chose <a href="https://github.com/authelia/authelia/">Authelia</a> as the SSO provider, which hopefully is declarative by default.</p>
<p>One thing to note with Authelia is it refuses to work without HTTPS. So we need to set that up using self-signed certificates.</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">shb</span>.<span class="va">certs</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>    <span class="va">cas</span>.<span class="va">selfsigned</span>.<span class="va">myca</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>      <span class="va">name</span> <span class="op">=</span> <span class="st">&quot;My CA&quot;</span><span class="op">;</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>    <span class="va">certs</span>.<span class="va">selfsigned</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a>      <span class="st">&quot;</span>example.com<span class="st">&quot;</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a>        <span class="va">ca</span> <span class="op">=</span> config.shb.certs.cas.selfsigned.myca<span class="op">;</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a>        <span class="va">domain</span> <span class="op">=</span> <span class="st">&quot;*.example.com&quot;</span><span class="op">;</span></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a>        <span class="va">group</span> <span class="op">=</span> <span class="st">&quot;nginx&quot;</span><span class="op">;</span></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a>      <span class="op">};</span></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a>  <span class="va">networking</span>.<span class="va">hosts</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;</span>127.0.0.1<span class="st">&quot;</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;</span><span class="sc">${</span>config.shb.authelia.subdomain<span class="sc">}</span><span class="st">.</span><span class="sc">${</span>config.shb.authelia.domain<span class="sc">}</span><span class="st">&quot;</span> <span class="op">];</span></span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a>  <span class="va">shb</span>.<span class="va">authelia</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a>    <span class="va">enable</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a>    <span class="va">domain</span> <span class="op">=</span> <span class="st">&quot;example.com&quot;</span><span class="op">;</span></span>
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a>    <span class="va">subdomain</span> <span class="op">=</span> <span class="st">&quot;auth&quot;</span><span class="op">;</span></span>
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a>    <span class="va">ssl</span> <span class="op">=</span> config.shb.certs.certs.selfsigned.<span class="st">&quot;example.com&quot;</span><span class="op">;</span></span>
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a>    <span class="va">ldapHostname</span> <span class="op">=</span> <span class="st">&quot;127.0.0.1&quot;</span><span class="op">;</span></span>
<span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a>    <span class="va">ldapPort</span> <span class="op">=</span> config.shb.lldap.ldapPort<span class="op">;</span></span>
<span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a>    <span class="va">dcdomain</span> <span class="op">=</span> config.shb.lldap.dcdomain<span class="op">;</span></span>
<span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a>    <span class="va">secrets</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb5-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a>      <span class="va">jwtSecret</span>.<span class="va">result</span> <span class="op">=</span> config.shb.hardcodedsecret.autheliaJwtSecret.result<span class="op">;</span></span>
<span id="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a>      <span class="va">ldapAdminPassword</span>.<span class="va">result</span> <span class="op">=</span> config.shb.hardcodedsecret.ldapAdminPassword.result<span class="op">;</span></span>
<span id="cb5-32"><a href="#cb5-32" aria-hidden="true" tabindex="-1"></a>      <span class="va">sessionSecret</span>.<span class="va">result</span> <span class="op">=</span> config.shb.hardcodedsecret.sessionSecret.result<span class="op">;</span></span>
<span id="cb5-33"><a href="#cb5-33" aria-hidden="true" tabindex="-1"></a>      <span class="va">storageEncryptionKey</span>.<span class="va">result</span> <span class="op">=</span> config.shb.hardcodedsecret.storageEncryptionKey.result<span class="op">;</span></span>
<span id="cb5-34"><a href="#cb5-34" aria-hidden="true" tabindex="-1"></a>      <span class="va">identityProvidersOIDCHMACSecret</span>.<span class="va">result</span> <span class="op">=</span></span>
<span id="cb5-35"><a href="#cb5-35" aria-hidden="true" tabindex="-1"></a>        config.shb.hardcodedsecret.identityProvidersOIDCHMACSecret.result<span class="op">;</span></span>
<span id="cb5-36"><a href="#cb5-36" aria-hidden="true" tabindex="-1"></a>      <span class="va">identityProvidersOIDCIssuerPrivateKey</span>.<span class="va">result</span> <span class="op">=</span></span>
<span id="cb5-37"><a href="#cb5-37" aria-hidden="true" tabindex="-1"></a>        config.shb.hardcodedsecret.identityProvidersOIDCIssuerPrivateKey.result<span class="op">;</span></span>
<span id="cb5-38"><a href="#cb5-38" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb5-39"><a href="#cb5-39" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb5-40"><a href="#cb5-40" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>I left out all the <code>shb.hardodedsecret</code> assignments from the above SSO snippet this time because they just clutter the view.</p>
<p>The service will be accessible at <code>auth.example.com</code> and we make it so requests to that fqdn goes to <code>127.0.0.1</code>. We also setup self-signed certificates for that domain using <a href="https://shb.skarabox.com/blocks-ssl.html">another one</a> of my NixOS module that hides that complexity.</p>
<h2 id="declarative-module-integration">Declarative Module Integration</h2>
<p>It’s worth taking a little detour to understand what’s needed for a NixOS module to integrate with the LDAP and SSO integration.</p>
<p>To integrate with LDAP, Jellyfin requires the LDAP plugin to be installed and to create the appropriate config file. None of this is supported declaratively by upstream Jellyfin nor the nixpkgs Jellyfin module. The LDAP config looks like so and I’ll leave it to you to check <a href="https://github.com/ibizaman/selfhostblocks/blob/4289925e8eac6a08f6b10ed3c85227d1a733c313/modules/services/jellyfin.nix#L502-L743">this link</a> in my repository to see how the plugin is installed.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="co"># jellyfin.nix</span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>pkgs.writeText <span class="st">&quot;LDAP-Auth.xml&quot;</span> <span class="st">&#39;&#39;</span></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="st">  &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;</span></span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a><span class="st">  &lt;PluginConfiguration xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xmlns:xsd=&quot;http://www.w3.org/2001/XMLSchema&quot;&gt;</span></span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;LdapServer&gt;</span><span class="sc">${</span>cfg.ldap.host<span class="sc">}</span><span class="st">&lt;/LdapServer&gt;</span></span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;LdapPort&gt;</span><span class="sc">${</span><span class="bu">builtins</span>.<span class="bu">toString</span> cfg.ldap.port<span class="sc">}</span><span class="st">&lt;/LdapPort&gt;</span></span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;UseSsl&gt;false&lt;/UseSsl&gt;</span></span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;UseStartTls&gt;false&lt;/UseStartTls&gt;</span></span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;SkipSslVerify&gt;false&lt;/SkipSslVerify&gt;</span></span>
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;LdapBindUser&gt;uid=admin,ou=people,</span><span class="sc">${</span>cfg.ldap.dcdomain<span class="sc">}</span><span class="st">&lt;/LdapBindUser&gt;</span></span>
<span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;LdapBindPassword&gt;%SECRET_LDAP_PASSWORD%&lt;/LdapBindPassword&gt;</span></span>
<span id="cb6-12"><a href="#cb6-12" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;LdapBaseDn&gt;ou=people,</span><span class="sc">${</span>cfg.ldap.dcdomain<span class="sc">}</span><span class="st">&lt;/LdapBaseDn&gt;</span></span>
<span id="cb6-13"><a href="#cb6-13" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;LdapSearchFilter&gt;(memberof=cn=</span><span class="sc">${</span>cfg.ldap.userGroup<span class="sc">}</span><span class="st">,ou=groups,</span><span class="sc">${</span>cfg.ldap.dcdomain<span class="sc">}</span><span class="st">)&lt;/LdapSearchFilter&gt;</span></span>
<span id="cb6-14"><a href="#cb6-14" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;LdapAdminBaseDn&gt;ou=people,</span><span class="sc">${</span>cfg.ldap.dcdomain<span class="sc">}</span><span class="st">&lt;/LdapAdminBaseDn&gt;</span></span>
<span id="cb6-15"><a href="#cb6-15" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;LdapAdminFilter&gt;(memberof=cn=</span><span class="sc">${</span>cfg.ldap.adminGroup<span class="sc">}</span><span class="st">,ou=groups,</span><span class="sc">${</span>cfg.ldap.dcdomain<span class="sc">}</span><span class="st">)&lt;/LdapAdminFilter&gt;</span></span>
<span id="cb6-16"><a href="#cb6-16" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;EnableLdapAdminFilterMemberUid&gt;false&lt;/EnableLdapAdminFilterMemberUid&gt;</span></span>
<span id="cb6-17"><a href="#cb6-17" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;LdapSearchAttributes&gt;uid, cn, mail, displayName&lt;/LdapSearchAttributes&gt;</span></span>
<span id="cb6-18"><a href="#cb6-18" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;LdapClientCertPath /&gt;</span></span>
<span id="cb6-19"><a href="#cb6-19" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;LdapClientKeyPath /&gt;</span></span>
<span id="cb6-20"><a href="#cb6-20" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;LdapRootCaPath /&gt;</span></span>
<span id="cb6-21"><a href="#cb6-21" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;CreateUsersFromLdap&gt;true&lt;/CreateUsersFromLdap&gt;</span></span>
<span id="cb6-22"><a href="#cb6-22" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;AllowPassChange&gt;false&lt;/AllowPassChange&gt;</span></span>
<span id="cb6-23"><a href="#cb6-23" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;LdapUsernameAttribute&gt;uid&lt;/LdapUsernameAttribute&gt;</span></span>
<span id="cb6-24"><a href="#cb6-24" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;LdapPasswordAttribute&gt;userPassword&lt;/LdapPasswordAttribute&gt;</span></span>
<span id="cb6-25"><a href="#cb6-25" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;EnableAllFolders&gt;true&lt;/EnableAllFolders&gt;</span></span>
<span id="cb6-26"><a href="#cb6-26" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;EnabledFolders /&gt;</span></span>
<span id="cb6-27"><a href="#cb6-27" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;PasswordResetUrl /&gt;</span></span>
<span id="cb6-28"><a href="#cb6-28" aria-hidden="true" tabindex="-1"></a><span class="st">  &lt;/PluginConfiguration&gt;</span></span>
<span id="cb6-29"><a href="#cb6-29" aria-hidden="true" tabindex="-1"></a><span class="st">&#39;&#39;</span>;</span></code></pre></div>
<p>Not shown is how the secret <code>%SECRET_LDAP_PASSWORD%</code> is replaced in this file safely. For this too I needed to write some code in my project but it’s getting upstreamed into nixpkgs <a href="https://github.com/NixOS/nixpkgs/pull/503858">in a PR</a>.</p>
<p>By the way, thanks to the declarative LDAP, you can add your user to the Jellyfin LDAP group with:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span> <span class="va">config</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>:</span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">shb</span>.<span class="va">lldap</span>.<span class="va">ensureGroups</span> <span class="op">=</span> <span class="op">[</span> config.shb.jellyfin.ldap.userGroup <span class="op">];</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a>  <span class="va">shb</span>.<span class="va">lldap</span>.<span class="va">ensureUsers</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a>    <span class="va">me</span>.<span class="va">groups</span> <span class="op">=</span> <span class="op">[</span> config.shb.jellyfin.ldap.userGroup <span class="op">];</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>For SSO, we need to install another plugin and create another file:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="co"># jellyfin.xni</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>pkgs.writeText <span class="st">&quot;SSO-Auth.xml&quot;</span> <span class="st">&#39;&#39;</span></span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="st">  &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;</span></span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a><span class="st">  &lt;PluginConfiguration xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot; xmlns:xsd=&quot;http://www.w3.org/2001/XMLSchema&quot;&gt;</span></span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;SamlConfigs /&gt;</span></span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;OidConfigs&gt;</span></span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a><span class="st">      &lt;item&gt;</span></span>
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a><span class="st">        &lt;key&gt;</span></span>
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a><span class="st">          &lt;string&gt;</span><span class="sc">${</span>cfg.sso.provider<span class="sc">}</span><span class="st">&lt;/string&gt;</span></span>
<span id="cb8-10"><a href="#cb8-10" aria-hidden="true" tabindex="-1"></a><span class="st">        &lt;/key&gt;</span></span>
<span id="cb8-11"><a href="#cb8-11" aria-hidden="true" tabindex="-1"></a><span class="st">        &lt;value&gt;</span></span>
<span id="cb8-12"><a href="#cb8-12" aria-hidden="true" tabindex="-1"></a><span class="st">          &lt;PluginConfiguration&gt;</span></span>
<span id="cb8-13"><a href="#cb8-13" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;SchemeOverride&gt;https&lt;/SchemeOverride&gt;</span></span>
<span id="cb8-14"><a href="#cb8-14" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;OidEndpoint&gt;</span><span class="sc">${</span>cfg.sso.endpoint<span class="sc">}</span><span class="st">&lt;/OidEndpoint&gt;</span></span>
<span id="cb8-15"><a href="#cb8-15" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;OidClientId&gt;</span><span class="sc">${</span>cfg.sso.clientID<span class="sc">}</span><span class="st">&lt;/OidClientId&gt;</span></span>
<span id="cb8-16"><a href="#cb8-16" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;OidSecret&gt;%SECRET_SSO_SECRET%&lt;/OidSecret&gt;</span></span>
<span id="cb8-17"><a href="#cb8-17" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;Enabled&gt;true&lt;/Enabled&gt;</span></span>
<span id="cb8-18"><a href="#cb8-18" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;EnableAuthorization&gt;true&lt;/EnableAuthorization&gt;</span></span>
<span id="cb8-19"><a href="#cb8-19" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;EnableAllFolders&gt;true&lt;/EnableAllFolders&gt;</span></span>
<span id="cb8-20"><a href="#cb8-20" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;EnabledFolders /&gt;</span></span>
<span id="cb8-21"><a href="#cb8-21" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;AdminRoles&gt;</span></span>
<span id="cb8-22"><a href="#cb8-22" aria-hidden="true" tabindex="-1"></a><span class="st">              &lt;string&gt;</span><span class="sc">${</span>cfg.ldap.adminGroup<span class="sc">}</span><span class="st">&lt;/string&gt;</span></span>
<span id="cb8-23"><a href="#cb8-23" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;/AdminRoles&gt;</span></span>
<span id="cb8-24"><a href="#cb8-24" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;Roles&gt;</span></span>
<span id="cb8-25"><a href="#cb8-25" aria-hidden="true" tabindex="-1"></a><span class="st">              &lt;string&gt;</span><span class="sc">${</span>cfg.ldap.userGroup<span class="sc">}</span><span class="st">&lt;/string&gt;</span></span>
<span id="cb8-26"><a href="#cb8-26" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;/Roles&gt;</span></span>
<span id="cb8-27"><a href="#cb8-27" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;EnableFolderRoles&gt;false&lt;/EnableFolderRoles&gt;</span></span>
<span id="cb8-28"><a href="#cb8-28" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;FolderRoleMappings /&gt;</span></span>
<span id="cb8-29"><a href="#cb8-29" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;RoleClaim&gt;groups&lt;/RoleClaim&gt;</span></span>
<span id="cb8-30"><a href="#cb8-30" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;OidScopes&gt;</span></span>
<span id="cb8-31"><a href="#cb8-31" aria-hidden="true" tabindex="-1"></a><span class="st">              &lt;string&gt;groups&lt;/string&gt;</span></span>
<span id="cb8-32"><a href="#cb8-32" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;/OidScopes&gt;</span></span>
<span id="cb8-33"><a href="#cb8-33" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;CanonicalLinks /&gt;</span></span>
<span id="cb8-34"><a href="#cb8-34" aria-hidden="true" tabindex="-1"></a><span class="st">            &lt;DisablePushedAuthorization&gt;true&lt;/DisablePushedAuthorization&gt;</span></span>
<span id="cb8-35"><a href="#cb8-35" aria-hidden="true" tabindex="-1"></a><span class="st">          &lt;/PluginConfiguration&gt;</span></span>
<span id="cb8-36"><a href="#cb8-36" aria-hidden="true" tabindex="-1"></a><span class="st">        &lt;/value&gt;</span></span>
<span id="cb8-37"><a href="#cb8-37" aria-hidden="true" tabindex="-1"></a><span class="st">      &lt;/item&gt;</span></span>
<span id="cb8-38"><a href="#cb8-38" aria-hidden="true" tabindex="-1"></a><span class="st">    &lt;/OidConfigs&gt;</span></span>
<span id="cb8-39"><a href="#cb8-39" aria-hidden="true" tabindex="-1"></a><span class="st">  &lt;/PluginConfiguration&gt;</span></span>
<span id="cb8-40"><a href="#cb8-40" aria-hidden="true" tabindex="-1"></a><span class="st">&#39;&#39;</span>;</span></code></pre></div>
<p>And also create some Authelia config:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="co"># jellyfin.nix</span></span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">shb</span>.<span class="va">authelia</span>.<span class="va">oidcClients</span> <span class="op">=</span> lib.optionals cfg.sso.enable <span class="op">[</span></span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a>    <span class="op">{</span></span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a>      <span class="va">client_id</span> <span class="op">=</span> cfg.sso.clientID<span class="op">;</span></span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a>      <span class="va">client_name</span> <span class="op">=</span> <span class="st">&quot;Jellyfin&quot;</span><span class="op">;</span></span>
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a>      <span class="va">client_secret</span>.<span class="va">source</span> <span class="op">=</span> cfg.sso.sharedSecretForAuthelia.result.path<span class="op">;</span></span>
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a>      <span class="va">public</span> <span class="op">=</span> <span class="cn">false</span><span class="op">;</span></span>
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a>      <span class="va">authorization_policy</span> <span class="op">=</span> cfg.sso.authorization_policy<span class="op">;</span></span>
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a>      <span class="va">redirect_uris</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;https://</span><span class="sc">${</span>cfg.subdomain<span class="sc">}</span><span class="st">.</span><span class="sc">${</span>cfg.domain<span class="sc">}</span><span class="st">/sso/OID/r/</span><span class="sc">${</span>cfg.sso.provider<span class="sc">}</span><span class="st">&quot;</span></span>
<span id="cb9-12"><a href="#cb9-12" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;https://</span><span class="sc">${</span>cfg.subdomain<span class="sc">}</span><span class="st">.</span><span class="sc">${</span>cfg.domain<span class="sc">}</span><span class="st">/sso/OID/redirect/</span><span class="sc">${</span>cfg.sso.provider<span class="sc">}</span><span class="st">&quot;</span></span>
<span id="cb9-13"><a href="#cb9-13" aria-hidden="true" tabindex="-1"></a>      <span class="op">];</span></span>
<span id="cb9-14"><a href="#cb9-14" aria-hidden="true" tabindex="-1"></a>      <span class="va">require_pkce</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb9-15"><a href="#cb9-15" aria-hidden="true" tabindex="-1"></a>      <span class="va">pkce_challenge_method</span> <span class="op">=</span> <span class="st">&quot;S256&quot;</span><span class="op">;</span></span>
<span id="cb9-16"><a href="#cb9-16" aria-hidden="true" tabindex="-1"></a>      <span class="va">userinfo_signed_response_alg</span> <span class="op">=</span> <span class="st">&quot;none&quot;</span><span class="op">;</span></span>
<span id="cb9-17"><a href="#cb9-17" aria-hidden="true" tabindex="-1"></a>      <span class="va">token_endpoint_auth_method</span> <span class="op">=</span> <span class="st">&quot;client_secret_post&quot;</span><span class="op">;</span></span>
<span id="cb9-18"><a href="#cb9-18" aria-hidden="true" tabindex="-1"></a>      <span class="va">scopes</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb9-19"><a href="#cb9-19" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;openid&quot;</span></span>
<span id="cb9-20"><a href="#cb9-20" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;profile&quot;</span></span>
<span id="cb9-21"><a href="#cb9-21" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;email&quot;</span></span>
<span id="cb9-22"><a href="#cb9-22" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;groups&quot;</span></span>
<span id="cb9-23"><a href="#cb9-23" aria-hidden="true" tabindex="-1"></a>      <span class="op">];</span></span>
<span id="cb9-24"><a href="#cb9-24" aria-hidden="true" tabindex="-1"></a>    <span class="op">}</span></span>
<span id="cb9-25"><a href="#cb9-25" aria-hidden="true" tabindex="-1"></a>  <span class="op">];</span></span>
<span id="cb9-26"><a href="#cb9-26" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>I’m leaving out some minute details on how the secret is shared and how the options are defined because there’s already a lot of information.</p>
<p>I wanted to show you all this because it’s quite overwhelming. It’s overwhelming for me too. And it’s horrible to test each time there’s an update. I hope this helps you understand why I thought having tests for these was useful.</p>
<h2 id="testing-ldap-and-sso-integrations">Testing LDAP and SSO Integrations</h2>
<p>We have all the pieces now to write the tests.</p>
<p>Let’s create 3 modules settings up Jellyfin. One for the basic case - no LDAP nor SSO integration - one for the LDAP integration with the <code>ldapModule.nix</code> file and for the SSO integration with the file <code>ssoModule.nix</code>. This way we can combine them easily and create our 3 test scenarios.</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span> <span class="va">pkgs</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>:</span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span></span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">clientLoginModule</span> <span class="op">=</span> <span class="bu">import</span> clientLoginModule.nix<span class="op">;</span></span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">ldapModule</span> <span class="op">=</span> <span class="bu">import</span> ldapModule.nix<span class="op">;</span></span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a>  <span class="va">ssoModule</span> <span class="op">=</span> <span class="bu">import</span> ssoModule.nix<span class="op">;</span></span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a>  <span class="va">basicTestModule</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a>    <span class="va">networking</span>.<span class="va">firewall</span>.<span class="va">allowedTCPPorts</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a>      <span class="dv">80</span></span>
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a>      <span class="dv">443</span></span>
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-13"><a href="#cb10-13" aria-hidden="true" tabindex="-1"></a>    <span class="va">shb</span>.<span class="va">jellyfin</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-14"><a href="#cb10-14" aria-hidden="true" tabindex="-1"></a>      <span class="va">enable</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb10-15"><a href="#cb10-15" aria-hidden="true" tabindex="-1"></a>      <span class="va">domain</span> <span class="op">=</span> <span class="st">&quot;example.com&quot;</span><span class="op">;</span></span>
<span id="cb10-16"><a href="#cb10-16" aria-hidden="true" tabindex="-1"></a>      <span class="va">subdomain</span> <span class="op">=</span> <span class="st">&quot;jellyfin&quot;</span><span class="op">;</span></span>
<span id="cb10-17"><a href="#cb10-17" aria-hidden="true" tabindex="-1"></a>      <span class="va">ssl</span> <span class="op">=</span> config.shb.certs.certs.selfsigned.<span class="st">&quot;example.com&quot;</span><span class="op">;</span></span>
<span id="cb10-18"><a href="#cb10-18" aria-hidden="true" tabindex="-1"></a>      <span class="va">port</span> <span class="op">=</span> <span class="dv">9096</span><span class="op">;</span></span>
<span id="cb10-19"><a href="#cb10-19" aria-hidden="true" tabindex="-1"></a>      <span class="va">admin</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-20"><a href="#cb10-20" aria-hidden="true" tabindex="-1"></a>        <span class="va">username</span> <span class="op">=</span> <span class="st">&quot;admin&quot;</span><span class="op">;</span></span>
<span id="cb10-21"><a href="#cb10-21" aria-hidden="true" tabindex="-1"></a>        <span class="va">password</span>.<span class="va">result</span> <span class="op">=</span> config.shb.hardcodedsecret.jellyfinAdminPassword.result<span class="op">;</span></span>
<span id="cb10-22"><a href="#cb10-22" aria-hidden="true" tabindex="-1"></a>      <span class="op">};</span></span>
<span id="cb10-23"><a href="#cb10-23" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb10-24"><a href="#cb10-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-25"><a href="#cb10-25" aria-hidden="true" tabindex="-1"></a>    <span class="va">shb</span>.<span class="va">hardcodedsecret</span>.<span class="va">jellyfinAdminPassword</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-26"><a href="#cb10-26" aria-hidden="true" tabindex="-1"></a>      <span class="va">request</span> <span class="op">=</span> config.shb.jellyfin.admin.password.request<span class="op">;</span></span>
<span id="cb10-27"><a href="#cb10-27" aria-hidden="true" tabindex="-1"></a>      <span class="va">settings</span>.<span class="va">content</span> <span class="op">=</span> <span class="st">&quot;adminPassword&quot;</span><span class="op">;</span></span>
<span id="cb10-28"><a href="#cb10-28" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb10-29"><a href="#cb10-29" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb10-30"><a href="#cb10-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-31"><a href="#cb10-31" aria-hidden="true" tabindex="-1"></a>  <span class="va">ldapTestModule</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-32"><a href="#cb10-32" aria-hidden="true" tabindex="-1"></a>    <span class="va">imports</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb10-33"><a href="#cb10-33" aria-hidden="true" tabindex="-1"></a>      basicTestModule</span>
<span id="cb10-34"><a href="#cb10-34" aria-hidden="true" tabindex="-1"></a>      ldapModule</span>
<span id="cb10-35"><a href="#cb10-35" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb10-36"><a href="#cb10-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-37"><a href="#cb10-37" aria-hidden="true" tabindex="-1"></a>    <span class="va">shb</span>.<span class="va">jellyfin</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-38"><a href="#cb10-38" aria-hidden="true" tabindex="-1"></a>      <span class="va">ldap</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-39"><a href="#cb10-39" aria-hidden="true" tabindex="-1"></a>        <span class="va">enable</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb10-40"><a href="#cb10-40" aria-hidden="true" tabindex="-1"></a>        <span class="va">host</span> <span class="op">=</span> <span class="st">&quot;127.0.0.1&quot;</span><span class="op">;</span></span>
<span id="cb10-41"><a href="#cb10-41" aria-hidden="true" tabindex="-1"></a>        <span class="va">port</span> <span class="op">=</span> config.shb.lldap.ldapPort<span class="op">;</span></span>
<span id="cb10-42"><a href="#cb10-42" aria-hidden="true" tabindex="-1"></a>        <span class="va">dcdomain</span> <span class="op">=</span> config.shb.lldap.dcdomain<span class="op">;</span></span>
<span id="cb10-43"><a href="#cb10-43" aria-hidden="true" tabindex="-1"></a>        <span class="va">userGroup</span> <span class="op">=</span> <span class="st">&quot;user_group&quot;</span><span class="op">;</span></span>
<span id="cb10-44"><a href="#cb10-44" aria-hidden="true" tabindex="-1"></a>        <span class="va">adminGroup</span> <span class="op">=</span> <span class="st">&quot;admin_group&quot;</span><span class="op">;</span></span>
<span id="cb10-45"><a href="#cb10-45" aria-hidden="true" tabindex="-1"></a>        <span class="va">adminPassword</span>.<span class="va">result</span> <span class="op">=</span> config.shb.hardcodedsecret.jellyfinLdapAdminPassword.result<span class="op">;</span></span>
<span id="cb10-46"><a href="#cb10-46" aria-hidden="true" tabindex="-1"></a>      <span class="op">};</span></span>
<span id="cb10-47"><a href="#cb10-47" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb10-48"><a href="#cb10-48" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-49"><a href="#cb10-49" aria-hidden="true" tabindex="-1"></a>    <span class="va">shb</span>.<span class="va">hardcodedsecret</span>.<span class="va">jellyfinLdapAdminPassword</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-50"><a href="#cb10-50" aria-hidden="true" tabindex="-1"></a>      <span class="va">request</span> <span class="op">=</span> config.shb.jellyfin.ldap.adminPassword.request<span class="op">;</span></span>
<span id="cb10-51"><a href="#cb10-51" aria-hidden="true" tabindex="-1"></a>      <span class="va">settings</span>.<span class="va">content</span> <span class="op">=</span> config.shb.hardcodedsecret.ldapAdminPassword.settings.content<span class="op">;</span></span>
<span id="cb10-52"><a href="#cb10-52" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb10-53"><a href="#cb10-53" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb10-54"><a href="#cb10-54" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-55"><a href="#cb10-55" aria-hidden="true" tabindex="-1"></a>  <span class="va">ssoTestModule</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-56"><a href="#cb10-56" aria-hidden="true" tabindex="-1"></a>    <span class="va">imports</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb10-57"><a href="#cb10-57" aria-hidden="true" tabindex="-1"></a>      basicTestModule</span>
<span id="cb10-58"><a href="#cb10-58" aria-hidden="true" tabindex="-1"></a>      ssoModule</span>
<span id="cb10-59"><a href="#cb10-59" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb10-60"><a href="#cb10-60" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-61"><a href="#cb10-61" aria-hidden="true" tabindex="-1"></a>    <span class="va">shb</span>.<span class="va">jellyfin</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-62"><a href="#cb10-62" aria-hidden="true" tabindex="-1"></a>      <span class="va">ldap</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-63"><a href="#cb10-63" aria-hidden="true" tabindex="-1"></a>        <span class="va">userGroup</span> <span class="op">=</span> <span class="st">&quot;user_group&quot;</span><span class="op">;</span></span>
<span id="cb10-64"><a href="#cb10-64" aria-hidden="true" tabindex="-1"></a>        <span class="va">adminGroup</span> <span class="op">=</span> <span class="st">&quot;admin_group&quot;</span><span class="op">;</span></span>
<span id="cb10-65"><a href="#cb10-65" aria-hidden="true" tabindex="-1"></a>      <span class="op">};</span></span>
<span id="cb10-66"><a href="#cb10-66" aria-hidden="true" tabindex="-1"></a>      <span class="va">sso</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-67"><a href="#cb10-67" aria-hidden="true" tabindex="-1"></a>        <span class="va">enable</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb10-68"><a href="#cb10-68" aria-hidden="true" tabindex="-1"></a>        <span class="va">endpoint</span> <span class="op">=</span> <span class="st">&quot;https://</span><span class="sc">${</span>config.shb.authelia.subdomain<span class="sc">}</span><span class="st">.</span><span class="sc">${</span>config.shb.authelia.domain<span class="sc">}</span><span class="st">&quot;</span><span class="op">;</span></span>
<span id="cb10-69"><a href="#cb10-69" aria-hidden="true" tabindex="-1"></a>        <span class="va">sharedSecret</span>.<span class="va">result</span> <span class="op">=</span> config.shb.hardcodedsecret.jellyfinSSOPassword.result<span class="op">;</span></span>
<span id="cb10-70"><a href="#cb10-70" aria-hidden="true" tabindex="-1"></a>        <span class="va">sharedSecretForAuthelia</span>.<span class="va">result</span> <span class="op">=</span> config.shb.hardcodedsecret.jellyfinSSOPasswordAuthelia.result<span class="op">;</span></span>
<span id="cb10-71"><a href="#cb10-71" aria-hidden="true" tabindex="-1"></a>      <span class="op">};</span></span>
<span id="cb10-72"><a href="#cb10-72" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb10-73"><a href="#cb10-73" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-74"><a href="#cb10-74" aria-hidden="true" tabindex="-1"></a>    <span class="va">shb</span>.<span class="va">hardcodedsecret</span>.<span class="va">jellyfinSSOPassword</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-75"><a href="#cb10-75" aria-hidden="true" tabindex="-1"></a>      <span class="va">request</span> <span class="op">=</span> config.shb.jellyfin.sso.sharedSecret.request<span class="op">;</span></span>
<span id="cb10-76"><a href="#cb10-76" aria-hidden="true" tabindex="-1"></a>      <span class="va">settings</span>.<span class="va">content</span> <span class="op">=</span> <span class="st">&quot;ssoPassword&quot;</span><span class="op">;</span></span>
<span id="cb10-77"><a href="#cb10-77" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb10-78"><a href="#cb10-78" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-79"><a href="#cb10-79" aria-hidden="true" tabindex="-1"></a>    <span class="va">shb</span>.<span class="va">hardcodedsecret</span>.<span class="va">jellyfinSSOPasswordAuthelia</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-80"><a href="#cb10-80" aria-hidden="true" tabindex="-1"></a>      <span class="va">request</span> <span class="op">=</span> config.shb.jellyfin.sso.sharedSecretForAuthelia.request<span class="op">;</span></span>
<span id="cb10-81"><a href="#cb10-81" aria-hidden="true" tabindex="-1"></a>      <span class="va">settings</span>.<span class="va">content</span> <span class="op">=</span> config.shb.hardcodedsecret.jellyfinSSOPassword.settings.content<span class="op">;</span></span>
<span id="cb10-82"><a href="#cb10-82" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb10-83"><a href="#cb10-83" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb10-84"><a href="#cb10-84" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-85"><a href="#cb10-85" aria-hidden="true" tabindex="-1"></a>  <span class="va">clientLoginTestModule</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-86"><a href="#cb10-86" aria-hidden="true" tabindex="-1"></a>    <span class="va">imports</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb10-87"><a href="#cb10-87" aria-hidden="true" tabindex="-1"></a>      clientLoginModule</span>
<span id="cb10-88"><a href="#cb10-88" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb10-89"><a href="#cb10-89" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-90"><a href="#cb10-90" aria-hidden="true" tabindex="-1"></a>    <span class="va">networking</span>.<span class="va">hosts</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb10-91"><a href="#cb10-91" aria-hidden="true" tabindex="-1"></a>      <span class="st">&quot;</span>192.168.1.2<span class="st">&quot;</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb10-92"><a href="#cb10-92" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;jellyfin.example.com&quot;</span></span>
<span id="cb10-93"><a href="#cb10-93" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;auth.example.com&quot;</span></span>
<span id="cb10-94"><a href="#cb10-94" aria-hidden="true" tabindex="-1"></a>      <span class="op">];</span></span>
<span id="cb10-95"><a href="#cb10-95" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb10-96"><a href="#cb10-96" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb10-97"><a href="#cb10-97" aria-hidden="true" tabindex="-1"></a><span class="kw">in</span></span>
<span id="cb10-98"><a href="#cb10-98" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb10-99"><a href="#cb10-99" aria-hidden="true" tabindex="-1"></a>  <span class="va">basic</span> <span class="op">=</span> pkgs.testers.runNixOSTest <span class="op">{</span></span>
<span id="cb10-100"><a href="#cb10-100" aria-hidden="true" tabindex="-1"></a>    <span class="va">name</span> <span class="op">=</span> <span class="st">&quot;jellyfin_basic&quot;</span><span class="op">;</span></span>
<span id="cb10-101"><a href="#cb10-101" aria-hidden="true" tabindex="-1"></a>    <span class="va">nodes</span>.<span class="va">server</span>.<span class="va">imports</span> <span class="op">=</span> <span class="op">[</span> basicTestModule <span class="op">];</span></span>
<span id="cb10-102"><a href="#cb10-102" aria-hidden="true" tabindex="-1"></a>    <span class="va">node</span>.<span class="va">client</span>.<span class="va">imports</span> <span class="op">=</span> <span class="op">[</span> clientLoginModule <span class="op">];</span></span>
<span id="cb10-103"><a href="#cb10-103" aria-hidden="true" tabindex="-1"></a>    <span class="va">testScript</span> <span class="op">=</span> ...</span>
<span id="cb10-104"><a href="#cb10-104" aria-hidden="true" tabindex="-1"></a>  <span class="er">}</span><span class="op">;</span></span>
<span id="cb10-105"><a href="#cb10-105" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-106"><a href="#cb10-106" aria-hidden="true" tabindex="-1"></a>  <span class="va">ldap</span> <span class="op">=</span> pkgs.testers.runNixOSTest <span class="op">{</span></span>
<span id="cb10-107"><a href="#cb10-107" aria-hidden="true" tabindex="-1"></a>    <span class="va">name</span> <span class="op">=</span> <span class="st">&quot;jellyfin_ldap&quot;</span><span class="op">;</span></span>
<span id="cb10-108"><a href="#cb10-108" aria-hidden="true" tabindex="-1"></a>    <span class="va">nodes</span>.<span class="va">server</span>.<span class="va">imports</span> <span class="op">=</span> <span class="op">[</span> ldapTestModule <span class="op">];</span></span>
<span id="cb10-109"><a href="#cb10-109" aria-hidden="true" tabindex="-1"></a>    <span class="va">node</span>.<span class="va">client</span>.<span class="va">imports</span> <span class="op">=</span> <span class="op">[</span> clientLoginModule <span class="op">];</span></span>
<span id="cb10-110"><a href="#cb10-110" aria-hidden="true" tabindex="-1"></a>    <span class="va">testScript</span> <span class="op">=</span> ...</span>
<span id="cb10-111"><a href="#cb10-111" aria-hidden="true" tabindex="-1"></a>  <span class="er">}</span><span class="op">;</span></span>
<span id="cb10-112"><a href="#cb10-112" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-113"><a href="#cb10-113" aria-hidden="true" tabindex="-1"></a>  <span class="va">sso</span> <span class="op">=</span> pkgs.testers.runNixOSTest <span class="op">{</span></span>
<span id="cb10-114"><a href="#cb10-114" aria-hidden="true" tabindex="-1"></a>    <span class="va">name</span> <span class="op">=</span> <span class="st">&quot;jellyfin_sso&quot;</span><span class="op">;</span></span>
<span id="cb10-115"><a href="#cb10-115" aria-hidden="true" tabindex="-1"></a>    <span class="va">nodes</span>.<span class="va">server</span>.<span class="va">imports</span> <span class="op">=</span> <span class="op">[</span> ssoTestModule <span class="op">];</span></span>
<span id="cb10-116"><a href="#cb10-116" aria-hidden="true" tabindex="-1"></a>    <span class="va">node</span>.<span class="va">client</span>.<span class="va">imports</span> <span class="op">=</span> <span class="op">[</span> clientLoginModule <span class="op">];</span></span>
<span id="cb10-117"><a href="#cb10-117" aria-hidden="true" tabindex="-1"></a>    <span class="va">testScript</span> <span class="op">=</span> ...</span>
<span id="cb10-118"><a href="#cb10-118" aria-hidden="true" tabindex="-1"></a>  <span class="er">}</span><span class="op">;</span></span>
<span id="cb10-119"><a href="#cb10-119" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>Here we setup Jellyfin to be accessible from <code>jellyfin.example.com</code>. We open up ports 80 and 443 so the client node can talk to the server node and we setup the client node so calls to Jellyfin and Authelia goes to the server node.</p>
<p>With all this behind us, we can finally write the <code>testScript</code>. Do you remember the <a href="#test-function-snippet"><code>test(page)</code> Python function</a> we created in the <code>login_playwright</code> executable? We will fill it out here.</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a>page.goto(<span class="st">&quot;https://jellyfin.example.com&quot;</span>)</span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a>page.get_by_label(re.<span class="bu">compile</span>(<span class="st">&#39;[Uu]ser&#39;</span>)).fill(<span class="st">&quot;alice&quot;</span>)</span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a>page.get_by_label(re.<span class="bu">compile</span>(<span class="st">&#39;[Pp]assword&#39;</span>)).fill(<span class="st">&quot;AlicePassword&quot;</span>)</span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a>page.get_by_role(<span class="st">&quot;button&quot;</span>, name<span class="op">=</span>re.<span class="bu">compile</span>(<span class="st">&#39;Sign In&#39;</span>)).click()</span>
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a>expect(page.get_by_text(re.<span class="bu">compile</span>(<span class="st">&#39;[Ii]nvalid&#39;</span>))).not_to_be_visible()</span></code></pre></div>
<p>Wouldn’t it be nice if it were this simple? Don’t get me wrong, this works, but it does not take into account all use cases nor all quirks of Jellyfin.</p>
<p>First, we need to decline these tests to handle the basic - no LDAP nor SSO integration - the LDAP integration and SSO integration cases. We then need to test the 3 users and make sure Alice can login, Bob can login with admin privileges and Charlie cannot connect. We also need to test the same 3 users but each with wrong passwords to make sure none can login. That makes for quite a few combinations in total.</p>
<p>The basic and LDAP integrations work the same way, they both use the service’s own login form:</p>
<figure>
<img src="/images/2026-08-22-testing-ldap-and-sso-integrations-in-nix-os-with-playwright/2_Jellyfin_login.png" class="zoom" alt="Jellyfin login form shows two text inputs followed by the Sign In button" />
<figcaption aria-hidden="true">Jellyfin login form shows two text inputs followed by the Sign In button</figcaption>
</figure>
<ol type="1">
<li>We fill out the username and password.</li>
<li>We click the Sign In button.</li>
<li>We assert the user is logged in.</li>
</ol>
<p>In code, a successful login looks like so:</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loginBasic(username, password):</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a>  page.goto(<span class="st">&quot;https://jellyfin.example.com&quot;</span>)</span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a>  page.get_by_label(re.<span class="bu">compile</span>(<span class="st">&#39;[Uu]ser&#39;</span>)).fill(username)</span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a>  page.get_by_label(re.<span class="bu">compile</span>(<span class="st">&#39;[Pp]assword&#39;</span>)).fill(password)</span>
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a>  page.get_by_role(<span class="st">&quot;button&quot;</span>, name<span class="op">=</span>re.<span class="bu">compile</span>(<span class="st">&#39;Sign In&#39;</span>)).click()</span>
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a>  expect(page.get_by_text(re.<span class="bu">compile</span>(<span class="st">&#39;[Ii]nvalid&#39;</span>))).not_to_be_visible()</span>
<span id="cb12-7"><a href="#cb12-7" aria-hidden="true" tabindex="-1"></a>  expect(page.get_by_label(re.<span class="bu">compile</span>(<span class="st">&#39;^[Uu]ser&#39;</span>))).not_to_be_visible()</span></code></pre></div>
<p>The <code>expect</code> combined with <code>not_to_be_visible()</code> is to make sure the popup saying the user could not log in has not shown up and that we’re not seeing the login form anymore.</p>
<p>And a bad login:</p>
<div class="sourceCode" id="cb13"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loginBasicFail(username, password):</span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a>  page.goto(<span class="st">&quot;https://jellyfin.example.com&quot;</span>)</span>
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a>  page.get_by_label(re.<span class="bu">compile</span>(<span class="st">&#39;[Uu]ser&#39;</span>)).fill(username)</span>
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a>  page.get_by_label(re.<span class="bu">compile</span>(<span class="st">&#39;[Pp]assword&#39;</span>)).fill(password)</span>
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a>  page.get_by_role(<span class="st">&quot;button&quot;</span>, name<span class="op">=</span>re.<span class="bu">compile</span>(<span class="st">&#39;Sign In&#39;</span>)).click()</span>
<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a>  expect(page.get_by_text(re.<span class="bu">compile</span>(<span class="st">&#39;[Ii]nvalid&#39;</span>))).to_be_visible()</span>
<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a>  expect(page.get_by_label(re.<span class="bu">compile</span>(<span class="st">&#39;^[Uu]ser&#39;</span>))).to_be_visible()</span></code></pre></div>
<p>Note that Jellyfin cannot create users declaratively so I’m only testing login with the admin user in that one. Having a declarative admin user is something I added myself too and <a href="https://github.com/jellyfin/jellyfin/pull/16168">upstreamed into Jellyfin</a>.</p>
<p>The SSO integration has a different flow.</p>
<ol type="1">
<li><p>We first need to click on the SSO button</p>
<figure>
<img src="/images/2026-08-22-testing-ldap-and-sso-integrations-in-nix-os-with-playwright/3_Jellyfin_sso_button.png" class="zoom" alt="Jellyfin login form showing an extra button to sign in with SSO" />
<figcaption aria-hidden="true">Jellyfin login form showing an extra button to sign in with SSO</figcaption>
</figure></li>
<li><p>which redirects to the login flow.</p>
<figure>
<img src="/images/2026-08-22-testing-ldap-and-sso-integrations-in-nix-os-with-playwright/4_Jellyfin_sso_login.png" class="zoom" alt="SSO login form from Authelia, showing two text inputs and a sign in button" />
<figcaption aria-hidden="true">SSO login form from Authelia, showing two text inputs and a sign in button</figcaption>
</figure></li>
<li><p>We then we have to grant access</p>
<figure>
<img src="/images/2026-08-22-testing-ldap-and-sso-integrations-in-nix-os-with-playwright/5_Jellyfin_sso_grant.png" class="zoom" alt="SSO permission grant page" />
<figcaption aria-hidden="true">SSO permission grant page</figcaption>
</figure></li>
<li><p>and then we can finally login.</p></li>
</ol>
<p>Which in code looks like so:</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loginSSO(username, password):</span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a>  page.goto(<span class="st">&quot;https://jellyfin.example.com&quot;</span>)</span>
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a>  <span class="cf">with</span> context.expect_page() <span class="im">as</span> p:</span>
<span id="cb14-4"><a href="#cb14-4" aria-hidden="true" tabindex="-1"></a>    page.locator(<span class="st">&#39;text=Sign in with Authelia&#39;</span>).click()</span>
<span id="cb14-5"><a href="#cb14-5" aria-hidden="true" tabindex="-1"></a>  page <span class="op">=</span> p</span>
<span id="cb14-6"><a href="#cb14-6" aria-hidden="true" tabindex="-1"></a>  page.get_by_label(<span class="st">&#39;Username&#39;</span>).fill(username)</span>
<span id="cb14-7"><a href="#cb14-7" aria-hidden="true" tabindex="-1"></a>  page.get_by_label(<span class="st">&#39;Password&#39;</span>).fill(password)</span>
<span id="cb14-8"><a href="#cb14-8" aria-hidden="true" tabindex="-1"></a>  page.get_by_role(<span class="st">&quot;button&quot;</span>, name<span class="op">=</span>re.<span class="bu">compile</span>(<span class="st">&#39;Sign In&#39;</span>)).click()</span>
<span id="cb14-9"><a href="#cb14-9" aria-hidden="true" tabindex="-1"></a>  page.get_by_text(re.<span class="bu">compile</span>(<span class="st">&#39;[Aa]ccept&#39;</span>)).click()</span>
<span id="cb14-10"><a href="#cb14-10" aria-hidden="true" tabindex="-1"></a>  expect(page.get_by_text(re.<span class="bu">compile</span>(<span class="st">&#39;[Ii]nvalid&#39;</span>))).not_to_be_visible()</span>
<span id="cb14-11"><a href="#cb14-11" aria-hidden="true" tabindex="-1"></a>  expect(page.get_by_label(re.<span class="bu">compile</span>(<span class="st">&#39;^[Uu]ser&#39;</span>))).not_to_be_visible()</span></code></pre></div>
<p>Although Jellyfin has an SSO plugin, it has no official way to show a “log in with SSO” button. To still show a button on the login page, we abuse the system designed to add your own branding to Jellyfin by adding the button there. Unfortunately that always opens the login page in a new tab, which we need to catch above with the <code>context.expect_page()</code> call.</p>
<p>Here, the fail login does not have the exact same flow. To avoid waiting for redirection, we navigate back to the Jellyfin base URL and make sure the login form shows up:</p>
<div class="sourceCode" id="cb15"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loginSSOFail(username, password):</span>
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a>  page.goto(<span class="st">&quot;https://jellyfin.example.com&quot;</span>)</span>
<span id="cb15-3"><a href="#cb15-3" aria-hidden="true" tabindex="-1"></a>  <span class="cf">with</span> context.expect_page() <span class="im">as</span> p:</span>
<span id="cb15-4"><a href="#cb15-4" aria-hidden="true" tabindex="-1"></a>    page.locator(<span class="st">&#39;text=Sign in with Authelia&#39;</span>).click()</span>
<span id="cb15-5"><a href="#cb15-5" aria-hidden="true" tabindex="-1"></a>  page <span class="op">=</span> p</span>
<span id="cb15-6"><a href="#cb15-6" aria-hidden="true" tabindex="-1"></a>  page.get_by_label(<span class="st">&#39;Username&#39;</span>).fill(username)</span>
<span id="cb15-7"><a href="#cb15-7" aria-hidden="true" tabindex="-1"></a>  page.get_by_label(<span class="st">&#39;Password&#39;</span>).fill(password)</span>
<span id="cb15-8"><a href="#cb15-8" aria-hidden="true" tabindex="-1"></a>  page.get_by_role(<span class="st">&quot;button&quot;</span>, name<span class="op">=</span>re.<span class="bu">compile</span>(<span class="st">&#39;Sign In&#39;</span>)).click()</span>
<span id="cb15-9"><a href="#cb15-9" aria-hidden="true" tabindex="-1"></a>  page.goto(<span class="st">&quot;https://jellyfin.example.com&quot;</span>)</span>
<span id="cb15-10"><a href="#cb15-10" aria-hidden="true" tabindex="-1"></a>  expect(page.get_by_text(re.<span class="bu">compile</span>(<span class="st">&#39;[Ii]nvalid&#39;</span>))).to_be_visible()</span>
<span id="cb15-11"><a href="#cb15-11" aria-hidden="true" tabindex="-1"></a>  expect(page.get_by_label(re.<span class="bu">compile</span>(<span class="st">&#39;^[Uu]ser&#39;</span>))).to_be_visible()</span></code></pre></div>
<p>Taking all this together, we can now complete the various <code>testScripts</code>:</p>
<div class="sourceCode" id="cb16"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a><span class="co"># ...</span></span>
<span id="cb16-2"><a href="#cb16-2" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb16-3"><a href="#cb16-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">basic</span> <span class="op">=</span> pkgs.testers.runNixOSTest <span class="op">{</span></span>
<span id="cb16-4"><a href="#cb16-4" aria-hidden="true" tabindex="-1"></a>    <span class="va">name</span> <span class="op">=</span> <span class="st">&quot;jellyfin_basic&quot;</span><span class="op">;</span></span>
<span id="cb16-5"><a href="#cb16-5" aria-hidden="true" tabindex="-1"></a>    <span class="va">nodes</span>.<span class="va">server</span>.<span class="va">imports</span> <span class="op">=</span> <span class="op">[</span> basicTestModule <span class="op">];</span></span>
<span id="cb16-6"><a href="#cb16-6" aria-hidden="true" tabindex="-1"></a>    <span class="va">nodes</span>.<span class="va">client</span>.<span class="va">imports</span> <span class="op">=</span> <span class="op">[</span> clientLoginModule <span class="op">];</span></span>
<span id="cb16-7"><a href="#cb16-7" aria-hidden="true" tabindex="-1"></a>    <span class="va">testScript</span> <span class="op">=</span> <span class="st">&#39;&#39;</span></span>
<span id="cb16-8"><a href="#cb16-8" aria-hidden="true" tabindex="-1"></a><span class="st">      start_all()</span></span>
<span id="cb16-9"><a href="#cb16-9" aria-hidden="true" tabindex="-1"></a><span class="st">      loginBasic(&quot;admin&quot;, config.shb.hardcodedsecret.jellyfinAdminPassword.settings.content)</span></span>
<span id="cb16-10"><a href="#cb16-10" aria-hidden="true" tabindex="-1"></a><span class="st">      loginBasicFail(&quot;admin&quot;, &quot;BadPassword&quot;)</span></span>
<span id="cb16-11"><a href="#cb16-11" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&#39;</span><span class="op">;</span></span>
<span id="cb16-12"><a href="#cb16-12" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb16-13"><a href="#cb16-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb16-14"><a href="#cb16-14" aria-hidden="true" tabindex="-1"></a>  <span class="va">ldap</span> <span class="op">=</span> pkgs.testers.runNixOSTest <span class="op">{</span></span>
<span id="cb16-15"><a href="#cb16-15" aria-hidden="true" tabindex="-1"></a>    <span class="va">name</span> <span class="op">=</span> <span class="st">&quot;jellyfin_ldap&quot;</span><span class="op">;</span></span>
<span id="cb16-16"><a href="#cb16-16" aria-hidden="true" tabindex="-1"></a>    <span class="va">nodes</span>.<span class="va">server</span>.<span class="va">imports</span> <span class="op">=</span> <span class="op">[</span> ldapTestModule <span class="op">];</span></span>
<span id="cb16-17"><a href="#cb16-17" aria-hidden="true" tabindex="-1"></a>    <span class="va">nodes</span>.<span class="va">client</span>.<span class="va">imports</span> <span class="op">=</span> <span class="op">[</span> clientLoginModule <span class="op">];</span></span>
<span id="cb16-18"><a href="#cb16-18" aria-hidden="true" tabindex="-1"></a>    <span class="va">testScript</span> <span class="op">=</span> <span class="st">&#39;&#39;</span></span>
<span id="cb16-19"><a href="#cb16-19" aria-hidden="true" tabindex="-1"></a><span class="st">      start_all()</span></span>
<span id="cb16-20"><a href="#cb16-20" aria-hidden="true" tabindex="-1"></a><span class="st">      loginBasic(&quot;admin&quot;, config.shb.hardcodedsecret.jellyfinAdminPassword.settings.content)</span></span>
<span id="cb16-21"><a href="#cb16-21" aria-hidden="true" tabindex="-1"></a><span class="st">      loginBasicFail(&quot;admin&quot;, &quot;BadPassword&quot;)</span></span>
<span id="cb16-22"><a href="#cb16-22" aria-hidden="true" tabindex="-1"></a><span class="st">      loginBasic(&quot;alice&quot;, config.shb.hardcodedsecret.alice.settings.content)</span></span>
<span id="cb16-23"><a href="#cb16-23" aria-hidden="true" tabindex="-1"></a><span class="st">      loginBasicFail(&quot;alice&quot;, &quot;BadPassword&quot;)</span></span>
<span id="cb16-24"><a href="#cb16-24" aria-hidden="true" tabindex="-1"></a><span class="st">      loginBasic(&quot;bob&quot;, config.shb.hardcodedsecret.alice.settings.content)</span></span>
<span id="cb16-25"><a href="#cb16-25" aria-hidden="true" tabindex="-1"></a><span class="st">      loginBasicFail(&quot;bob&quot;, &quot;BadPassword&quot;)</span></span>
<span id="cb16-26"><a href="#cb16-26" aria-hidden="true" tabindex="-1"></a><span class="st">      loginBasicFail(&quot;charlie&quot;, config.shb.hardcodedsecret.alice.settings.content)</span></span>
<span id="cb16-27"><a href="#cb16-27" aria-hidden="true" tabindex="-1"></a><span class="st">      loginBasicFail(&quot;charlie&quot;, &quot;BadPassword&quot;)</span></span>
<span id="cb16-28"><a href="#cb16-28" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&#39;</span><span class="op">;</span></span>
<span id="cb16-29"><a href="#cb16-29" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb16-30"><a href="#cb16-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb16-31"><a href="#cb16-31" aria-hidden="true" tabindex="-1"></a>  <span class="va">sso</span> <span class="op">=</span> pkgs.testers.runNixOSTest <span class="op">{</span></span>
<span id="cb16-32"><a href="#cb16-32" aria-hidden="true" tabindex="-1"></a>    <span class="va">name</span> <span class="op">=</span> <span class="st">&quot;jellyfin_sso&quot;</span><span class="op">;</span></span>
<span id="cb16-33"><a href="#cb16-33" aria-hidden="true" tabindex="-1"></a>    <span class="va">nodes</span>.<span class="va">server</span>.<span class="va">imports</span> <span class="op">=</span> <span class="op">[</span> ssoTestModule <span class="op">];</span></span>
<span id="cb16-34"><a href="#cb16-34" aria-hidden="true" tabindex="-1"></a>    <span class="va">nodes</span>.<span class="va">client</span>.<span class="va">imports</span> <span class="op">=</span> <span class="op">[</span> clientLoginModule <span class="op">];</span></span>
<span id="cb16-35"><a href="#cb16-35" aria-hidden="true" tabindex="-1"></a>    <span class="va">testScript</span> <span class="op">=</span> <span class="st">&#39;&#39;</span></span>
<span id="cb16-36"><a href="#cb16-36" aria-hidden="true" tabindex="-1"></a><span class="st">      start_all()</span></span>
<span id="cb16-37"><a href="#cb16-37" aria-hidden="true" tabindex="-1"></a><span class="st">      loginSSO(&quot;alice&quot;, config.shb.hardcodedsecret.alice.settings.content)</span></span>
<span id="cb16-38"><a href="#cb16-38" aria-hidden="true" tabindex="-1"></a><span class="st">      loginSSOFail(&quot;alice&quot;, &quot;BadPassword&quot;)</span></span>
<span id="cb16-39"><a href="#cb16-39" aria-hidden="true" tabindex="-1"></a><span class="st">      loginSSO(&quot;bob&quot;, config.shb.hardcodedsecret.alice.settings.content)</span></span>
<span id="cb16-40"><a href="#cb16-40" aria-hidden="true" tabindex="-1"></a><span class="st">      loginSSOFail(&quot;bob&quot;, &quot;BadPassword&quot;)</span></span>
<span id="cb16-41"><a href="#cb16-41" aria-hidden="true" tabindex="-1"></a><span class="st">      loginSSOFail(&quot;charlie&quot;, config.shb.hardcodedsecret.alice.settings.content)</span></span>
<span id="cb16-42"><a href="#cb16-42" aria-hidden="true" tabindex="-1"></a><span class="st">      loginSSOFail(&quot;charlie&quot;, &quot;BadPassword&quot;)</span></span>
<span id="cb16-43"><a href="#cb16-43" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&#39;</span><span class="op">;</span></span>
<span id="cb16-44"><a href="#cb16-44" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb16-45"><a href="#cb16-45" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p><code>start_all()</code> starts all VM nodes. In the SSO integration case, we don’t login with the admin user since it is not provisioned by the SSO integration. I aslo left out how to import the <code>login*</code> Python function for brevity.</p>
<p>The actual test framework is actually organised a bit differently to provide more reusable pieces and accommodate to the various particularities of the different services. You can take a look at the <a href="https://github.com/ibizaman/selfhostblocks/blob/main/test/services/jellyfin.nix">real jellyfin test file</a> and the <a href="https://github.com/ibizaman/selfhostblocks/blob/main/test/common.nix">real common testing framework</a> by clicking on the links.</p>
<h2 id="conclusion">Conclusion?</h2>
<p>I know, the conclusion is not at the end of the blog post, but we did achieve our original goal now!</p>
<p>We can finally make sure no update will break our previous integrations with LDAP and SSO providers without us noticing.</p>
<p>But let’s continue a bit, we’re not totally done yet.</p>
<h2 id="debugging">Debugging</h2>
<p>Thanks to saving the traces, it’s easy to figure out what’s wrong. After running a test, you can see the traces with:</p>
<div class="sourceCode" id="cb17"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb17-1"><a href="#cb17-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix build .#checks.x86_64-linux.vm_jellyfin_https <span class="at">-L</span> <span class="at">--keep-failed</span></span>
<span id="cb17-2"><a href="#cb17-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb17-3"><a href="#cb17-3" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ls <span class="at">-1</span> result/trace/</span>
<span id="cb17-4"><a href="#cb17-4" aria-hidden="true" tabindex="-1"></a><span class="ex">0.zip</span></span>
<span id="cb17-5"><a href="#cb17-5" aria-hidden="true" tabindex="-1"></a><span class="ex">1.zip</span></span>
<span id="cb17-6"><a href="#cb17-6" aria-hidden="true" tabindex="-1"></a><span class="ex">2.zip</span></span>
<span id="cb17-7"><a href="#cb17-7" aria-hidden="true" tabindex="-1"></a><span class="ex">3.zip</span></span>
<span id="cb17-8"><a href="#cb17-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb17-9"><a href="#cb17-9" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix run github:ibizaman/selfhostblocks#playwright show-trace result/trace/0.zip</span></code></pre></div>
<p>The <code>--keep-failed</code> is necessary to use if the test failed, because it will then print where the temp files are locate. In that case, you can access them with:</p>
<div class="sourceCode" id="cb18"><pre class="sourceCode sh"><code class="sourceCode bash"><span id="cb18-1"><a href="#cb18-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo cp <span class="at">-r</span> /nix/var/nix/builds/nix-256919-1305278864/build trace <span class="kw">&amp;&amp;</span> <span class="fu">sudo</span> chown <span class="at">-R</span> <span class="va">$USER</span>: trace</span>
<span id="cb18-2"><a href="#cb18-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb18-3"><a href="#cb18-3" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix run github:ibizaman/selfhostblocks#playwright show-trace trace/shared-xchg/trace/0.zip</span></code></pre></div>
<p>You then get this nice detailed view of everything that happened from the browser’s perspective.</p>
<figure>
<img src="/images/2026-08-22-testing-ldap-and-sso-integrations-in-nix-os-with-playwright/6_Playwright_trace.png" class="zoom" alt="SSO permission grant page" />
<figcaption aria-hidden="true">SSO permission grant page</figcaption>
</figure>
<p>It’s usually best to start with this view and then look at the test logs, to at least narrow down where to look.</p>
<h2 id="bonus-test">Bonus Test</h2>
<p>But that’s not all folks. Something I noticed in the Nextcloud service is that one can correctly setup the LDAP and SSO integration but still make them inconsistent with each other. What I mean is if you set up both integrations and login once using the LDAP flow and once through the SSO flow, you might end up with two different users!</p>
<p>Now, how do you test something like this? Well, NixOS provides us with something quite unique, specializations. Think of those as parallel configurations for our NixOS host that we can switch between at runtime, without needing to deploy again.</p>
<p>So let’s create this final test:</p>
<div class="sourceCode" id="cb19"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb19-1"><a href="#cb19-1" aria-hidden="true" tabindex="-1"></a><span class="co"># ...</span></span>
<span id="cb19-2"><a href="#cb19-2" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb19-3"><a href="#cb19-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">users</span> <span class="op">=</span> pkgs.testers.runNixOSTest <span class="op">{</span></span>
<span id="cb19-4"><a href="#cb19-4" aria-hidden="true" tabindex="-1"></a>    <span class="va">name</span> <span class="op">=</span> <span class="st">&quot;jellyfin_users&quot;</span><span class="op">;</span></span>
<span id="cb19-5"><a href="#cb19-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb19-6"><a href="#cb19-6" aria-hidden="true" tabindex="-1"></a>    <span class="va">nodes</span>.<span class="va">server</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb19-7"><a href="#cb19-7" aria-hidden="true" tabindex="-1"></a>      <span class="va">imports</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb19-8"><a href="#cb19-8" aria-hidden="true" tabindex="-1"></a>        basicTestModule</span>
<span id="cb19-9"><a href="#cb19-9" aria-hidden="true" tabindex="-1"></a>      <span class="op">];</span></span>
<span id="cb19-10"><a href="#cb19-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb19-11"><a href="#cb19-11" aria-hidden="true" tabindex="-1"></a>      <span class="va">specialisation</span>.<span class="va">ldap</span>.<span class="va">configuration</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb19-12"><a href="#cb19-12" aria-hidden="true" tabindex="-1"></a>        <span class="va">imports</span> <span class="op">=</span> <span class="op">[</span> ldapTestModule <span class="op">];</span></span>
<span id="cb19-13"><a href="#cb19-13" aria-hidden="true" tabindex="-1"></a>      <span class="op">};</span></span>
<span id="cb19-14"><a href="#cb19-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb19-15"><a href="#cb19-15" aria-hidden="true" tabindex="-1"></a>      <span class="va">specialisation</span>.<span class="va">sso</span>.<span class="va">configuration</span> <span class="op">=</span></span>
<span id="cb19-16"><a href="#cb19-16" aria-hidden="true" tabindex="-1"></a>        <span class="op">{</span> <span class="va">config</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>:</span>
<span id="cb19-17"><a href="#cb19-17" aria-hidden="true" tabindex="-1"></a>        <span class="op">{</span></span>
<span id="cb19-18"><a href="#cb19-18" aria-hidden="true" tabindex="-1"></a>          <span class="va">imports</span> <span class="op">=</span> <span class="op">[</span> ssoTestModule <span class="op">];</span></span>
<span id="cb19-19"><a href="#cb19-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb19-20"><a href="#cb19-20" aria-hidden="true" tabindex="-1"></a>          <span class="co"># https://github.com/ibizaman/selfhostblocks/issues/843</span></span>
<span id="cb19-21"><a href="#cb19-21" aria-hidden="true" tabindex="-1"></a>          <span class="va">shb</span>.<span class="va">jellyfin</span>.<span class="va">admin</span>.<span class="va">username</span> <span class="op">=</span> lib.mkForce <span class="st">&quot;jellyfin2&quot;</span><span class="op">;</span></span>
<span id="cb19-22"><a href="#cb19-22" aria-hidden="true" tabindex="-1"></a>       <span class="op">};</span></span>
<span id="cb19-23"><a href="#cb19-23" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb19-24"><a href="#cb19-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb19-25"><a href="#cb19-25" aria-hidden="true" tabindex="-1"></a>    <span class="va">nodes</span>.<span class="va">client</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb19-26"><a href="#cb19-26" aria-hidden="true" tabindex="-1"></a>      <span class="va">imports</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb19-27"><a href="#cb19-27" aria-hidden="true" tabindex="-1"></a>        clientLoginModule</span>
<span id="cb19-28"><a href="#cb19-28" aria-hidden="true" tabindex="-1"></a>      <span class="op">];</span></span>
<span id="cb19-29"><a href="#cb19-29" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb19-30"><a href="#cb19-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb19-31"><a href="#cb19-31" aria-hidden="true" tabindex="-1"></a>    <span class="va">testScript</span> <span class="op">=</span></span>
<span id="cb19-32"><a href="#cb19-32" aria-hidden="true" tabindex="-1"></a>      <span class="kw">let</span></span>
<span id="cb19-33"><a href="#cb19-33" aria-hidden="true" tabindex="-1"></a>        <span class="va">specializationsServer</span> <span class="op">=</span> <span class="st">&quot;</span><span class="sc">${</span>nodes.server.system.build.toplevel<span class="sc">}</span><span class="st">/specialisation&quot;</span><span class="op">;</span></span>
<span id="cb19-34"><a href="#cb19-34" aria-hidden="true" tabindex="-1"></a>      <span class="kw">in</span></span>
<span id="cb19-35"><a href="#cb19-35" aria-hidden="true" tabindex="-1"></a>      <span class="st">&#39;&#39;</span></span>
<span id="cb19-36"><a href="#cb19-36" aria-hidden="true" tabindex="-1"></a><span class="st">        def switch_to_specialization(name):</span></span>
<span id="cb19-37"><a href="#cb19-37" aria-hidden="true" tabindex="-1"></a><span class="st">            with subtest(f&quot;switch specialization to {name}&quot;):</span></span>
<span id="cb19-38"><a href="#cb19-38" aria-hidden="true" tabindex="-1"></a><span class="st">                server.succeed(f&#39;</span><span class="sc">${</span>specializationsServer<span class="sc">}</span><span class="st">/{name}/bin/switch-to-configuration test&#39;)</span></span>
<span id="cb19-39"><a href="#cb19-39" aria-hidden="true" tabindex="-1"></a><span class="st">                server.wait_for_unit(&quot;multi-user.target&quot;)</span></span>
<span id="cb19-40"><a href="#cb19-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb19-41"><a href="#cb19-41" aria-hidden="true" tabindex="-1"></a><span class="st">        start_all()</span></span>
<span id="cb19-42"><a href="#cb19-42" aria-hidden="true" tabindex="-1"></a><span class="st">        server.wait_for_unit(&quot;multi-user.target&quot;)</span></span>
<span id="cb19-43"><a href="#cb19-43" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb19-44"><a href="#cb19-44" aria-hidden="true" tabindex="-1"></a><span class="st">        switch_to_specialization(&quot;ldap&quot;)</span></span>
<span id="cb19-45"><a href="#cb19-45" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb19-46"><a href="#cb19-46" aria-hidden="true" tabindex="-1"></a><span class="st">        # This sleep is needed because Jellyfin reports it is ready before it truly is ready.</span></span>
<span id="cb19-47"><a href="#cb19-47" aria-hidden="true" tabindex="-1"></a><span class="st">        # See ticket https://github.com/ibizaman/selfhostblocks/issues/842</span></span>
<span id="cb19-48"><a href="#cb19-48" aria-hidden="true" tabindex="-1"></a><span class="st">        print(&quot;sleeping 60 seconds...&quot;)</span></span>
<span id="cb19-49"><a href="#cb19-49" aria-hidden="true" tabindex="-1"></a><span class="st">        import time</span></span>
<span id="cb19-50"><a href="#cb19-50" aria-hidden="true" tabindex="-1"></a><span class="st">        time.sleep(60)</span></span>
<span id="cb19-51"><a href="#cb19-51" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb19-52"><a href="#cb19-52" aria-hidden="true" tabindex="-1"></a><span class="st">        with subtest(&quot;find alice&quot;):</span></span>
<span id="cb19-53"><a href="#cb19-53" aria-hidden="true" tabindex="-1"></a><span class="st">            users = json.loads(server.succeed(&quot;sqlite3 /var/lib/jellyfin/data/jellyfin.db -json &#39;SELECT * FROM Users;&#39;&quot;))</span></span>
<span id="cb19-54"><a href="#cb19-54" aria-hidden="true" tabindex="-1"></a><span class="st">            aliceUsers = [u for u in users if u[&quot;Username&quot;] == &quot;alice&quot;]</span></span>
<span id="cb19-55"><a href="#cb19-55" aria-hidden="true" tabindex="-1"></a><span class="st">            if len(aliceUsers) != 1:</span></span>
<span id="cb19-56"><a href="#cb19-56" aria-hidden="true" tabindex="-1"></a><span class="st">                raise Exception(f&quot;Unexpected number of users for alice, got {len(aliceUsers)}\n{json.dumps(users, indent=4)}&quot;)</span></span>
<span id="cb19-57"><a href="#cb19-57" aria-hidden="true" tabindex="-1"></a><span class="st">            alice = aliceUsers[0]</span></span>
<span id="cb19-58"><a href="#cb19-58" aria-hidden="true" tabindex="-1"></a><span class="st">            print(f&quot;Users: \n{json.dumps(users, indent=4)}&quot;)</span></span>
<span id="cb19-59"><a href="#cb19-59" aria-hidden="true" tabindex="-1"></a><span class="st">            if alice[&quot;AuthenticationProviderId&quot;] != &quot;Jellyfin.Plugin.LDAP_Auth.LdapAuthenticationProviderPlugin&quot;:</span></span>
<span id="cb19-60"><a href="#cb19-60" aria-hidden="true" tabindex="-1"></a><span class="st">                raise Exception(&quot;Unexpected authentication provider id, got {alice[&#39;AuthenticationProviderId&#39;]}&quot;)</span></span>
<span id="cb19-61"><a href="#cb19-61" aria-hidden="true" tabindex="-1"></a><span class="st">            if alice[&quot;PasswordResetProviderId&quot;] != &quot;Jellyfin.Plugin.LDAP_Auth.LdapAuthenticationProviderPlugin&quot;:</span></span>
<span id="cb19-62"><a href="#cb19-62" aria-hidden="true" tabindex="-1"></a><span class="st">                raise Exception(&quot;Unexpected password resiet provider id, got {alice[&#39;PasswordResetProviderId&#39;]}&quot;)</span></span>
<span id="cb19-63"><a href="#cb19-63" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb19-64"><a href="#cb19-64" aria-hidden="true" tabindex="-1"></a><span class="st">        switch_to_specialization(&quot;sso&quot;)</span></span>
<span id="cb19-65"><a href="#cb19-65" aria-hidden="true" tabindex="-1"></a><span class="st">        time.sleep(60)</span></span>
<span id="cb19-66"><a href="#cb19-66" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb19-67"><a href="#cb19-67" aria-hidden="true" tabindex="-1"></a><span class="st">        with subtest(&quot;find alice&quot;):</span></span>
<span id="cb19-68"><a href="#cb19-68" aria-hidden="true" tabindex="-1"></a><span class="st">            users = json.loads(server.succeed(&quot;sqlite3 /var/lib/jellyfin/data/jellyfin.db -json &#39;SELECT * FROM Users;&#39;&quot;))</span></span>
<span id="cb19-69"><a href="#cb19-69" aria-hidden="true" tabindex="-1"></a><span class="st">            aliceUsers = [u for u in users if u[&quot;Username&quot;] == &quot;alice&quot;]</span></span>
<span id="cb19-70"><a href="#cb19-70" aria-hidden="true" tabindex="-1"></a><span class="st">            if len(aliceUsers) != 1:</span></span>
<span id="cb19-71"><a href="#cb19-71" aria-hidden="true" tabindex="-1"></a><span class="st">                raise Exception(f&quot;Unexpected number of users for alice, got {len(aliceUsers)}\n{json.dumps(users, indent=4)}&quot;)</span></span>
<span id="cb19-72"><a href="#cb19-72" aria-hidden="true" tabindex="-1"></a><span class="st">            alice = aliceUsers[0]</span></span>
<span id="cb19-73"><a href="#cb19-73" aria-hidden="true" tabindex="-1"></a><span class="st">            print(f&quot;Users: \n{json.dumps(users, indent=4)}&quot;)</span></span>
<span id="cb19-74"><a href="#cb19-74" aria-hidden="true" tabindex="-1"></a><span class="st">            if alice[&quot;AuthenticationProviderId&quot;] != &quot;Jellyfin.Plugin.LDAP_Auth.LdapAuthenticationProviderPlugin&quot;:</span></span>
<span id="cb19-75"><a href="#cb19-75" aria-hidden="true" tabindex="-1"></a><span class="st">                raise Exception(&quot;Unexpected authentication provider id, got {alice[&#39;AuthenticationProviderId&#39;]}&quot;)</span></span>
<span id="cb19-76"><a href="#cb19-76" aria-hidden="true" tabindex="-1"></a><span class="st">            if alice[&quot;PasswordResetProviderId&quot;] != &quot;Jellyfin.Plugin.LDAP_Auth.LdapAuthenticationProviderPlugin&quot;:</span></span>
<span id="cb19-77"><a href="#cb19-77" aria-hidden="true" tabindex="-1"></a><span class="st">                raise Exception(&quot;Unexpected password resiet provider id, got {alice[&#39;PasswordResetProviderId&#39;]}&quot;)</span></span>
<span id="cb19-78"><a href="#cb19-78" aria-hidden="true" tabindex="-1"></a><span class="st">      &#39;&#39;</span><span class="op">;</span></span>
<span id="cb19-79"><a href="#cb19-79" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb19-80"><a href="#cb19-80" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>The meat of the test is the if clause <code>len(aliceUsers) != 1</code>. This makes sure only one <code>alice</code> is ever found in the database. There are some extra checks to make sure Alice was created with the LDAP provider but those are maybe superfluous, we’ll see in the long run.</p>
<p>In the test, we create 2 specializations. One for LDAP integration and one for SSO. We switch from one to the other mid test. So on top of making sure only one use is created, we make sure changing the configuration works fine too.</p>
<p>You’ll also notice comments linking to issues <a href="https://github.com/ibizaman/selfhostblocks/issues/842">842</a> and <a href="https://github.com/ibizaman/selfhostblocks/issues/843">843</a>. Those are quirks of Jellyfin I needed to circumvent. One day I’ll get back and fix them.</p>
<h2 id="conclusion-1">Conclusion</h2>
<p>We’re really at the end here. If you followed along, well, thanks! It was a long and frustrated journey to get this result but I don’t regret it. I like those tests and plan to cover more services.</p>
<p>Feel free to <a href="https://github.com/ibizaman/selfhostblocks">check the repo</a> or join the <a href="https://matrix.to/#/#selfhostblocks:matrix.org">Matrix chat</a> to talk about it.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>ESPSomfy on Atom Lite with NixOS</title>
    <link href="https://blog.tiserbox.com//posts/2026-07-04-esp-somfy-on-atom-lite-with-nix-os.html" />
    <id>https://blog.tiserbox.com//posts/2026-07-04-esp-somfy-on-atom-lite-with-nix-os.html</id>
    <published>2026-07-04T00:00:00Z</published>
    <updated>2026-07-04T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on July  4, 2026
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>, <a title="All pages tagged &#39;esp&#39;." href="/tags/esp.html">esp</a>, <a title="All pages tagged &#39;iot&#39;." href="/tags/iot.html">iot</a>
	  
	</section>
    <section>
        <p>I have an <a href="https://shop.m5stack.com/products/atom-lite-esp32-development-kit">Atom Lite</a> and wanted to use it to install <a href="https://github.com/rstrouse/ESPSomfy-RTS">ESPSomfy-rts</a> on it with a <a href="https://github.com/rstrouse/ESPSomfy-RTS/wiki/Simple-ESPSomfy-RTS-device#picking-a-cc1101-transceiver">E07-M1101D</a> antenna.</p>
<p>The goal is to control Somfy covers with Home Assistant.</p>
<p>If you have the same setup, you might enjoy some opinionated instructions.
So these are the steps I followed.</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Atom%20Lite.webp" alt="Atom Lite Spec" />
<figcaption aria-hidden="true">Atom Lite Spec</figcaption>
</figure>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Finished%20Product%202.jpg" alt="The finished product" />
<figcaption aria-hidden="true">The finished product</figcaption>
</figure>
<h1 id="installation">Installation</h1>
<p>These are the steps are followed to flash my Atom Lite.</p>
<h2 id="plug-the-atom-lite-in-the-computer">1. Plug the Atom Lite in the computer</h2>
<p>With a USB cable!</p>
<h2 id="find-the-atom-lite">2. Find the Atom Lite</h2>
<p>If you have multiple <code>ttyUSB</code> devices,
repeat the command after disconnecting the Atom Lite and see which one disappeared.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ls <span class="at">-l</span> /dev/ttyUSB<span class="pp">*</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="ex">crw-rw----</span> 1 root dialout 188, 0 Jun  4 12:02 /dev/ttyUSB0 </span></code></pre></div>
<h2 id="enter-shell-with-needed-tools">3. Enter shell with needed tools</h2>
<p>Yay for nix!</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">nix-shell</span> <span class="at">-p</span> unzip curl esptool</span></code></pre></div>
<h2 id="check-atom-lite-is-flashable">4. Check Atom Lite is flashable</h2>
<p>For good measure but I suppose you could skip this step.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo esptool <span class="at">--port</span> /dev/ttyUSB0 flash-id</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="ex">esptool</span> v5.2.0</span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Connected</span> to ESP32 on /dev/ttyUSB0:</span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="ex">Chip</span> type:          ESP32-PICO-D4 <span class="er">(</span><span class="ex">revision</span> v1.1<span class="kw">)</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="ex">Features:</span>           Wi-Fi, BT, Dual Core + LP Core, 240MHz, Embedded Flash, Vref calibration in eFuse, Coding Scheme None ta</span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="ex">Crystal</span> frequency:  40MHz</span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="ex">MAC:</span>                64:b7:08:b7:98:08</span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a><span class="ex">Stub</span> flasher running.</span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a><span class="ex">Flash</span> Memory Information:</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a><span class="ex">=========================</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a><span class="ex">Manufacturer:</span> c8</span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a><span class="ex">Device:</span> 4016</span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a><span class="ex">Detected</span> flash size: 4MB</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a><span class="ex">Flash</span> voltage set by a strapping pin: 3.3V</span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a><span class="ex">Hard</span> resetting via RTS pin...</span></code></pre></div>
<h2 id="erase-the-atom-lite">5. Erase the Atom Lite</h2>
<p>I’m not sure this is needed but it won’t hurt.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo esptool <span class="at">--port</span> /dev/ttyUSB0 erase-flash</span></code></pre></div>
<h2 id="download-the-firmware">6. Download the firmware</h2>
<p>From the <a href="https://github.com/rstrouse/ESPSomfy-RTS/releases">release page</a>, download the <code>*.onboard.esp32.bin.zip</code> archive
or copy paste the curl command and update the release version:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> curl <span class="at">-LO</span> https://github.com/rstrouse/ESPSomfy-RTS/releases/download/v2.4.7/SomfyController.onboard.esp32.bin.zip</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> unzip SomfyController.onboard.esp32.bin.zip</span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Archive:</span>  SomfyController.onboard.esp32.bin.zip</span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>  <span class="ex">inflating:</span> SomfyController.onboard.esp32.bin </span></code></pre></div>
<h2 id="flash-the-atom-lite">7. Flash the Atom Lite</h2>
<p>This step took about a minute for me.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo esptool <span class="at">--port</span> /dev/ttyUSB0 <span class="dt">\</span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>    write-flash 0x0 SomfyController.onboard.esp32.bin</span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="ex">esptool</span> v5.2.0</span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a><span class="ex">Connected</span> to ESP32 on /dev/ttyUSB0:</span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a><span class="ex">Chip</span> type:          ESP32-PICO-D4 <span class="er">(</span><span class="ex">revision</span> v1.1<span class="kw">)</span></span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a><span class="ex">Features:</span>           Wi-Fi, BT, Dual Core + LP Core, 240MHz, Embedded Flash, Vref calibration in eFuse, Coding Scheme None</span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a><span class="ex">Crystal</span> frequency:  40MHz</span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a><span class="ex">MAC:</span>                64:b7:08:b7:98:08</span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a><span class="ex">Stub</span> flasher running.</span>
<span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-12"><a href="#cb6-12" aria-hidden="true" tabindex="-1"></a><span class="ex">Configuring</span> flash size...</span>
<span id="cb6-13"><a href="#cb6-13" aria-hidden="true" tabindex="-1"></a><span class="ex">Flash</span> will be erased from 0x00000000 to 0x003effff...</span>
<span id="cb6-14"><a href="#cb6-14" aria-hidden="true" tabindex="-1"></a><span class="ex">Wrote</span> 4128768 bytes <span class="er">(</span><span class="ex">929394</span> compressed<span class="kw">)</span> <span class="ex">at</span> 0x00000000 in 91.0 seconds <span class="er">(</span><span class="ex">363.1</span> kbit/s<span class="kw">)</span><span class="bu">.</span></span>
<span id="cb6-15"><a href="#cb6-15" aria-hidden="true" tabindex="-1"></a><span class="ex">Hash</span> of data verified.</span>
<span id="cb6-16"><a href="#cb6-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-17"><a href="#cb6-17" aria-hidden="true" tabindex="-1"></a><span class="ex">Hard</span> resetting via RTS pin...</span></code></pre></div>
<h2 id="verify-installation">8. Verify Installation</h2>
<p>Now, the Atom Lite will create its own WiFi access point with name <code>ESPSomfyRTS</code>.</p>
<h1 id="hardware">Hardware</h1>
<h2 id="connect-to-atom-lites-wifi-ap">1. Connect to Atom Lite’s WiFi AP</h2>
<p>Connect to the <code>ESPSomfyRTS</code> network.</p>
<h2 id="load-espsomfyrts-web-interface">2. Load ESPSomfyRTS web interface</h2>
<p>Go to <a href="http://192.168.4.1">http://192.168.4.1</a>.
The IP address is hardcoded.
The webpage should show “RADIO NOT INITIALIZED”.</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Web1.png" alt="See? Told you it would say RADIO NOT INITIALIZED" />
<figcaption aria-hidden="true">See? Told you it would say RADIO NOT INITIALIZED</figcaption>
</figure>
<h2 id="configure-device">3. Configure device</h2>
<p>Click on the gear icon.
Go through the all the tabs first, we’ll configure the radio afterwards.</p>
<p>Don’t forget to click “save” before switching tab otherwise your changes will be lost.</p>
<p>You know what? Click twice on “save”, for good measure.</p>
<ul>
<li><code>System &gt; Options</code></li>
</ul>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Web2.png" alt="Interesting" />
<figcaption aria-hidden="true">Interesting</figcaption>
</figure>
<ul>
<li><code>System &gt; Security</code></li>
</ul>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Web3.png" alt="Oh security, good!" />
<figcaption aria-hidden="true">Oh security, good!</figcaption>
</figure>
<p>I store the username and password in Bitwarden (using my <a href="https://github.com/ibizaman/selfhostblocks">Vaultwarden self-hosted server</a>).</p>
<ul>
<li><code>Network &gt; Adapter</code></li>
</ul>
<p>You don’t <em>need</em> to setup WiFi, you could always connect to the device through its <code>ESPSomfyRTF</code> WiFi AP
but then you’ll lose on the Home Assistant integration.
So I recommend doing it.</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Web4.png" alt="No, I won’t show you the list of WiFi networks around my house. I know how that works ;)" />
<figcaption aria-hidden="true">No, I won’t show you the list of WiFi networks around my house. I know how that works ;)</figcaption>
</figure>
<ul>
<li><code>Somfy</code></li>
</ul>
<p>Skip the <code>Somfy</code> tab for now, we need to setup the radio first.</p>
<h2 id="wiring">4. Wiring</h2>
<ul>
<li><code>Radio &gt; Transceiver</code></li>
</ul>
<p>We will now configure the pinout with the following wiring.
I chose this wiring specifically to make the physical wiring as simple as possible,
considering the antenna would be located underneath the Atom Lite, with a breadboard in between.</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Layout.jpg" alt="Narrator: “It was not simple”." />
<figcaption aria-hidden="true">Narrator: “It was not simple”.</figcaption>
</figure>
<p>The pinout is shown here:</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Pinout.jpg" alt="Good old pen and paper does the trick." />
<figcaption aria-hidden="true">Good old pen and paper does the trick.</figcaption>
</figure>
<p>Combining both diagrams, we can see a quite neat wiring:</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Combined%20Diagram.jpg" alt="You see it too, right? A lot of straight lines = simple." />
<figcaption aria-hidden="true">You see it too, right? A lot of straight lines = simple.</figcaption>
</figure>
<p>The correspondance is:</p>
<table>
<thead>
<tr class="header">
<th>Antenna</th>
<th>Atom Lite</th>
<th>ESPSomfyRTS</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>(1) Ground</td>
<td>Ground</td>
<td>-</td>
</tr>
<tr class="even">
<td>(2) 3V3</td>
<td>3V3</td>
<td>-</td>
</tr>
<tr class="odd">
<td>(3) GD0</td>
<td>GPIO 33</td>
<td>TX</td>
</tr>
<tr class="even">
<td>(4) CSN</td>
<td>GPIO 23</td>
<td>CSN</td>
</tr>
<tr class="odd">
<td>(5) SCK</td>
<td>GPIO 25</td>
<td>SCLK</td>
</tr>
<tr class="even">
<td>(6) MOSI</td>
<td>GPIO 19</td>
<td>MOSI</td>
</tr>
<tr class="odd">
<td>(7) MISO</td>
<td>GPIO 21</td>
<td>MISO</td>
</tr>
<tr class="even">
<td>(8) GDO2</td>
<td>GPIO 22</td>
<td>RX</td>
</tr>
</tbody>
</table>
<p>Now, we can set the pinout correspondance in the web UI:</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Web5.png" alt="The pinout set to their correct values in the web UI." />
<figcaption aria-hidden="true">The pinout set to their correct values in the web UI.</figcaption>
</figure>
<h2 id="test">5. Test</h2>
<p>I’ll do first a test using a solderless breadboard.
I’m using the male and female socket headers I’ll use later when soldering.</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Atom%20with%20pins.jpg" alt="Atom Lite next to its socket headers." />
<figcaption aria-hidden="true">Atom Lite next to its socket headers.</figcaption>
</figure>
<p>The breadboard setup looks like so.
I had a hard time wrapping my head around where to put the wires
because I needed to mirror the diagram from above.</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Breadboard%20setup.jpg" alt="Solderless breadboard all wired up. I removed the Atom Lite so you could see the socket headers. I did put it back before continuing. I swear I did not forget." />
<figcaption aria-hidden="true">Solderless breadboard all wired up.
I removed the Atom Lite so you could see the socket headers.
I did put it back before continuing.
I swear I did not forget.</figcaption>
</figure>
<p>Now, let’s enable the radio in the <code>Radio &gt; Transceiver</code> tab in the web interface
by toggling the “Enable Radio” checkbox.
<strong>Then, click save.</strong></p>
<p>To see if everything works fine, go to the <code>Radio &gt; Logs</code> tab and press on the official Somfy remote,
you should see some log lines like so:</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Logs.png" alt="Logs" />
<figcaption aria-hidden="true">Logs</figcaption>
</figure>
<p>You could also configure already the rooms and covers but I’ll leave that for after the soldering.</p>
<h2 id="soldering">6. Soldering</h2>
<p>Since everything went well, it’s time to solder.</p>
<p>Comparing the breadboard used for soldering to the diagram,
you’ll see there’s an extra connection that we need to get rid of
between the <code>GD0</code> pin from the antenna and the <code>5V</code> pin from the Atom Lite.
I marked it in red.</p>
<p>Note the diagram is mirrored at the pink line.</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Soldering%20Plan.jpg" alt="Everything is going to plan." />
<figcaption aria-hidden="true">Everything is going to plan.</figcaption>
</figure>
<p>I used a drill with the smallest wood drill bit I had to cut the connection.</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Soldering%20Cut.jpg" alt="Hey, at least I put something between the breadboard and the table." />
<figcaption aria-hidden="true">Hey, at least I put something between the breadboard and the table.</figcaption>
</figure>
<p>And a dremel to cut the female header sockets for the antenna.</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Antenna%20pins.jpg" alt="If you close your eyes, you could think I did a great job cutting here." />
<figcaption aria-hidden="true">If you close your eyes, you could think I did a great job cutting here.</figcaption>
</figure>
<p>Now that we have all the pieces set, let’s solder!</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Soldering.jpg" alt="Gravity and perfect equilibrium is my third hand." />
<figcaption aria-hidden="true">Gravity and perfect equilibrium is my third hand.</figcaption>
</figure>
<p>We will need two wires to connect the antenna and the Atom Lite.
After decades of stripping wires with a pair of scissors,
I invested in a wire stripper and what a quality of life improvement that is!</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Strip%20wire.jpg" alt="My newly faithful wire stripper!" />
<figcaption aria-hidden="true">My newly faithful wire stripper!</figcaption>
</figure>
<p>I did a horrendous job at the soldering so I will only allow you to look it from afar.</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Finished%20product.jpg" alt="It’s still my best soldering job to date though…" />
<figcaption aria-hidden="true">It’s still my best soldering job to date though…</figcaption>
</figure>
<p>I did cut the extra board with a dremel.</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Dremel.jpg" alt="I did the cutting outside so I wouldn’t suffer from the dust." />
<figcaption aria-hidden="true">I did the cutting outside so I wouldn’t suffer from the dust.</figcaption>
</figure>
<p>The finished product</p>
<figure>
<img src="/images/2026-06-04-esp-somfy-on-atom-lite-with-nix-os/Finished%20Product%202.jpg" alt=":)" />
<figcaption aria-hidden="true">:)</figcaption>
</figure>
<h1 id="next-steps">Next Steps</h1>
<p>Now we can plug back the Atom Lite in a USB charger and configure the covers.</p>
<p>I’ll leave you to <a href="espsomfy-rts">the official wiki</a> because now you have all that’s needed to follow it.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Bisect Experiment at the PR Level</title>
    <link href="https://blog.tiserbox.com//posts/2026-05-12-bisect-experiment-at-the-pr-level.html" />
    <id>https://blog.tiserbox.com//posts/2026-05-12-bisect-experiment-at-the-pr-level.html</id>
    <published>2026-05-12T00:00:00Z</published>
    <updated>2026-05-12T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on May 12, 2026
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>, <a title="All pages tagged &#39;forge action&#39;." href="/tags/forge%20action.html">forge action</a>, <a title="All pages tagged &#39;auto update&#39;." href="/tags/auto%20update.html">auto update</a>
	  
	</section>
    <section>
        <!--toc:start-->
<ul>
<li><a href="#first-automation-attempt">First Automation Attempt</a></li>
<li><a href="#naive-commit-selection">Naive Commit Selection</a></li>
<li><a href="#smarter-commit-selection">Smarter Commit Selection</a></li>
<li><a href="#manual-run">Manual Run</a></li>
<li><a href="#links">Links</a></li>
<li><a href="#possible-improvement">Possible Improvement</a></li>
<li><a href="#upstreaming">Upstreaming</a>
<!--toc:end--></li>
</ul>
<p>I maintain the <a href="https://github.com/ibizaman/selfhostblocks">SelfHostBlocks</a> project.
It relies on <code>nixos/nixpkgs/unstable</code> as its sole dependency
and it attempts to follow the latest commit as close as possible.</p>
<p>If you’re wondering why, it’s because I like the shiny new stuff.</p>
<h1 id="first-automation-attempt">First Automation Attempt</h1>
<p>To automate this update process,
I initially created a crude workflow which worked like follows:</p>
<ol type="1">
<li>Workflow runs at midnight.
It runs <code>nix flake update nixpkgs</code>
and if this results in a change in the <code>flake.lock</code> file,
it commits that and creates a PR with the <code>automerge</code> label.
If a PR already exists, it instead force pushes to that PR.</li>
<li>When the PR is created with the <code>automerge</code> label,
the auto-merge feature of Github gets activated.</li>
<li>Custom NixOS VM tests run on this PR and if they all succeed,
the <code>flake.lock</code> update PR gets merged automatically.</li>
</ol>
<p>I wrote more extensively about this in <a href="https://blog.tiserbox.com/posts/2023-12-25-automated-flake-lock-update-pull-requests-and-merging.html">another blog post</a>.</p>
<h1 id="naive-commit-selection">Naive Commit Selection</h1>
<p>Although this is automated and did produce PRs that get auto-merged,
more often than not the commit selection is too naive.
Constantly trying to update to the tip of unstable can lead to test failure.</p>
<p>The most frequent reasons for failure are:</p>
<ol type="1">
<li>A package I rely on cannot be built.</li>
<li>A module option got updated leading to some breaking change.</li>
<li>The behavior of a module changed and broke an assumption I had.</li>
</ol>
<p>The workflow being naive does not realize the PR it had created has a failure
and it will happily update the <code>flake.lock</code> file to the latest unstable commit.
That might fix the failure when a package cannot be built
but will never fix the other two.</p>
<p>So when that happens, this update PR will be failing until I take a look at it
which is annoying.</p>
<h1 id="smarter-commit-selection">Smarter Commit Selection</h1>
<p>I changed the workflow to check if the PR already existed
and if so to check its test status.</p>
<p>If the tests are failing,
it will then clone the nixpkgs repo
and choose the commit in the middle between the last good commit
(the tip of my project’s main branch)
and the latest failing commit
(the PR’s nixpkgs commit).</p>
<p>It then uses this new middle commit to run a flake update
similarly as before and pushes that change to the existing PR.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="ex">nix</span> flake update nixpkgs <span class="dt">\</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="at">--override-input</span> <span class="dt">\</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>    nixpkgs <span class="dt">\</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>    github:nixos/nixpkgs/<span class="va">$commit</span></span></code></pre></div>
<p>If the PR still fails, on next run it will choose again a commit in the middle between the
last good commit and the last failing one.</p>
<p>If the PR succeeds, then it gets auto-merged and the cycle repeats.
The workflow will see no PR is opened so it will update the <code>flake.lock</code> file
using the latest commit on <code>unstable</code>.</p>
<p>If the tests in the PR continuously fail,
there might be no more commit to bisect on as the good commit will become the parent of bad commit.
In that case, the workflow will go back to the latest commit on <code>unstable</code> and will try anew.</p>
<h1 id="manual-run">Manual Run</h1>
<p>I’m running the workflow every 3 hours
but it is possible to run the workflow manually to speed up the process.
This will be useful when I’m catering it and trying to fix the issues that appear.</p>
<p>When running the workflow manually, I also added an option to bisect a commit <em>in the future</em>.
That is to choose a commit between the currently failing one in the PR and the tip of nixpkgs unstable.</p>
<h1 id="links">Links</h1>
<ul>
<li><a href="https://github.com/ibizaman/selfhostblocks/actions/runs/25743636941/workflow">Github workflow</a></li>
<li>The <a href="https://github.com/ibizaman/selfhostblocks/blob/main/.github/workflows/update-flake-lock-pr.nix">nix/bash script used in the workflow</a></li>
<li><a href="https://github.com/ibizaman/selfhostblocks/actions/runs/25743636941/job/75600738546">Example run</a></li>
<li>… which produced <a href="https://github.com/ibizaman/selfhostblocks/pull/708">this PR</a></li>
<li>Another PR <a href="https://github.com/ibizaman/selfhostblocks/pull/702">with some failures</a></li>
</ul>
<h1 id="possible-improvement">Possible Improvement</h1>
<p>Always choosing a commit in the past is not necessarily the best strategy.
It could be worth parsing the log output and figuring out what failed.
Indeed, when a package fails to build, choosing a commit in the future might work too
as a broken package is usually fixed in the next couple of days at most.</p>
<p>Choosing a commit in the future is possible thanks to the manual run mode
but this could be made automatic.</p>
<p>Better selection of commits could be helped by using one of the tools in the <a href="https://wiki.nixos.org/wiki/Bisecting">wiki</a>.
<code>Hydrasect</code> is particularly interesting to be able to pick a commit with cached derivations.</p>
<h1 id="upstreaming">Upstreaming</h1>
<p>I’m not even sure this is a good idea and will be useful in practice.
Time will tell.</p>
<p>If others still might like a tool like this,
it could be extracted as its own repo but it should be made generic first.
It should be able to let the user choose which flake input to update.
It is hardcoded to “nixpkgs” now.</p>
<p>I’m also not sure how to best handle updating multiple inputs
as the number of combination of commits to try would explode quickly.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Use Bitwarden as auth source in Emacs</title>
    <link href="https://blog.tiserbox.com//posts/2026-05-01-use-bitwarden-as-auth-source-in-emacs.html" />
    <id>https://blog.tiserbox.com//posts/2026-05-01-use-bitwarden-as-auth-source-in-emacs.html</id>
    <published>2026-05-01T00:00:00Z</published>
    <updated>2026-05-01T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on May  1, 2026
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>, <a title="All pages tagged &#39;emacs&#39;." href="/tags/emacs.html">emacs</a>
	  
	</section>
    <section>
        <!--toc:start-->
<ul>
<li><a href="#1-setup-emacs">1. Setup Emacs</a></li>
<li><a href="#2-setup-nix-config">2. Setup nix config</a></li>
<li><a href="#3-configure-secret">3. Configure secret</a></li>
<li><a href="#misc">Misc</a>
<ul>
<li><a href="#clear-auth-sources-cache">Clear auth-sources cache</a></li>
<li><a href="#self-hosted-bitwarden">Self-hosted Bitwarden</a></li>
<li><a href="#log-into-vault">Log into vault</a></li>
<li><a href="#unlock-vault">Unlock vault</a></li>
<li><a href="#sync-vault">Sync vault</a>
<!--toc:end--></li>
</ul></li>
</ul>
<p>From Magit, it is possible to create pull requests.
Of course, we need to setup a secret token to be able to create a pull request on the forge.</p>
<p>One way is to store the secret in a file read by emacs.
By default, <code>auth-sources</code> variable is:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode lisp"><code class="sourceCode commonlisp"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>(<span class="st">&quot;~/.authinfo&quot;</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;~/.authinfo.gpg&quot;</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;~/.netrc&quot;</span>)</span></code></pre></div>
<p>The issue is these files store the secrets in clear text.
Instead, let’s configure emacs to read the secret from Bitwarden.</p>
<h2 id="setup-emacs">1. Setup Emacs</h2>
<p>We add usage of the <a href="https://github.com/seanfarley/emacs-bitwarden/">emacs-bitwarden</a> package in our <code>init.el</code> file:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="op">(</span>use<span class="op">-</span>package bitwarden</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  :ensure t</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  :config</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>  <span class="op">(</span>bitwarden<span class="op">-</span>auth<span class="op">-</span>source<span class="op">-</span>enable<span class="op">))</span></span></code></pre></div>
<p>The package is only available on github so we’ll need to package it ourselves in the next section.</p>
<h2 id="setup-nix-config">2. Setup nix config</h2>
<p>I use the <a href="https://github.com/nix-community/emacs-overlay">emacs-overlay</a> NixOS module to install emacs.
Assuming the following base nix config:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">emacsWithPackages</span> <span class="op">=</span> pkgs.emacsWithPackagesFromUsePackage <span class="op">{</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>    <span class="va">config</span> <span class="op">=</span> emacs<span class="op">-</span>conf <span class="op">+</span> <span class="st">&quot;/init.el&quot;</span><span class="op">;</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>    <span class="va">defaultInitFile</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>    <span class="va">package</span> <span class="op">=</span> pkgs.emacs<span class="op">-</span>unstable<span class="op">-</span>pgtk<span class="op">;</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="kw">in</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>  config = <span class="op">{</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>    <span class="va">nixpkgs</span>.<span class="va">overlays</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>      emacs-overlay.overlays.emacs</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>    <span class="va">environment</span>.<span class="va">systemPackages</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>      emacsWithPackages</span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span>;</span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a>};</span></code></pre></div>
<p>We can then compile the package ourselves:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode diff"><code class="sourceCode diff"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a> let</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>   emacsWithPackages = pkgs.emacsWithPackagesFromUsePackage {</span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>     config = emacs-conf + &quot;/init.el&quot;;</span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>     defaultInitFile = true;</span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>     package = pkgs.emacs-unstable-pgtk;</span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a> </span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a><span class="va">+    override = final: prev: {</span></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a><span class="va">+      bitwarden = final.melpaBuild {</span></span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a><span class="va">+        pname = &quot;bitwarden&quot;;</span></span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a><span class="va">+        version = &quot;0.1.0&quot;;</span></span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a><span class="va">+        src = pkgs.fetchFromGitHub {</span></span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a><span class="va">+          owner = &quot;seanfarley&quot;;</span></span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a><span class="va">+          repo = &quot;emacs-bitwarden&quot;;</span></span>
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a><span class="va">+          rev = &quot;50c0078d356e0ac0bcaf26b40113700ba4123ec3&quot;;</span></span>
<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a><span class="va">+          hash = &quot;sha256-5zAkoCdBDI7sNLtxOy4t91A4IGV84lD3Cz5nnsQ0P4Q=&quot;;</span></span>
<span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a><span class="va">+        };</span></span>
<span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a><span class="va">+      };</span></span>
<span id="cb4-18"><a href="#cb4-18" aria-hidden="true" tabindex="-1"></a><span class="va">+    };</span></span>
<span id="cb4-19"><a href="#cb4-19" aria-hidden="true" tabindex="-1"></a><span class="va">+  };</span></span>
<span id="cb4-20"><a href="#cb4-20" aria-hidden="true" tabindex="-1"></a> in</span>
<span id="cb4-21"><a href="#cb4-21" aria-hidden="true" tabindex="-1"></a>   config = {</span>
<span id="cb4-22"><a href="#cb4-22" aria-hidden="true" tabindex="-1"></a>     nixpkgs.overlays = [</span>
<span id="cb4-23"><a href="#cb4-23" aria-hidden="true" tabindex="-1"></a>       emacs-overlay.overlays.emacs</span>
<span id="cb4-24"><a href="#cb4-24" aria-hidden="true" tabindex="-1"></a>     ];</span>
<span id="cb4-25"><a href="#cb4-25" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-26"><a href="#cb4-26" aria-hidden="true" tabindex="-1"></a>     environment.systemPackages = [</span>
<span id="cb4-27"><a href="#cb4-27" aria-hidden="true" tabindex="-1"></a>       emacsWithPackages</span>
<span id="cb4-28"><a href="#cb4-28" aria-hidden="true" tabindex="-1"></a><span class="va">+      pkgs.bitwarden-cli</span></span>
<span id="cb4-29"><a href="#cb4-29" aria-hidden="true" tabindex="-1"></a>     ];</span>
<span id="cb4-30"><a href="#cb4-30" aria-hidden="true" tabindex="-1"></a>   };</span>
<span id="cb4-31"><a href="#cb4-31" aria-hidden="true" tabindex="-1"></a> };</span></code></pre></div>
<h2 id="configure-secret">3. Configure secret</h2>
<p>Now we have an emacs with the emacs-bitwarden package loaded.
We can verify this by looking at <code>auth-sources</code>:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode lisp"><code class="sourceCode commonlisp"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>(bitwarden</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;~/.authinfo&quot;</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;~/.authinfo.gpg&quot;</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;~/.netrc&quot;</span>)</span></code></pre></div>
<p>Now, in Magit, we can open a repository and try to create a pull request:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode lisp"><code class="sourceCode commonlisp"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>M-x forge-create-pullreq</span></code></pre></div>
<p>After writing the description, I tried to create the pull request with <code>C-c C-c</code> but got this error message:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="ex">ghub--token:</span> Required Github token</span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a><span class="kw">(</span><span class="st">&quot;ibizaman^forge&quot;</span> for either <span class="st">&quot;api.github.com&quot;</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a><span class="ex">or</span> <span class="st">&quot;api.github.com&quot;</span><span class="kw">)</span> <span class="ex">does</span> not exist.</span></code></pre></div>
<p>This tells me exactly what secret I need to add in Bitwarden.</p>
<p>First, let’s create a PAT in Github in <a href="https://github.com/settings/personal-access-tokens">https://github.com/settings/personal-access-tokens</a>
with fine-grained permissions and give the following permissions:</p>
<ul>
<li><code>Administration - write</code></li>
<li><code>Pull Requests - write</code></li>
</ul>
<p>Now, we store this value in Bitwarden in a secret with:</p>
<ul>
<li><code>Username</code> = <code>ibizaman^forge</code></li>
<li><code>Password</code> = PAT from Github</li>
<li><code>Website</code> = <code>api.github.com</code></li>
<li><code>Name</code> = Name can be anything of your choosing.</li>
</ul>
<p>Before being able to see the password,
<code>auth-sources</code> will need some cleanup:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode lisp"><code class="sourceCode commonlisp"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a>M-x auth-source-forge-all-cached</span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>M-x bitwarden-login</span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a>M-x bitwarden-unlock</span></code></pre></div>
<p>See next section for more explanation on those options.</p>
<h2 id="misc">Misc</h2>
<p>A few more random tips.</p>
<h3 id="clear-auth-sources-cache">Clear auth-sources cache</h3>
<p>Emacs aggressively caches secrets, even failure to find secrets.
Reset the cache with <code>M-x auth-source-forget-all-cached</code>.</p>
<h3 id="self-hosted-bitwarden">Self-hosted Bitwarden</h3>
<p>To use a self-hosted Bitwarden or Vaultwarden, run on the command line:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="ex">bw</span> config server https://<span class="op">&lt;</span>fqdn<span class="op">&gt;</span></span></code></pre></div>
<h3 id="log-into-vault">Log into vault</h3>
<p>To login into bitwarden, run on the command line:</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="ex">bw</span> login</span></code></pre></div>
<p>or in emacs:</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode lisp"><code class="sourceCode commonlisp"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a>M-x bitwarden-login</span></code></pre></div>
<h3 id="unlock-vault">Unlock vault</h3>
<p>Afterwards, you will need to unlock the vault with one of the following commands.
You will for example need to do this on reboot too.</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="ex">bw</span> unlock</span></code></pre></div>
<p>or in emacs:</p>
<div class="sourceCode" id="cb13"><pre class="sourceCode lisp"><code class="sourceCode commonlisp"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a>M-x bitwarden-unlock</span></code></pre></div>
<h3 id="sync-vault">Sync vault</h3>
<p>After updating a secret, to synchronize the vault, run either:</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="ex">bw</span> sync</span></code></pre></div>
<p>or in emacs:</p>
<div class="sourceCode" id="cb15"><pre class="sourceCode lisp"><code class="sourceCode commonlisp"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a>M-x bitwarden-sync</span></code></pre></div>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Qemu tips for cdrom, ide and nvme hard drives</title>
    <link href="https://blog.tiserbox.com//posts/2025-06-01-qemu-tips-for-cdrom-ide-and-nvme-hard-drives.html" />
    <id>https://blog.tiserbox.com//posts/2025-06-01-qemu-tips-for-cdrom-ide-and-nvme-hard-drives.html</id>
    <published>2025-06-01T00:00:00Z</published>
    <updated>2025-06-01T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on June  1, 2025
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;qemu&#39;." href="/tags/qemu.html">qemu</a>, <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <!--toc:start-->
<ul>
<li><a href="#nix">Nix</a></li>
<li><a href="#cdrom-sr0">CDROM (sr0)</a></li>
<li><a href="#iso-file-in-nix">Iso file in Nix</a></li>
<li><a href="#efi">EFI</a></li>
<li><a href="#sata-sdx-drive">SATA (sdX) drive</a></li>
<li><a href="#nvme-nvmex-drive">NVMe (nvmeX) drive</a>
<!--toc:end--></li>
</ul>
<p>Qemu’s documentation is a bit scattered around the web.</p>
<p>I managed to follow the <a href="https://www.qemu.org/docs/master/system/devices/nvme.html">nvme</a> documentation
but just couldn’t find anything in <a href="https://www.qemu.org">qemu.org</a> about
SATA drives apart from some small snippets in other sections.</p>
<p>Some other examples could be found on StackOverflow.</p>
<p>This blog post regroups snippets geared towards concrete examples
to start a Qemu VM with EFI, SATA, NVMe and/or cdrom.</p>
<h2 id="nix">Nix</h2>
<p>You can test with:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>nix run nixpkgs<span class="co">#qemu -- &lt;options go here&gt;</span></span></code></pre></div>
<p>For EFI though, using the following script will help you to get easily the
needed firmware:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">nixos-qemu</span> <span class="op">=</span> pkgs.callPackage <span class="st">&quot;</span><span class="sc">${</span>pkgs.path<span class="sc">}</span><span class="st">/nixos/lib/qemu-common.nix&quot;</span> <span class="op">{};</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">qemu</span> <span class="op">=</span> nixos<span class="op">-</span>qemu.qemuBinary pkgs.qemu<span class="op">;</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="kw">in</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>  pkgs.writeShellScriptBin <span class="st">&quot;script&quot;</span> <span class="st">&#39;&#39;</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a><span class="st">  </span><span class="sc">${</span>qemu<span class="sc">}</span><span class="st"> ... &lt;options go here&gt;</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a><span class="st">  &#39;&#39;</span>;</span></code></pre></div>
<p>We purposely use the facilities provided by <a href="https://github.com/NixOS/nixpkgs/blob/master/nixos/lib/qemu-common.nix#L35">qemu-common.nix</a> because
all the wiring is done for us already for EFI and some options
matching our architecture are set.</p>
<h2 id="cdrom-sr0">CDROM (sr0)</h2>
<p>Assuming you have a ISO file available at <code>./iso</code>.
See <a href="#iso-file-in-nix">next section</a> if you want to create an ISO file with nix.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="ex">--drive</span> media=cdrom,format=raw,readonly=on,file=./iso</span></code></pre></div>
<p>It will show up as <code>sr0</code>.</p>
<h2 id="iso-file-in-nix">Iso file in Nix</h2>
<p>With <a href="https://github.com/nix-community/nixos-generators">nixos-generators</a>:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">iso</span> <span class="op">=</span> nixos<span class="op">-</span>generators.nixosGenerate <span class="op">{</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>    <span class="kw">inherit</span> system<span class="op">;</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>    <span class="va">format</span> <span class="op">=</span> <span class="st">&quot;install-iso&quot;</span><span class="op">;</span></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>    <span class="va">modules</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a><span class="kw">in</span></span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a>  &lt;use &#39;iso&#39; variable here&gt;</span></code></pre></div>
<h2 id="efi">EFI</h2>
<p>To boot using EFI, add:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="ex">--drive</span> if=pflash,format=raw,unit=0,readonly=on,file=<span class="va">${pkgs</span><span class="er">.OVMF.firmware</span><span class="va">}</span></span></code></pre></div>
<p>Here I’m using the <a href="#nix">Nix snippet</a> to get the OVMF firmware.</p>
<h2 id="sata-sdx-drive">SATA (sdX) drive</h2>
<p>With the following variable:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="va">diskSata1</span><span class="op">=</span>./diskSata1.qcow2</span></code></pre></div>
<p>Create a drive:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="va">${qemu}</span> create <span class="at">-f</span> qcow2 <span class="va">$diskSata1</span> 20G</span></code></pre></div>
<p>Then use it as a SATA drive:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="ex">--drive</span> format=qcow2,file=<span class="va">$diskSata1</span>,if=none,id=diskSata1 <span class="dt">\</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>--device ide-hd,drive=diskSata1</span></code></pre></div>
<p>Pay attention that the values for <code>id=</code> and <code>drive=</code> can be arbitrary
but must match.</p>
<p>It will show up as a <code>sdX</code> drive:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> lsblk</span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a><span class="ex">sda</span></span></code></pre></div>
<p>To add another drive as <code>sdb</code>, use the same method
but create a new file
and replace occurrences of <code>diskSata1</code> in <code>id=</code> and <code>drive=</code> with another value.</p>
<p>To specify the device name yourself,
add <code>serial=&lt;name&gt;</code> to the <code>--device</code> options.</p>
<h2 id="nvme-nvmex-drive">NVMe (nvmeX) drive</h2>
<p>The method for NVMe drives is exactly the same as for <a href="#sata-sdx-drive">SATA</a> drives
but replace <code>--device ide-hd,...</code> with <code>--device nvme,...</code>.</p>
<h2 id="epilogue">Epilogue</h2>
<p>I’m using this in my project <a href="https://github.com/ibizaman/skarabox">Skarabox</a> whose goal is to be the fastest
way to install NixOS on host with all bells and whistles included.</p>
<p>Specifically, I’m using a qemu VM to <a href="skarabox-test">test the installation</a> on a VM.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Access a Host Through SSH on NixOS</title>
    <link href="https://blog.tiserbox.com//posts/2025-03-23-access-a-host-through-ssh-on-nix-os.html" />
    <id>https://blog.tiserbox.com//posts/2025-03-23-access-a-host-through-ssh-on-nix-os.html</id>
    <published>2025-03-23T00:00:00Z</published>
    <updated>2025-03-23T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on March 23, 2025
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>, <a title="All pages tagged &#39;server&#39;." href="/tags/server.html">server</a>
	  
	</section>
    <section>
        <!--toc:start-->
<ul>
<li><a href="#scaffolding">Scaffolding</a></li>
<li><a href="#generate-a-ssh-keypair">Generate a SSH keypair</a></li>
<li><a href="#configuration-for-the-server-host">Configuration for the Server Host</a></li>
<li><a href="#configuration-for-the-client-host">Configuration for the Client Host</a></li>
<li><a href="#cleanup">Cleanup</a></li>
<li><a href="#conclusion">Conclusion</a>
<!--toc:end--></li>
</ul>
<p>Let’s see how to grant ourselves ssh access
form a client host to a server host using NixOS.</p>
<p>This post assumes you can already deploy to the server host,
by means of another ssh key.
So this is not about bootstrapping a server.
Here, we’ll see how to grant another user access to the host,
for example a backup user with reduced access.</p>
<h2 id="scaffolding">Scaffolding</h2>
<p>For this post, I’ll be using:</p>
<ul>
<li>flakes,</li>
<li><a href="https://github.com/serokell/deploy-rs">deploy-rs</a> to deploy on the server host,</li>
<li><a href="https://nixos.org/manual/nixos/stable/#sec-changing-config">nixos-rebuild</a> to deploy on the client host,</li>
<li><a href="https://github.com/Mic92/sops-nix">sops-nix</a> to store the ssh private key encrypted</li>
<li>and <a href="https://github.com/nix-community/home-manager">home-manager</a> to configure the client host.</li>
</ul>
<p>This requires a bit of trial and error to get all these pieces to fit together,
so here is a flake that defines the scaffolding
for both the client and the server host:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">inputs</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>    <span class="va">nixpkgs</span>.<span class="va">url</span> <span class="op">=</span> <span class="st">&quot;github:nixos/nixpkgs/nixos-unstable&quot;</span><span class="op">;</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>    <span class="va">home-manager</span>.<span class="va">url</span> <span class="op">=</span> <span class="st">&quot;github:nix-community/home-manager&quot;</span><span class="op">;</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>    <span class="va">home-manager</span>.<span class="va">inputs</span>.<span class="va">nixpkgs</span>.<span class="va">follows</span> <span class="op">=</span> <span class="st">&quot;nixpkgs&quot;</span><span class="op">;</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>    <span class="va">sops-nix</span>.<span class="va">url</span> <span class="op">=</span> <span class="st">&quot;github:Mic92/sops-nix&quot;</span><span class="op">;</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>  <span class="va">outputs</span> <span class="op">=</span> <span class="op">{</span> <span class="va">self</span><span class="op">,</span> <span class="va">nixpkgs</span><span class="op">,</span> <span class="va">home-manager</span><span class="op">,</span> <span class="va">sops-nix</span> <span class="op">}</span>: <span class="kw">let</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>    <span class="va">system</span> <span class="op">=</span> <span class="st">&quot;x86_64-linux&quot;</span><span class="op">;</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a>    <span class="va">specialArgs</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a>      <span class="va">serverUser</span> <span class="op">=</span> <span class="st">&quot;vorta&quot;</span><span class="op">;</span></span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a>      <span class="va">clientUser</span> <span class="op">=</span> <span class="st">&quot;me&quot;</span><span class="op">;</span></span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a>      <span class="va">serverHost</span> <span class="op">=</span> <span class="st">&quot;server&quot;</span><span class="op">;</span></span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a>      <span class="va">clientHost</span> <span class="op">=</span> <span class="st">&quot;client&quot;</span><span class="op">;</span></span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a>  <span class="kw">in</span> <span class="op">{</span></span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a>    <span class="va">nixosModules</span>.<span class="va">server</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a>      <span class="va">imports</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a>        sops-nix.nixosModules.default</span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a>        <span class="ss">./server.nix</span></span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a>      <span class="op">];</span></span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a>    <span class="va">nixosConfigurations</span>.<span class="va">server</span> <span class="op">=</span> nixpkgs.lib.nixosSystem <span class="op">{</span></span>
<span id="cb1-29"><a href="#cb1-29" aria-hidden="true" tabindex="-1"></a>      <span class="kw">inherit</span> system specialArgs<span class="op">;</span></span>
<span id="cb1-30"><a href="#cb1-30" aria-hidden="true" tabindex="-1"></a>      <span class="va">modules</span> <span class="op">=</span> <span class="op">[</span> self.nixosModules.client <span class="op">];</span></span>
<span id="cb1-31"><a href="#cb1-31" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb1-32"><a href="#cb1-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-33"><a href="#cb1-33" aria-hidden="true" tabindex="-1"></a>    <span class="va">nixosModules</span>.<span class="va">client</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb1-34"><a href="#cb1-34" aria-hidden="true" tabindex="-1"></a>      <span class="va">imports</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb1-35"><a href="#cb1-35" aria-hidden="true" tabindex="-1"></a>        home-manager.nixosModules.default</span>
<span id="cb1-36"><a href="#cb1-36" aria-hidden="true" tabindex="-1"></a>        sops-nix.nixosModules.default</span>
<span id="cb1-37"><a href="#cb1-37" aria-hidden="true" tabindex="-1"></a>        <span class="ss">./client.nix</span></span>
<span id="cb1-38"><a href="#cb1-38" aria-hidden="true" tabindex="-1"></a>      <span class="op">];</span></span>
<span id="cb1-39"><a href="#cb1-39" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb1-40"><a href="#cb1-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-41"><a href="#cb1-41" aria-hidden="true" tabindex="-1"></a>    <span class="va">nixosConfigurations</span>.<span class="va">client</span> <span class="op">=</span> nixpkgs.lib.nixosSystem <span class="op">{</span></span>
<span id="cb1-42"><a href="#cb1-42" aria-hidden="true" tabindex="-1"></a>      <span class="kw">inherit</span> system specialArgs<span class="op">;</span></span>
<span id="cb1-43"><a href="#cb1-43" aria-hidden="true" tabindex="-1"></a>      <span class="va">modules</span> <span class="op">=</span> <span class="op">[</span> self.nixosModules.client <span class="op">];</span></span>
<span id="cb1-44"><a href="#cb1-44" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb1-45"><a href="#cb1-45" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb1-46"><a href="#cb1-46" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>We use <code>specialArgs</code> here to pass arguments
to both the client and the server configuration
instead of copying values around.</p>
<p>Deploying to the server host is done with:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>nix run nixpkgs<span class="co">#deploy-rs .#server</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Or, if deploy-rs is installed on your system:</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>deploy .<span class="co">#server</span></span></code></pre></div>
<p>Deploying to the client host is done with:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>sudo nixos<span class="op">-</span>rebuild <span class="op">--</span>flake . switch</span></code></pre></div>
<h2 id="generate-a-ssh-keypair">Generate a SSH keypair</h2>
<p>This step is still manual:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="ex">nix</span> shell nixpkgs#openssh <span class="at">--command</span> <span class="dt">\</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>  ssh-keygen <span class="at">-t</span> ed25519 <span class="at">-f</span> mykey</span></code></pre></div>
<p>Pick no passphrase if you intend this user to get automated access.</p>
<p>This will create two files,
<code>mykey</code> with the private key
and <code>mykey.pub</code> with the public key.
We will use both in the later sections.</p>
<h2 id="configuration-for-the-server-host">Configuration for the Server Host</h2>
<p>In this example, I create a new <code>vorta</code> user
to allow access for backups by the <a href="https://vorta.borgbase.com/">VortaBackup</a> software
into the folder <code>/srv/backup/vorta/me</code>.</p>
<p>This goes in <code>./server.nix</code>:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span> <span class="va">config</span><span class="op">,</span> <span class="va">serverUser</span><span class="op">,</span> <span class="va">clientUser</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>:</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">users</span>.<span class="va">users</span>.${<span class="va">serverUser</span><span class="op">}</span> = <span class="op">{</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>    <span class="va">isSystemUser</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>    <span class="co"># Needed to be able to log in.</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>    <span class="va">useDefaultShell</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a>    <span class="va">home</span> <span class="op">=</span> <span class="st">&quot;/srv/backup/</span><span class="sc">${</span>serverUser<span class="sc">}</span><span class="st">/</span><span class="sc">${</span>clientUser<span class="sc">}</span><span class="st">&quot;</span><span class="op">;</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a>    <span class="va">homeMode</span> <span class="op">=</span> <span class="st">&quot;770&quot;</span><span class="op">;</span></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a>    <span class="va">createHome</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a>    <span class="va">group</span> <span class="op">=</span> <span class="st">&quot;backup&quot;</span><span class="op">;</span></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a>    <span class="va">isSystemUser</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a>    <span class="va">openssh</span>.<span class="va">authorizedKeys</span>.<span class="va">keys</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a>      <span class="st">&quot;ssh-ed25519 AAAAAA...&quot;</span></span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span>;</span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a>  users.groups.backup = <span class="op">{}</span>;</span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
<p>The <code>users.users.${serverUser}.openssh.authorizedKeys.keys</code> field
contains the public key from the <code>mykey.pub</code> file.</p>
<p>If you intend to keep the file laying around, you could instead do:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>users.users.$<span class="op">{</span><span class="va">serverUser</span><span class="op">}</span>.openssh.authorizedKeys.keys = <span class="op">[</span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>  <span class="op">(</span><span class="bu">builtins</span>.readFile <span class="ss">./mykey.pub</span><span class="op">)</span></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="op">]</span>;</span></code></pre></div>
<p>Both versions give the same result and it’s really a matter of taste here.</p>
<h2 id="configuration-for-the-client-host">Configuration for the Client Host</h2>
<p>Copy the private key from <code>mykey</code> under some yaml field in your sops file.
I’ll assume it’s under <code>ssh/server/client-me-vorta</code>.
The convention is <code>ssh/${serverHost}/${clientHost}-${clientUser}-${serverHost}</code>
and is an organized to let me have multiple access per server-client pair.</p>
<p>This goes in <code>./client.nix</code>:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span> <span class="va">config</span><span class="op">,</span> <span class="va">serverUser</span><span class="op">,</span> <span class="va">clientUser</span><span class="op">,</span> <span class="va">serverHost</span><span class="op">,</span> <span class="va">clientHost</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>:</span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">sops</span>.<span class="va">secrets</span>.<span class="st">&quot;</span>ssh/<span class="sc">${</span>serverHost<span class="sc">}</span>/backup<span class="st">&quot;</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a>    <span class="va">owner</span> <span class="op">=</span> user<span class="op">;</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a>    <span class="va">path</span> <span class="op">=</span> <span class="st">&quot;/home/</span><span class="sc">${</span>clientUser<span class="sc">}</span><span class="st">/.ssh/</span><span class="sc">${</span>serverHost<span class="sc">}</span><span class="st">-</span><span class="sc">${</span>serverUser<span class="sc">}</span><span class="st">&quot;</span><span class="op">;</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a>    <span class="va">key</span> <span class="op">=</span> <span class="st">&quot;ssh/</span><span class="sc">${</span>serverHost<span class="sc">}</span><span class="st">/</span><span class="sc">${</span>clientHost<span class="sc">}</span><span class="st">-</span><span class="sc">${</span>clientUser<span class="sc">}</span><span class="st">-</span><span class="sc">${</span>serverHost<span class="sc">}</span><span class="st">&quot;</span><span class="op">;</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a>  <span class="va">home-manager</span>.<span class="va">users</span>.${<span class="va">clientUser</span><span class="op">}</span> = <span class="op">{</span></span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a>    <span class="va">programs</span>.<span class="va">ssh</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a>      <span class="va">enable</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a>      <span class="va">matchBlocks</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;</span><span class="sc">${</span>serverHost<span class="sc">}</span>-<span class="sc">${</span>serverUser<span class="sc">}</span><span class="st">&quot;</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a>          <span class="va">user</span> <span class="op">=</span> serverUser<span class="op">;</span></span>
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a>          <span class="va">hostname</span> <span class="op">=</span> serverHost<span class="op">;</span></span>
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a>          <span class="va">identityFile</span> <span class="op">=</span> config.sops.secrets.</span>
<span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a>            <span class="st">&quot;ssh/</span><span class="sc">${</span>serverHost<span class="sc">}</span><span class="st">/backup&quot;</span>.path<span class="op">;</span></span>
<span id="cb7-18"><a href="#cb7-18" aria-hidden="true" tabindex="-1"></a>        <span class="op">};</span></span>
<span id="cb7-19"><a href="#cb7-19" aria-hidden="true" tabindex="-1"></a>      <span class="op">};</span></span>
<span id="cb7-20"><a href="#cb7-20" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb7-21"><a href="#cb7-21" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span>;</span>
<span id="cb7-22"><a href="#cb7-22" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
<p>I use the <code>key</code> field of sops-nix to give a nickname to the secret,
making it a bit easier to recall later in the <code>home-manager</code> config.
This is optional.</p>
<h2 id="cleanup">Cleanup</h2>
<p>The <code>mykey</code> files laying around this way is not good.
Let’s delete them securely with the <code>shred</code> tool:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="ex">nix</span> shell coreutils <span class="at">--command</span> shred mykey</span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a><span class="ex">nix</span> shell coreutils <span class="at">--command</span> shred mykey.pub</span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="fu">rm</span> mykey</span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a><span class="fu">rm</span> mykey.pub</span></code></pre></div>
<p>Omit the <code>mykey.pub</code> file if you used the <code>readFile</code> version of the code.</p>
<h2 id="conclusion">Conclusion</h2>
<p>After deploying, we now can access the server with the new user with:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="fu">ssh</span> <span class="va">${serverHost}</span>-<span class="va">${serverUser}</span></span></code></pre></div>
<p>With the example values I chose above, it’s:</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="fu">ssh</span> server-vorta</span></code></pre></div>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Enable Hibernation on NixOS</title>
    <link href="https://blog.tiserbox.com//posts/2025-03-10-enable-hibernation-on-nix-os.html" />
    <id>https://blog.tiserbox.com//posts/2025-03-10-enable-hibernation-on-nix-os.html</id>
    <published>2025-03-10T00:00:00Z</published>
    <updated>2025-03-10T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on March 10, 2025
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <p>Hibernation was broken for me since a long time.
I postponed trying to fix it for such a long time.
Then, I stumbled onto <a href="post">this post</a> which had the fix!</p>
<p>To celebrate the finding, I’m sharing a NixOS module
to enable hibernation.</p>
<p>As a prerequisite, you need to create a swap partition,
as noted in the <a href="wiki">NixOS wiki</a>.</p>
<p>As for the module, here it is:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span> <span class="va">config</span><span class="op">,</span> <span class="va">pkgs</span><span class="op">,</span> <span class="va">lib</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>:</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">cfg</span> <span class="op">=</span> config.base<span class="op">;</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="kw">in</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>  <span class="va">options</span>.<span class="va">base</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>    <span class="va">hibernation</span> <span class="op">=</span> lib.mkOption <span class="op">{</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>      <span class="va">description</span> <span class="op">=</span> <span class="st">&#39;&#39;</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a><span class="st">        Options to configuration hibernation.</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a><span class="st">      &#39;&#39;</span><span class="op">;</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>      <span class="va">type</span> <span class="op">=</span> lib.types.submodule <span class="op">{</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a>        <span class="va">options</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a>          <span class="va">enable</span> <span class="op">=</span> lib.mkEnableOption <span class="st">&quot;hibernation&quot;</span><span class="op">;</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a>          <span class="va">device</span> <span class="op">=</span> lib.mkOption <span class="op">{</span></span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a>            <span class="va">type</span> <span class="op">=</span> lib.types.str<span class="op">;</span></span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a>            <span class="va">description</span> <span class="op">=</span> <span class="st">&#39;&#39;</span></span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a><span class="st">              Device used to store hibernation</span></span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a><span class="st">              information. Use lsblk to find it.</span></span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a><span class="st">            &#39;&#39;</span><span class="op">;</span></span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a>            <span class="va">example</span> <span class="op">=</span> <span class="st">&quot;/dev/disk/by-label/swap&quot;</span><span class="op">;</span></span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a>          <span class="op">};</span></span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a>          <span class="va">hibernateAfterSleepDelay</span> <span class="op">=</span> lib.mkOption <span class="op">{</span></span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a>            <span class="va">type</span> <span class="op">=</span> lib.types.str<span class="op">;</span></span>
<span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a>            <span class="va">description</span> <span class="op">=</span> <span class="st">&#39;&#39;</span></span>
<span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a><span class="st">              Hibernate after sleeping for this long.</span></span>
<span id="cb1-29"><a href="#cb1-29" aria-hidden="true" tabindex="-1"></a><span class="st">            &#39;&#39;</span><span class="op">;</span></span>
<span id="cb1-30"><a href="#cb1-30" aria-hidden="true" tabindex="-1"></a>            <span class="va">default</span> <span class="op">=</span> <span class="st">&quot;2h&quot;</span><span class="op">;</span></span>
<span id="cb1-31"><a href="#cb1-31" aria-hidden="true" tabindex="-1"></a>          <span class="op">};</span></span>
<span id="cb1-32"><a href="#cb1-32" aria-hidden="true" tabindex="-1"></a>        <span class="op">};</span></span>
<span id="cb1-33"><a href="#cb1-33" aria-hidden="true" tabindex="-1"></a>      <span class="op">};</span></span>
<span id="cb1-34"><a href="#cb1-34" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb1-35"><a href="#cb1-35" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb1-36"><a href="#cb1-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-37"><a href="#cb1-37" aria-hidden="true" tabindex="-1"></a>  <span class="va">config</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb1-38"><a href="#cb1-38" aria-hidden="true" tabindex="-1"></a>    <span class="va">powerManagement</span>.<span class="va">enable</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb1-39"><a href="#cb1-39" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-40"><a href="#cb1-40" aria-hidden="true" tabindex="-1"></a>    <span class="co"># Specifies where the hibernation info will be stored.</span></span>
<span id="cb1-41"><a href="#cb1-41" aria-hidden="true" tabindex="-1"></a>    <span class="va">boot</span>.<span class="va">kernelParams</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb1-42"><a href="#cb1-42" aria-hidden="true" tabindex="-1"></a>      <span class="st">&quot;resume=</span><span class="sc">${</span>cfg.hibernation.device<span class="sc">}</span><span class="st">&quot;</span></span>
<span id="cb1-43"><a href="#cb1-43" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb1-44"><a href="#cb1-44" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-45"><a href="#cb1-45" aria-hidden="true" tabindex="-1"></a>    <span class="co"># Allow hibernation</span></span>
<span id="cb1-46"><a href="#cb1-46" aria-hidden="true" tabindex="-1"></a>    <span class="va">security</span>.<span class="va">protectKernelImage</span> <span class="op">=</span> <span class="op">!</span>cfg.hibernation.enable<span class="op">;</span></span>
<span id="cb1-47"><a href="#cb1-47" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-48"><a href="#cb1-48" aria-hidden="true" tabindex="-1"></a>    <span class="co"># Enable hibernation in menus</span></span>
<span id="cb1-49"><a href="#cb1-49" aria-hidden="true" tabindex="-1"></a>    <span class="va">environment</span>.<span class="va">etc</span> <span class="op">=</span> lib.mkIf cfg.hibernation.enable <span class="op">{</span></span>
<span id="cb1-50"><a href="#cb1-50" aria-hidden="true" tabindex="-1"></a>      <span class="st">&quot;</span>/polkit-1/localauthority/50-local.d/com.ubuntu.enable-hibernate.pkla<span class="st">&quot;</span>.<span class="va">text</span> <span class="op">=</span> <span class="st">&#39;&#39;</span></span>
<span id="cb1-51"><a href="#cb1-51" aria-hidden="true" tabindex="-1"></a><span class="st">        [Re-enable hibernate by default in upower]</span></span>
<span id="cb1-52"><a href="#cb1-52" aria-hidden="true" tabindex="-1"></a><span class="st">        Identity=unix-user:*</span></span>
<span id="cb1-53"><a href="#cb1-53" aria-hidden="true" tabindex="-1"></a><span class="st">        Action=org.freedesktop.upower.hibernate</span></span>
<span id="cb1-54"><a href="#cb1-54" aria-hidden="true" tabindex="-1"></a><span class="st">        ResultActive=yes</span></span>
<span id="cb1-55"><a href="#cb1-55" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-56"><a href="#cb1-56" aria-hidden="true" tabindex="-1"></a><span class="st">        [Re-enable hibernate by default in logind]</span></span>
<span id="cb1-57"><a href="#cb1-57" aria-hidden="true" tabindex="-1"></a><span class="st">        Identity=unix-user:*</span></span>
<span id="cb1-58"><a href="#cb1-58" aria-hidden="true" tabindex="-1"></a><span class="st">        Action=org.freedesktop.login1.hibernate;org.freedesktop.login1.handle-hibernate-key;org.freedesktop.login1;org.freedesktop.login1.hibernate-multiple-sessions;org.freedesktop.login1.hibernate-ignore-inhibit</span></span>
<span id="cb1-59"><a href="#cb1-59" aria-hidden="true" tabindex="-1"></a><span class="st">        ResultActive=yes</span></span>
<span id="cb1-60"><a href="#cb1-60" aria-hidden="true" tabindex="-1"></a><span class="st">        &#39;&#39;</span><span class="op">;</span></span>
<span id="cb1-61"><a href="#cb1-61" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb1-62"><a href="#cb1-62" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-63"><a href="#cb1-63" aria-hidden="true" tabindex="-1"></a>    <span class="co"># Set suspend-then-hibernate as defaults</span></span>
<span id="cb1-64"><a href="#cb1-64" aria-hidden="true" tabindex="-1"></a>    <span class="va">services</span>.<span class="va">logind</span> <span class="op">=</span> lib.mkIf cfg.hibernation.enable <span class="op">{</span></span>
<span id="cb1-65"><a href="#cb1-65" aria-hidden="true" tabindex="-1"></a>      <span class="va">lidSwitch</span> <span class="op">=</span> <span class="st">&quot;suspend-then-hibernate&quot;</span><span class="op">;</span></span>
<span id="cb1-66"><a href="#cb1-66" aria-hidden="true" tabindex="-1"></a>      <span class="va">extraConfig</span> <span class="op">=</span> <span class="st">&#39;&#39;</span></span>
<span id="cb1-67"><a href="#cb1-67" aria-hidden="true" tabindex="-1"></a><span class="st">        HandlePowerKey=suspend-then-hibernate</span></span>
<span id="cb1-68"><a href="#cb1-68" aria-hidden="true" tabindex="-1"></a><span class="st">        IdleAction=suspend-then-hibernate</span></span>
<span id="cb1-69"><a href="#cb1-69" aria-hidden="true" tabindex="-1"></a><span class="st">        IdleActionSec=2m</span></span>
<span id="cb1-70"><a href="#cb1-70" aria-hidden="true" tabindex="-1"></a><span class="st">      &#39;&#39;</span><span class="op">;</span></span>
<span id="cb1-71"><a href="#cb1-71" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb1-72"><a href="#cb1-72" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-73"><a href="#cb1-73" aria-hidden="true" tabindex="-1"></a>    <span class="co"># </span></span>
<span id="cb1-74"><a href="#cb1-74" aria-hidden="true" tabindex="-1"></a>    <span class="va">systemd</span>.<span class="va">sleep</span>.<span class="va">extraConfig</span> <span class="op">=</span> lib.mkIf cfg.hibernation.enable</span>
<span id="cb1-75"><a href="#cb1-75" aria-hidden="true" tabindex="-1"></a>      <span class="st">&quot;HibernateDelaySec=</span><span class="sc">${</span>cfg.hibernation.hibernateAfterSleepDelay<span class="sc">}</span><span class="st">&quot;</span><span class="op">;</span></span>
<span id="cb1-76"><a href="#cb1-76" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb1-77"><a href="#cb1-77" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>Happy hibernation!</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Parental Control with Jellyfin and Radarr/Sonarr</title>
    <link href="https://blog.tiserbox.com//posts/2025-03-01-parental-control-with-jellyfin-and-radarr-sonarr.html" />
    <id>https://blog.tiserbox.com//posts/2025-03-01-parental-control-with-jellyfin-and-radarr-sonarr.html</id>
    <published>2025-03-01T00:00:00Z</published>
    <updated>2025-03-01T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on March  1, 2025
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;jellyfin&#39;." href="/tags/jellyfin.html">jellyfin</a>
	  
	</section>
    <section>
        <p>The goal is to create a user in Jellyfin
that can only see media files that
have a given tag, say “kids”,
and that tag was set inside Radarr, Sonarr
or another *arr application.</p>
<h2 id="arr-configuration">*arr Configuration</h2>
<ol type="1">
<li><p>Within Settings &gt; Metadata &gt; Kodi,
check “Enable” and “Movie Metadata”.</p></li>
<li><p>Then, when adding or editing a movie,
add a tag to mark media that children can watch,
like “enfants”.</p>
<figure>
<img src="/images/2025-03-01-parental-control-with-jellyfin-and-radarr-sonarr/edit_tag.png" class="zoom" alt="Edit modal showing tag used for parental control" />
<figcaption aria-hidden="true">Edit modal showing tag used for parental control</figcaption>
</figure></li>
</ol>
<p>This will create a .nfo file that includes the tag:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode xml"><code class="sourceCode xml"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>&lt;<span class="kw">movie</span>&gt;</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  ...</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>  &lt;<span class="kw">tag</span>&gt;enfants&lt;/<span class="kw">tag</span>&gt;</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>  ...</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>&lt;/<span class="kw">movie</span>&gt;</span></code></pre></div>
<p>If you add this tag to an existing movie
that is already synced with Jellyfin,
click the “Refresh &amp; Scan” button.</p>
<h2 id="jellyfin-configuration">Jellyfin Configuration</h2>
<p>In Jellyfin, under Administration &gt; Dashboard &gt; Users,
create a user and in “Parental Control” tab,
only allow items with the tag chosen above:</p>
<figure>
<img src="/images/2025-03-01-parental-control-with-jellyfin-and-radarr-sonarr/allow_tag.png" class="zoom" alt="Jellyfin option to allow only media with given tag" />
<figcaption aria-hidden="true">Jellyfin option to allow only media with given tag</figcaption>
</figure>
<p>The tag seems to be case-insensitive.</p>
<p>That’s it!</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Pre-Seeded Known Host to Access New Server</title>
    <link href="https://blog.tiserbox.com//posts/2024-11-28-pre-seeded-known-host-to-access-new-server.html" />
    <id>https://blog.tiserbox.com//posts/2024-11-28-pre-seeded-known-host-to-access-new-server.html</id>
    <published>2024-11-28T00:00:00Z</published>
    <updated>2024-11-28T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on November 28, 2024
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <!--toc:start-->
<ul>
<li><a href="#why-skipping-this-step-is-a-good-idea">Why Skipping this Step is a Good Idea</a></li>
<li><a href="#why-does-host-key-verification-exist">Why Does Host Key Verification Exist</a></li>
<li><a href="#pre-seed-the-host-key">Pre-Seed the Host Key</a>
<!--toc:end--></li>
</ul>
<p>This post goes over how to create a new server
and be able to ssh into it
without needing to go through the ssh host
verification manual step.</p>
<p>This step is annoying, error prone
and tedious enough that I’m sure most of us
answer “yes” to the question as fast
as when reading some Terms of Services.</p>
<p>To skip to the solution, <a href="#pre-seed-the-host-key">click here</a>.</p>
<h2 id="why-skipping-this-step-is-a-good-idea">Why Skipping this Step is a Good Idea</h2>
<p>As said in the preamble, this step is tedious and error prone.
One needs to extract the newly generated public key from the new server,
then generate the fingerprint of the key
and compare it visually with the one printed by SSH.</p>
<p>Usually, extracting the public key requires accessing
the server in some less secure way, by for example
using the username/password protocol to SSH into the server.
This is not ideal.</p>
<p>I would even argue this step is inherently insecure
especially in a cloud environment
because how can you be really sure this public key/fingerprint
is from the server you just booted
unless it’s physically in front of you?</p>
<h2 id="why-does-host-key-verification-exist">Why Does Host Key Verification Exist</h2>
<p>In NixOS, when OpenSSH’s systemd unit starts for the first time,
it generates some SSH keys using <code>ssh-keygen</code> by default
at <code>/etc/ssh/ssh_host_rsa_key</code> and <code>/etc/ssh/ssh_host_ed25519_key</code>.</p>
<p><em>This step is also done in most other distributions.</em></p>
<p>The location, number and flags used to generate the keys
can be changed with the <a href="https://search.nixos.org/options?channel=24.11&amp;show=services.openssh.hostKeys&amp;from=0&amp;size=50&amp;sort=relevance&amp;type=packages&amp;query=services.openssh.hostkeys">services.openssh.hostKeys</a> option.</p>
<p>Now, let’s assume one can SSH into the server
by having shared their own private key with the server.
On the first attempt, SSH will prompt them with the following
question:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="er">The</span> <span class="er">authenticity</span> <span class="er">of</span> <span class="er">host</span> <span class="er">&#39;</span><span class="ot">[</span><span class="er">XXX.XXX.XXX.XXX</span><span class="ot">]</span><span class="er">:22</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="er">(</span><span class="ot">[</span><span class="er">XXX.XXX.XXX.XXX</span><span class="ot">]</span><span class="er">:22)&#39;</span> <span class="er">can&#39;t</span> <span class="er">be</span> <span class="er">established.</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="er">ED25519</span> <span class="er">key</span> <span class="er">fingerprint</span> <span class="er">is</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="er">SHA256:YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY.</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="er">Are</span> <span class="er">you</span> <span class="er">sure</span> <span class="er">you</span> <span class="er">want</span> <span class="er">to</span> <span class="er">continue</span> <span class="er">connecting</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a><span class="er">(yes/no/</span><span class="ot">[</span><span class="er">fingerprint</span><span class="ot">]</span><span class="er">)?</span></span></code></pre></div>
<p>This fingerprint corresponds to the public key of one of the SSH keys
generated by OpenSSH.
One can see a key fingerprint with:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ssh-keygen <span class="at">-lvf</span> <span class="op">&lt;</span>public key file<span class="op">&gt;</span></span></code></pre></div>
<p>If they say yes, the following will get printed:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="er">Warning:</span> <span class="er">Permanently</span> <span class="er">added</span> <span class="er">&#39;</span><span class="ot">[</span><span class="er">XXX.XXX.XXX.XXX</span><span class="ot">]</span><span class="er">:22&#39;</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="er">(ED25519)</span> <span class="er">to</span> <span class="er">the</span> <span class="er">list</span> <span class="er">of</span> <span class="er">known</span> <span class="er">hosts.</span></span></code></pre></div>
<p>And indeed, a new entry is added to their <code>~/.ssh/known_hosts</code> file:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> cat ~/.ssh/known_hosts <span class="kw">|</span> <span class="fu">grep</span> XXX.XXX.XXX.XXX</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="ex">[192.168.1.30]:22345</span> ssh-ed25519 ZZZZZ...</span></code></pre></div>
<p>The whole reason is to protect them if, in the future,
the host changes without them knowing about it.
There are a myriad of reasons for this,
some innocuous and some malevolent.
Anyway, the next time they’ll try to connect,
they will see this message:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="er">@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="er">@</span>    <span class="er">WARNING:</span> <span class="er">REMOTE</span> <span class="er">HOST</span> <span class="er">IDENTIFICATION</span> <span class="er">HAS</span> <span class="er">CHANGED!</span>     <span class="er">@</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="er">@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="er">IT</span> <span class="er">IS</span> <span class="er">POSSIBLE</span> <span class="er">THAT</span> <span class="er">SOMEONE</span> <span class="er">IS</span> <span class="er">DOING</span> <span class="er">SOMETHING</span> <span class="er">NASTY!</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a><span class="er">Someone</span> <span class="er">could</span> <span class="er">be</span> <span class="er">eavesdropping</span> <span class="er">on</span> <span class="er">you</span> <span class="er">right</span> <span class="er">now</span> <span class="er">(man-in-the-middle</span> <span class="er">attack)!</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a><span class="er">It</span> <span class="er">is</span> <span class="er">also</span> <span class="er">possible</span> <span class="er">that</span> <span class="er">a</span> <span class="er">host</span> <span class="er">key</span> <span class="er">has</span> <span class="er">just</span> <span class="er">been</span> <span class="er">changed.</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a><span class="er">The</span> <span class="er">fingerprint</span> <span class="er">for</span> <span class="er">the</span> <span class="er">ED25519</span> <span class="er">key</span> <span class="er">sent</span> <span class="er">by</span> <span class="er">the</span> <span class="er">remote</span> <span class="er">host</span> <span class="er">is</span></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a><span class="er">SHA256:WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW.</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a><span class="er">Please</span> <span class="er">contact</span> <span class="er">your</span> <span class="er">system</span> <span class="er">administrator.</span></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a><span class="er">Add</span> <span class="er">correct</span> <span class="er">host</span> <span class="er">key</span> <span class="er">in</span> <span class="er">/home/them/.ssh/known_hosts</span> <span class="er">to</span> <span class="er">get</span> <span class="er">rid</span> <span class="er">of</span> <span class="er">this</span> <span class="er">message.</span></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a><span class="er">Offending</span> <span class="er">ED25519</span> <span class="er">key</span> <span class="er">in</span> <span class="er">/home/them/.ssh/known_hosts:1</span></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a><span class="er">Host</span> <span class="er">key</span> <span class="er">for</span> <span class="ot">[</span><span class="er">XXX.XXX.XXX.XXX</span><span class="ot">]</span><span class="er">:22</span> <span class="er">has</span> <span class="er">changed</span> <span class="er">and</span> <span class="er">you</span> <span class="er">have</span> <span class="er">requested</span> <span class="er">strict</span> <span class="er">checking.</span></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a><span class="er">Host</span> <span class="er">key</span> <span class="er">verification</span> <span class="er">failed.</span></span></code></pre></div>
<p>Essentially, it’s saying the fingerprint of the host’s public key changed
and warns that it could be potentially a bad sign.</p>
<p>If they happen to know this is okay,
because for example they indeed generated a new host key for some reason,
getting rid of the fingerprint saved the first time
and restarting the host key verification procedure
is done by deleting the entry in the <code>~/.ssh/known_hosts</code> file with:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="fu">ssh-keygen</span> <span class="at">-R</span> <span class="st">&quot;[XXX.XXX.XXX.XXX]:22&quot;</span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="co"># Host [XXX.XXX.XXX.XXX]:22 found: line 1</span></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/them/.ssh/known_hosts</span> updated.</span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a><span class="ex">Original</span> contents retained as</span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/them/.ssh/known_hosts.old</span></span></code></pre></div>
<h2 id="pre-seed-the-host-key">Pre-Seed the Host Key</h2>
<p>We can generate the SSH key pair with:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ssh-keygen <span class="at">-N</span> <span class="st">&quot;&quot;</span> <span class="at">-f</span> hostkey</span></code></pre></div>
<p>Then, assuming we use SOPS with <a href="https://github.com/Mic92/sops-nix">sops-nix</a> to handle secrets,
we add the content of the <code>hostkey</code> and <code>hostkey.pub</code> files
to our SOPS secrets file:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="fu">hostkey</span><span class="kw">:</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">privateKey</span><span class="kw">:</span><span class="at"> XXXX...</span></span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">publicKey</span><span class="kw">:</span><span class="at"> YYYY...</span></span></code></pre></div>
<p>Finally, the two NixOS snippets for the remaining work are
for the server we just let SOPS populate the correct location
by using the <a href="https://github.com/Mic92/sops-nix?tab=readme-ov-file#symlinks-to-other-directories"><code>path</code></a> option:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a>sops.secrets.<span class="st">&quot;hostkey/privateKey&quot;</span> = <span class="op">{</span></span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">path</span> <span class="op">=</span> <span class="st">&quot;/etc/ssh/ssh_host_rsa_key&quot;</span><span class="op">;</span></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>And for our laptop we add it to our system <a href="https://search.nixos.org/options?channel=24.11&amp;show=programs.ssh.knownHosts&amp;from=0&amp;size=50&amp;sort=relevance&amp;type=packages&amp;query=programs.ssh.knownhosts">known hosts</a>:</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a>sops.secrets.<span class="st">&quot;hostkey/publicKey&quot;</span> = <span class="op">{}</span>;</span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a>programs.ssh.knownHosts.<span class="st">&quot;myserver&quot;</span>.publicKeyFile =</span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a>  sops.secrets.<span class="st">&quot;hostkey/publicKey&quot;</span>.path;</span></code></pre></div>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Display Pictures from Nextcloud to a Kodi Photo Frame</title>
    <link href="https://blog.tiserbox.com//posts/2024-09-03-share-pictures-to-jellyfin-photo-frame-from-nextcloud.html" />
    <id>https://blog.tiserbox.com//posts/2024-09-03-share-pictures-to-jellyfin-photo-frame-from-nextcloud.html</id>
    <published>2024-09-03T00:00:00Z</published>
    <updated>2024-09-03T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on September  3, 2024
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>, <a title="All pages tagged &#39;nextcloud&#39;." href="/tags/nextcloud.html">nextcloud</a>, <a title="All pages tagged &#39;systemd&#39;." href="/tags/systemd.html">systemd</a>, <a title="All pages tagged &#39;iot&#39;." href="/tags/iot.html">iot</a>, <a title="All pages tagged &#39;server&#39;." href="/tags/server.html">server</a>
	  
	</section>
    <section>
        <p>So I wanted to show family pictures on the TV.
My current setup is:</p>
<ul>
<li>a server using <a href="https://nixos.org/">NixOS</a> to host <a href="https://nextcloud.com/">Nextcloud</a>,</li>
<li>an <a href="https://www.amazon.com/Orange-Pi-Allwinner-Computer-Support/dp/B09TQZH4GJ">OrangePi 3</a> with <a href="https://libreelec.tv/">Libreelec</a> and <a href="https://kodi.tv/">Kodi</a>,</li>
<li>and mobile phones with the iOS Nextcloud app.</li>
</ul>
<p>What I wanted is to be able to select, from Nextcloud, which pictures to show on the TV and those would appear there automatically. This post goes over how I did it.</p>
<p>Btw, I’m hosting <a href="https://jellyfin.org/">Jellyfin</a> and using the <a href="https://jellyfin.org/docs/general/clients/kodi/">Jellyfin For Kodi</a> plugin on the OrangePi but this does not matter for the following setup.</p>
<!-- ![image example](/images/2023-09-30-share-pictures-to-jellyfin-photo-frame-from-nextcloud) -->
<h2 id="wanted-user-experience">Wanted User Experience</h2>
<p>To show pictures:</p>
<ol type="1">
<li>Pictures are uploaded from the iOS app to Nextcloud.</li>
<li>User goes over pictures and shares good ones with the Photoframe Nextcloud user.</li>
<li>Wait for screensaver on OrangePi box to start and see new pictures!</li>
</ol>
<p>To remove pictures:</p>
<ol type="1">
<li>Unshare pictures with Photoframe Nextcloud user.</li>
</ol>
<h2 id="setup">Setup</h2>
<p>I didn’t want the OrangePi to connect to the Nextcloud server using WebDav.
I tried that first and got into scenarios where I was sharing so many pictures
that the screensaver could not load them correctly.
There are multiple reasons for this and one can search to optimize them.
But this got me thinking, why couldn’t I instead copy all the shared pictures
to the OrangePi directly?
This makes the solution very robust to any networking mishap.
And that’s what I did.</p>
<h3 id="on-nextcloud">On Nextcloud</h3>
<ol type="1">
<li>I created a <code>Photoframe</code> user in Nextcloud.
I logged in with that user once and configured it to <a href="https://docs.nextcloud.com/server/latest/user_manual/en/files/sharing.html#internal-shares-with-users-and-groups">automatically accept incoming shares</a>.</li>
<li>With my user, I shared some pictures with that new user to test that I could and that the <code>Photoframe</code> user would see them.</li>
</ol>
<h3 id="on-the-server">On the Server</h3>
<p>The idea here is to <code>rsync</code> the shared pictures to the OrangePi box.
This implies that:</p>
<ul>
<li><code>rsync</code> has access to the shared pictures only.</li>
<li><code>rsync</code> can ssh into the OrangePi box.</li>
<li><code>rsync</code> is installed on the server and the OrangePi box.</li>
</ul>
<p>To make rsync access the shared pictures only, I mounted the <code>Photoframe</code> Nextcloud folder through WebDav in a directory on the server.
This is done using <a href="https://github.com/ibizaman/selfhostblocks/blob/main/modules/blocks/davfs.nix">this Self Host Blocks module</a> and with the following config on my server:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>shb.davfs.mounts = <span class="op">[</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="co"># Mount a WebDav folder in the /srv/photoframe.</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>  <span class="op">{</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>    <span class="va">remoteUrl</span> <span class="op">=</span> <span class="st">&quot;https://$MYDOMAIN/remote.php/dav/files/photoframe&quot;</span><span class="op">;</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>    <span class="va">mountPoint</span> <span class="op">=</span> <span class="st">&quot;/srv/photoframe&quot;</span><span class="op">;</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>    <span class="va">username</span> <span class="op">=</span> <span class="st">&quot;photoframe&quot;</span><span class="op">;</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>    <span class="va">passwordFile</span> <span class="op">=</span> config.sops.secrets.<span class="st">&quot;photoframe&quot;</span>.path<span class="op">;</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>    <span class="va">uid</span> <span class="op">=</span> <span class="dv">992</span><span class="op">;</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>    <span class="va">gid</span> <span class="op">=</span> <span class="dv">991</span><span class="op">;</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a><span class="op">]</span>;</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a><span class="co"># Password for Photoframe user.</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="co"># For now, it must be in format:</span></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a><span class="co">#</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a><span class="co">#   &lt;mountPoint&gt; &lt;username&gt; &lt;password&gt;</span></span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a><span class="co">#</span></span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a><span class="co"># In this example, it is:</span></span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a><span class="co">#</span></span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a><span class="co">#   /srv/photoframe photoframe XHsbaf...</span></span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a><span class="co">#</span></span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a>sops.secrets.<span class="st">&quot;webdav/nextcloud&quot;</span> = <span class="op">{</span></span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a>  <span class="va">sopsFile</span> <span class="op">=</span> <span class="ss">./secrets.yaml</span><span class="op">;</span></span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a>  <span class="va">mode</span> <span class="op">=</span> <span class="st">&quot;0600&quot;</span><span class="op">;</span></span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a>  <span class="va">path</span> <span class="op">=</span> <span class="st">&quot;/etc/davfs2/secrets&quot;</span><span class="op">;</span></span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a>users.groups.photoframe = <span class="op">{</span></span>
<span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a>  <span class="co"># Must correspond to the gid above.</span></span>
<span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a>  <span class="va">gid</span> <span class="op">=</span> <span class="dv">991</span><span class="op">;</span></span>
<span id="cb1-29"><a href="#cb1-29" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span>
<span id="cb1-30"><a href="#cb1-30" aria-hidden="true" tabindex="-1"></a>users.users.photoframe = <span class="op">{</span></span>
<span id="cb1-31"><a href="#cb1-31" aria-hidden="true" tabindex="-1"></a>  <span class="va">isSystemUser</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb1-32"><a href="#cb1-32" aria-hidden="true" tabindex="-1"></a>  <span class="co"># Must correspond to the uid above.</span></span>
<span id="cb1-33"><a href="#cb1-33" aria-hidden="true" tabindex="-1"></a>  <span class="va">uid</span> <span class="op">=</span> <span class="dv">992</span><span class="op">;</span></span>
<span id="cb1-34"><a href="#cb1-34" aria-hidden="true" tabindex="-1"></a>  <span class="va">group</span> <span class="op">=</span> <span class="st">&quot;photoframe&quot;</span><span class="op">;</span></span>
<span id="cb1-35"><a href="#cb1-35" aria-hidden="true" tabindex="-1"></a>  <span class="va">home</span> <span class="op">=</span> <span class="st">&quot;/var/lib/photoframe&quot;</span><span class="op">;</span></span>
<span id="cb1-36"><a href="#cb1-36" aria-hidden="true" tabindex="-1"></a>  <span class="va">createHome</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb1-37"><a href="#cb1-37" aria-hidden="true" tabindex="-1"></a>  <span class="va">packages</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb1-38"><a href="#cb1-38" aria-hidden="true" tabindex="-1"></a>    pkgs.rsync</span>
<span id="cb1-39"><a href="#cb1-39" aria-hidden="true" tabindex="-1"></a>  <span class="op">];</span></span>
<span id="cb1-40"><a href="#cb1-40" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>I could verify this worked by making sure the secret looked good with <code>cat /etc/davfs2/secrets</code>
and also by seeing that the <code>/srv/photoframe</code> directory was created and not empty. In case of error, check <code>systemctl status srv-photoframe.mount</code>.</p>
<p>To be able to ssh into Kodi, I needed to create an ssh key pair.
So I ran the following and got two files <code>ssh-orangepi</code> and <code>ssh-orangepi.pub</code>:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="fu">ssh-keygen</span> <span class="at">-t</span> ed25519 <span class="at">-N</span> <span class="st">&quot;&quot;</span> <span class="at">-f</span> ssh-orangepi</span></code></pre></div>
<p>I wrote the private key in my Sops config.
I copied over the public key into the OrangePi’s <code>/root/.ssh/authorizedKeys</code> file.
I sshed once from the server to the OrangePi to accept the host key fingerprint.
Finally, I could put a cron job that would run <code>rsync</code> on a schedule:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>systemd.services.sync<span class="op">-</span>to<span class="op">-</span>orangepi = <span class="op">{</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">description</span> <span class="op">=</span> <span class="st">&quot;Sync Pictures to OrangePi&quot;</span><span class="op">;</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">after</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;network.target&quot;</span> <span class="st">&quot;srv-photoframe.mount&quot;</span> <span class="op">];</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">bindsTo</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;srv-photoframe.mount&quot;</span> <span class="op">];</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>  <span class="va">path</span> <span class="op">=</span> <span class="op">[</span> pkgs.openssh <span class="op">];</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>  <span class="va">serviceConfig</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>    <span class="va">User</span> <span class="op">=</span> <span class="st">&quot;photoframe&quot;</span><span class="op">;</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>    <span class="va">Group</span> <span class="op">=</span> <span class="st">&quot;photoframe&quot;</span><span class="op">;</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>    <span class="va">Type</span> <span class="op">=</span> <span class="st">&quot;oneshot&quot;</span><span class="op">;</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>    <span class="va">ExecStart</span> <span class="op">=</span> <span class="st">&#39;&#39;</span></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a><span class="st">      </span><span class="sc">${</span>pkgs.rsync<span class="sc">}</span><span class="st">/bin/rsync \</span></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a><span class="st">        --rsh &#39;ssh -i /etc/davfs2/ssh-salon&#39; \</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a><span class="st">        # Add more things to exclude if needed.</span></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a><span class="st">        --exclude=&#39;lost+found&#39; \</span></span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a><span class="st">        --delete \</span></span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a><span class="st">        --delete-excluded \</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a><span class="st">        -a \</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a><span class="st">        /srv/photoframe/ \</span></span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a><span class="st">        root@orangepi.$MYDOMAIN:/storage/pictures</span></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a><span class="st">      &#39;&#39;</span><span class="op">;</span></span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a><span class="co"># The private ssh key.</span></span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a>sops.secrets.<span class="st">&quot;webdav/ssh-orangepi&quot;</span> = <span class="op">{</span></span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a>  <span class="va">sopsFile</span> <span class="op">=</span> <span class="ss">./secrets.yaml</span><span class="op">;</span></span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a>  <span class="va">mode</span> <span class="op">=</span> <span class="st">&quot;0600&quot;</span><span class="op">;</span></span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a>  <span class="va">owner</span> <span class="op">=</span> <span class="st">&quot;photoframe&quot;</span><span class="op">;</span></span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a>  <span class="va">path</span> <span class="op">=</span> <span class="st">&quot;/etc/davfs2/ssh-salon&quot;</span><span class="op">;</span></span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a>systemd.timers.sync<span class="op">-</span>to<span class="op">-</span>orangepi = <span class="op">{</span></span>
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a>  <span class="va">wantedBy</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;timers.target&quot;</span> <span class="op">];</span></span>
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a>  <span class="va">timerConfig</span>.<span class="va">OnBootSec</span> <span class="op">=</span> <span class="st">&quot;10m&quot;</span><span class="op">;</span></span>
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a>  <span class="va">timerConfig</span>.<span class="va">OnUnitActiveSec</span> <span class="op">=</span> <span class="st">&quot;2h&quot;</span><span class="op">;</span></span>
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a>  <span class="va">timerConfig</span>.<span class="va">RandomizedDelaySec</span> <span class="op">=</span> <span class="st">&quot;10m&quot;</span><span class="op">;</span></span>
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>It is important to put <code>--delete</code> and <code>--delete-excluded</code> in the rsync config.
This way, unsharing pictures will effectively delete them from the OrangePi.</p>
<p>The <a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#BindsTo=">BindsTo</a> option is important too because it deactivates the service if the WebDav mount point gets stopped.
Otherwise, in that case <code>rsync</code> will happily synchronize an empty directory - the unmounted directory - and delete all pictures on the OrangePi.</p>
<p>I checked for any issues with <code>systemctl status sync-to-orangepi.service</code>, <code>systemctl status sync-to-orangepi.timer</code> and <code>journalctl -u sync-to-orangepi.service -f</code>.
Btw, before this steps worked, I needed to install <code>rsync</code> on the OrangePi. See next step.</p>
<p>The files get stored in <code>/storage/pictures</code> on the OrangePi, which is on the internal eMMC flash memory.</p>
<h3 id="on-the-orangepi">On the OrangePi</h3>
<p>I first needed to install <code>rsync</code>.
For that, I installed the <code>Network Tools</code> addon in Kodi.</p>
<p>I then navigated to the Settings menu, chose Interface and went to the Screensaver menu.
There, I said to display pictures from the <code>/storage/pictures</code> folder and that was it!</p>
<p>I then tested the screensaver and saw the pictures I shared earlier to test.</p>
<h2 id="possible-improvements">Possible Improvements</h2>
<p>All the manual steps here were tedious and error-prone.
Coming from the NixOS world, I want this all to be declarative.
This means creating users in Nextcloud declaratively and settings user options.
On the OrangePi side, that will mean probably switching from LibreElec to NixOS, but I’m not sure if that’s necessary.
Anyway, I’ll be working on this.</p>
<h2 id="further-reading">Further Reading</h2>
<p>I talk about how to setup the OrangePi 3 box with LibreElec and Kodi with the Jellyfin for Kodi plugin in <a href="./posts/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3.html">another blog post</a>.</p>
<p>I use <a href="https://github.com/ibizaman/selfhostblocks">Self Host Blocks</a> to setup my server with NixOS and <a href="https://github.com/ibizaman/skarabox">Skarabox</a> to bootstrap a new server.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>A Story about Garbage Collection on NixOS</title>
    <link href="https://blog.tiserbox.com//posts/2024-08-07-a-story-about-garbage-collection-on-nixos.html" />
    <id>https://blog.tiserbox.com//posts/2024-08-07-a-story-about-garbage-collection-on-nixos.html</id>
    <published>2024-08-07T00:00:00Z</published>
    <updated>2024-08-07T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on August  7, 2024
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <!--toc:start-->
<ul>
<li><a href="#context">Context</a></li>
<li><a href="#starting-small">Starting Small</a></li>
<li><a href="#going-deeper">Going Deeper</a></li>
<li><a href="#analyzing-store-size">Analyzing Store Size</a></li>
<li><a href="#analyzing-store-size-some-more">Analyzing Store Size Some More</a></li>
<li><a href="#lets-step-back">Let’s step back</a>
<!--toc:end--></li>
</ul>
<h2 id="context">Context</h2>
<p>So you want to cleanup the Nix store.
I wanted too.
This led me down a medium sized rabbit whole.
Read this post to follow it too!
You might learn a couple things.</p>
<p>I realized my disk was close to full.
74Gb available from a 1Tb SAD.
Time to clean up the Nix store!</p>
<h2 id="starting-small">Starting Small</h2>
<p>The ubiquitous command to delete old NixOS profiles is <code>nix-collect-garbage</code>.
So I used just that:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> df <span class="at">-h</span> /</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="ex">Filesystem</span>             Size  Used Avail Use% Mounted on</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="ex">/dev/disk/by-uuid/...</span>  909G  789G   74G  92% /</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-collect-garbage <span class="at">--delete-older-than</span> 2d <span class="at">--dry-run</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> old generations of profile /home/ibizaman/.local/state/nix/profiles/home-manager</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a><span class="ex">would</span> remove profile version 77</span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a><span class="ex">would</span> remove profile version 76</span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a><span class="ex">would</span> remove profile version 75</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a><span class="ex">would</span> remove profile version 74</span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a><span class="ex">would</span> remove profile version 73</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a><span class="ex">would</span> remove profile version 72</span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="ex">would</span> remove profile version 71</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a><span class="ex">would</span> remove profile version 70</span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a><span class="ex">would</span> remove profile version 69</span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> old generations of profile /nix/var/nix/profiles/per-user/ibizaman/profile</span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> old generations of profile /nix/var/nix/profiles/per-user/ibizaman/profile</span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-collect-garbage <span class="at">--delete-older-than</span> 2d</span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a><span class="ex">note:</span> currently hard linking saves 22381.91 MiB</span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a><span class="ex">16721</span> store paths deleted, 22869.11 MiB freed</span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> df <span class="at">-h</span> /</span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a><span class="ex">Filesystem</span>             Size  Used Avail Use% Mounted on</span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a><span class="ex">/dev/disk/by-uuid/...</span>  909G  765G   97G  89% /</span></code></pre></div>
<p>Not bad, but my disk is still quite full.</p>
<h2 id="going-deeper">Going Deeper</h2>
<p>Next step is to understand what Nix GC root exists, their size and determine which one I’m okay to delete and garbage collect.</p>
<p>From <a href="https://wiki.nixos.org/wiki/Cleaning_the_nix_store">the wiki</a>, one can print the GC roots with:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">--gc</span> <span class="at">--print-roots</span> <span class="kw">|</span> <span class="fu">egrep</span> <span class="at">-v</span> <span class="st">&quot;^(/nix/var|/run/\w+-system|\{memory|/proc)&quot;</span></span></code></pre></div>
<p>This command ignores some paths, notably ones that are used by the current system.</p>
<p>This gave me quite a few GC roots.
Instead of listing them here, I’ll break them down into categories</p>
<p>First, the project result directories. For example:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/selfhostblocks/result-modules</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  <span class="ex">-</span><span class="op">&gt;</span> /nix/store/v8va9mzlwnyplgzdjv0n4hy82m6dr9di-nix-flake-tests-success</span></code></pre></div>
<p>If you don’t need to retain those and you’re okay to rebuild them later, just delete the link.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> rm /home/ibizaman/Projects/selfhostblocks/result-modules</span></code></pre></div>
<p>I had also some home-manager GC roots like:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/.local/state/home-manager/gcroots/current-home</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>  <span class="ex">-</span><span class="op">&gt;</span> /nix/store/srp7f0jfjr8bq4vf030cw1jmgh2yqz34-home-manager-generation</span></code></pre></div>
<p>Understanding why they’re needed is done with <code>nix-store -q --root</code>:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">-q</span> <span class="at">--roots</span> /nix/store/srp7f0jfjr8bq4vf030cw1jmgh2yqz34-home-manager-generation</span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="ex">/nix/var/nix/profiles/system-145-link</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/9fj82c87fphb6j6cxd9i0ns6wiag9gyh-nixos-system-laspin-24.05pre-git</span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="ex">/nix/var/nix/profiles/system-144-link</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/yczj1ln41syk95krrw6n26m6c1wy8q3r-nixos-system-laspin-24.05pre-git</span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a><span class="ex">/nix/var/nix/profiles/system-147-link</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/gyldgm273sgjb214f99h0ks3dbga2qd0-nixos-system-laspin-24.05pre-git</span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a><span class="ex">/run/current-system</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/gyldgm273sgjb214f99h0ks3dbga2qd0-nixos-system-laspin-24.05pre-git</span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a><span class="ex">/nix/var/nix/profiles/system-146-link</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/zj4p4fkyjam69ndgydkl8rrl3m09qyrw-nixos-system-laspin-24.05pre-git</span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a><span class="ex">/run/booted-system</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/zj4p4fkyjam69ndgydkl8rrl3m09qyrw-nixos-system-laspin-24.05pre-git</span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/.local/state/home-manager/gcroots/current-home</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/srp7f0jfjr8bq4vf030cw1jmgh2yqz34-home-manager-generation</span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/.local/state/nix/profiles/home-manager-78-link</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/srp7f0jfjr8bq4vf030cw1jmgh2yqz34-home-manager-generation</span></code></pre></div>
<p>This one is clearly used by a lot of stuff, I probably won’t be able to clean it up.
It makes sense, it’s the <code>current-home</code> one after all.
Another one is easier to delete:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/.local/state/nix/profiles/home-manager-77-link</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>  <span class="ex">-</span><span class="op">&gt;</span> /nix/store/aawzc42n09xg5vm9n3phzagl95b6zfhg-home-manager-generation</span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">-q</span> <span class="at">--roots</span> /nix/store/aawzc42n09xg5vm9n3phzagl95b6zfhg-home-manager-generation</span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/.local/state/nix/profiles/home-manager-67-link</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a>  <span class="ex">-</span><span class="op">&gt;</span> /nix/store/aawzc42n09xg5vm9n3phzagl95b6zfhg-home-manager-generation</span></code></pre></div>
<p>We also have the direnv related ones:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/oxo/.direnv/flake-profile-1-link</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>  <span class="ex">-</span><span class="op">&gt;</span> /nix/store/8b6kd8lcq5vckq0dkpab9hr558088q45-nix-shell-env</span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">-q</span> <span class="at">--roots</span> /nix/store/8b6kd8lcq5vckq0dkpab9hr558088q45-nix-shell-env</span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/oxo/.direnv/flake-profile-1-link</span></span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a>  <span class="ex">-</span><span class="op">&gt;</span> /nix/store/8b6kd8lcq5vckq0dkpab9hr558088q45-nix-shell-env</span></code></pre></div>
<p>Same story as the project ones, you can delete them if they’re not needed anymore.</p>
<p>And then, I had a bunch of:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="ex">{temp:2597292}</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/3d62w5jhcjgdjay4wg351d1jl7l2j2yh-gmp-6.2.1.tar.bz2.drv</span></code></pre></div>
<p>This means a currently running process is holding on to this path.
If nothing else holds on to the path, stopping the process will allow it to get garbage collected.
Let’s check it:</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">-q</span> <span class="at">--roots</span> /nix/store/3d62w5jhcjgdjay4wg351d1jl7l2j2yh-gmp-6.2.1.tar.bz2.drv</span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/blog/result</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/ikvnbi9w7ikbwv7501v9hj4ghnc01y9i-site</span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/blog/.direnv/flake-profile-6-link</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/sn3wz8n8md5y31w2505557ylb2ygillk-site-env-env</span></code></pre></div>
<p>No luck here.
The path is also held by two links.</p>
<h2 id="analyzing-store-size">Analyzing Store Size</h2>
<p>I used a pretty crude method <em>(output formatted by myself)</em>:</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo ncdu /nix/store</span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a><span class="ex">[...]</span></span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a><span class="ex">1.6</span> GiB  /lp2w7k4x4fsaidm18xgqjp2b70i6w6ci-ghc-9.6.4</span>
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a><span class="ex">1.6</span> GiB  /gjng6wrl7dsk8d98yjfiz9qb3g64h9f2-ghc-9.6.4</span>
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a><span class="ex">1.6</span> GiB  /2qqlva2zbkdhbyrz4qyacgq57s8kfy1l-ghc-9.4.8</span>
<span id="cb11-7"><a href="#cb11-7" aria-hidden="true" tabindex="-1"></a><span class="ex">1.6</span> GiB  /k033yvpca1r7fi0hwgkh5wnky5iixvlk-ghc-9.4.8</span>
<span id="cb11-8"><a href="#cb11-8" aria-hidden="true" tabindex="-1"></a><span class="ex">1.6</span> GiB  /psds2pz1qhlr4z8qcahqii6kq1xsawb8-ghc-9.4.8</span>
<span id="cb11-9"><a href="#cb11-9" aria-hidden="true" tabindex="-1"></a><span class="ex">1.6</span> GiB  /dpc4240h48a4kmgj684w1wkjmmi8ccxq-ghc-9.2.8</span>
<span id="cb11-10"><a href="#cb11-10" aria-hidden="true" tabindex="-1"></a><span class="ex">[...]</span></span></code></pre></div>
<p>I probably don’t need all those GHC version.
Let’s see.</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">-q</span> <span class="at">--roots</span> /nix/store/dpc4240h48a4kmgj684w1wkjmmi8ccxq-ghc-9.2.8</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/blog/result</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/ikvnbi9w7ikbwv7501v9hj4ghnc01y9i-site</span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/blog/.direnv/flake-profile-6-link</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/sn3wz8n8md5y31w2505557ylb2ygillk-site-env-env</span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">-q</span> <span class="at">--roots</span> /nix/store/psds2pz1qhlr4z8qcahqii6kq1xsawb8-ghc-9.4.8</span>
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a><span class="ex">/tmp/tmpbldxg_9z/ahsdpqwigcr399x602z9hlyrbmp3pa4q-vm-test-run-postgresql-peerAuth.drv</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/ahsdpqwigcr399x602z9hlyrbmp3pa4q-vm-test-run-postgresql-peerAuth.drv</span>
<span id="cb12-7"><a href="#cb12-7" aria-hidden="true" tabindex="-1"></a><span class="ex">/tmp/tmpbldxg_9z/i00k3m9h6f0ihijmklfk1lhg4dr3cjw7-vm-test-run-postgresql-peerWithoutUser.drv</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/i00k3m9h6f0ihijmklfk1lhg4dr3cjw7-vm-test-run-postgresql-peerWithoutUser.drv</span>
<span id="cb12-8"><a href="#cb12-8" aria-hidden="true" tabindex="-1"></a><span class="ex">/tmp/tmpbldxg_9z/8jc6kjg3qryhzrkggnw3f5kp8xsgzis2-vm-test-run-postgresql-tcpIpWithoutPasswordAuth.drv</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/8jc6kjg3qryhzrkggnw3f5kp8xsgzis2-vm-test-run-postgresql-tcpIpWithou<span class="op">&gt;</span></span>
<span id="cb12-9"><a href="#cb12-9" aria-hidden="true" tabindex="-1"></a><span class="ex">/tmp/tmpbldxg_9z/mgy2s5368srbps5bdan8774xdf2q1p3f-vm-test-run-monitoring-basic.drv</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/mgy2s5368srbps5bdan8774xdf2q1p3f-vm-test-run-monitoring-basic.drv</span>
<span id="cb12-10"><a href="#cb12-10" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/blog/.direnv/flake-profile-6-link</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/sn3wz8n8md5y31w2505557ylb2ygillk-site-env-env</span>
<span id="cb12-11"><a href="#cb12-11" aria-hidden="true" tabindex="-1"></a><span class="ex">/tmp/tmpbldxg_9z/la2qw92yhz2y2kd6qv2wn9jjvcy286z0-vm-test-run-postgresql-tcpIPPasswordAuth.drv</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/la2qw92yhz2y2kd6qv2wn9jjvcy286z0-vm-test-run-postgresql-tcpIPPasswordAuth.<span class="op">&gt;</span></span>
<span id="cb12-12"><a href="#cb12-12" aria-hidden="true" tabindex="-1"></a><span class="ex">/tmp/tmpbldxg_9z/4blarcmd0hrggpvz8qbmkxmgnndmy9ss-vm-test-run-ldap-auth.drv</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/4blarcmd0hrggpvz8qbmkxmgnndmy9ss-vm-test-run-ldap-auth.drv</span></code></pre></div>
<p>I want to keep the GC roots of my blog here, so I won’t mess with those.</p>
<div class="sourceCode" id="cb13"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">-q</span> <span class="at">--roots</span> /nix/store/k033yvpca1r7fi0hwgkh5wnky5iixvlk-ghc-9.4.8</span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/selfhostblocks/result-vm_authelia_basic</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/xrihrmahqg32xwd7f7gjpcj02kpdjp67-vm-test-run-authelia-basic</span></code></pre></div>
<p>This one can go away.</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> rm /home/ibizaman/Projects/selfhostblocks/result-vm_authelia_basic</span></code></pre></div>
<div class="sourceCode" id="cb15"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">--gc</span> <span class="at">--print-dead</span></span>
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a><span class="ex">finding</span> garbage collector roots...</span>
<span id="cb15-3"><a href="#cb15-3" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> stale link from <span class="st">&#39;/nix/var/nix/gcroots/auto/44wr5bc9szn0f870qy673kqi8ivhmc7c&#39;</span> to <span class="st">&#39;/home/ibizaman/Projects/selfhostblocks/result-vm_authelia_basic&#39;</span></span>
<span id="cb15-4"><a href="#cb15-4" aria-hidden="true" tabindex="-1"></a><span class="ex">determining</span> live/dead paths...</span>
<span id="cb15-5"><a href="#cb15-5" aria-hidden="true" tabindex="-1"></a><span class="ex">[...]</span></span></code></pre></div>
<p>That printed a bunch of paths, good!</p>
<div class="sourceCode" id="cb16"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">--gc</span></span>
<span id="cb16-2"><a href="#cb16-2" aria-hidden="true" tabindex="-1"></a><span class="ex">[...]</span></span>
<span id="cb16-3"><a href="#cb16-3" aria-hidden="true" tabindex="-1"></a><span class="ex">note:</span> currently hard linking saves 17869.65 MiB</span>
<span id="cb16-4"><a href="#cb16-4" aria-hidden="true" tabindex="-1"></a><span class="ex">5011</span> store paths deleted, 3609.95 MiB freed</span></code></pre></div>
<p>3Gb saved. Not bad.
Let’s try another GHC one.</p>
<div class="sourceCode" id="cb17"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb17-1"><a href="#cb17-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">-q</span> <span class="at">--roots</span> /nix/store/2qqlva2zbkdhbyrz4qyacgq57s8kfy1l-ghc-9.4.8</span>
<span id="cb17-2"><a href="#cb17-2" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/selfhostblocks/result-vm_monitoring_auth</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/avqv7a5sr382f35z4ahksa8kilbn569g-vm-test-run-monitoring-basic</span>
<span id="cb17-3"><a href="#cb17-3" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/selfhostblocks/result-vm_postgresql_peerWithoutUser</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/6qgznnl4wmg32q96lmcmwqsvzsxrw1q4-vm-test-run-postgresql-peerWithoutUser</span>
<span id="cb17-4"><a href="#cb17-4" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/selfhostblocks/result-vm_ssl_test</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/x8vprjx1jfbqzis874sc38b6kx5rmqa1-vm-test-run-ssl-test</span>
<span id="cb17-5"><a href="#cb17-5" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/selfhostblocks/result-vm_postgresql_tcpIPPasswordAuth</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/vrs0klwdk7dxqbagbv3qbxwiv9zh5aly-vm-test-run-postgresql-tcpIPPasswordAuth</span>
<span id="cb17-6"><a href="#cb17-6" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/selfhostblocks/result-vm_lib_template</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/rgbqh4qmkkdml7inrs564w5r8wj35is4-vm-test-run-lib-template</span>
<span id="cb17-7"><a href="#cb17-7" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/selfhostblocks/result-vm_nextcloud_basic</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/a783xnwswwh8wmnvb9y2axm4cczcmn98-vm-test-run-nextcloud-basic</span>
<span id="cb17-8"><a href="#cb17-8" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/selfhostblocks/result-vm_ldap_auth</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/swnzf8lsqq8s60g5mcqaxjd1jz8p2z7m-vm-test-run-ldap-auth</span>
<span id="cb17-9"><a href="#cb17-9" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/selfhostblocks/result-vm_postgresql_peerAuth</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/f935av428f1cky6s8mh4yvf36y0m5g9a-vm-test-run-postgresql-peerAuth</span>
<span id="cb17-10"><a href="#cb17-10" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/selfhostblocks/result-vm_postgresql_tcpIPWithoutPasswordAuth</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/6nmzajhcrh8vgd65ck5hawy9bwq0c6z3-vm-test-run-postgresql-tcpIpWithoutPasswordAuth</span></code></pre></div>
<p>Okay, I’m fine getting rid of this one.
With some bash tricks, I can remove all those <code>result-vm-*</code> links with a one-liner:</p>
<div class="sourceCode" id="cb18"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb18-1"><a href="#cb18-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">-q</span> <span class="at">--roots</span> /nix/store/2qqlva2zbkdhbyrz4qyacgq57s8kfy1l-ghc-9.4.8 <span class="dt">\</span></span>
<span id="cb18-2"><a href="#cb18-2" aria-hidden="true" tabindex="-1"></a>    <span class="kw">|</span> <span class="fu">awk</span> <span class="at">-F</span><span class="st">&#39; -&gt; &#39;</span> <span class="st">&#39;{print $1}&#39;</span> <span class="dt">\</span></span>
<span id="cb18-3"><a href="#cb18-3" aria-hidden="true" tabindex="-1"></a>    <span class="kw">|</span> <span class="fu">xargs</span> rm</span>
<span id="cb18-4"><a href="#cb18-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb18-5"><a href="#cb18-5" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">--gc</span> <span class="at">--print-dead</span></span>
<span id="cb18-6"><a href="#cb18-6" aria-hidden="true" tabindex="-1"></a><span class="ex">finding</span> garbage collector roots...</span>
<span id="cb18-7"><a href="#cb18-7" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> stale link from <span class="st">&#39;/nix/var/nix/gcroots/auto/8df9bml2011d32bilfkga61r332q7kik&#39;</span> to <span class="st">&#39;/home/ibizaman/Projects/selfhostblocks/result-vm_lib_template&#39;</span></span>
<span id="cb18-8"><a href="#cb18-8" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> stale link from <span class="st">&#39;/nix/var/nix/gcroots/auto/l57ixd12ad2kqh7241bg5i7g05vssxr4&#39;</span> to <span class="st">&#39;/home/ibizaman/Projects/selfhostblocks/result-vm_postgresql_peerWithoutUser&#39;</span></span>
<span id="cb18-9"><a href="#cb18-9" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> stale link from <span class="st">&#39;/nix/var/nix/gcroots/auto/c2ja40qlc5110bd000ws1f14drjbkpvk&#39;</span> to <span class="st">&#39;/home/ibizaman/Projects/selfhostblocks/result-vm_postgresql_tcpIPWithoutPasswordAuth&#39;</span></span>
<span id="cb18-10"><a href="#cb18-10" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> stale link from <span class="st">&#39;/nix/var/nix/gcroots/auto/wkm7xcfg9in3c8pm01zl3d5p72p13f9h&#39;</span> to <span class="st">&#39;/home/ibizaman/Projects/selfhostblocks/result-vm_postgresql_tcpIPPasswordAuth&#39;</span></span>
<span id="cb18-11"><a href="#cb18-11" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> stale link from <span class="st">&#39;/nix/var/nix/gcroots/auto/ax33alfd9hw7xigb20qrpncmgmgn32rp&#39;</span> to <span class="st">&#39;/home/ibizaman/Projects/selfhostblocks/result-vm_postgresql_peerAuth&#39;</span></span>
<span id="cb18-12"><a href="#cb18-12" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> stale link from <span class="st">&#39;/nix/var/nix/gcroots/auto/6i84xyjq0fq6w45nmrr1bmkr26ji02qj&#39;</span> to <span class="st">&#39;/home/ibizaman/Projects/selfhostblocks/result-vm_ldap_auth&#39;</span></span>
<span id="cb18-13"><a href="#cb18-13" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> stale link from <span class="st">&#39;/nix/var/nix/gcroots/auto/2dp2ac8vqclzci3mab7pc1xj1k67frvw&#39;</span> to <span class="st">&#39;/home/ibizaman/Projects/selfhostblocks/result-vm_ssl_test&#39;</span></span>
<span id="cb18-14"><a href="#cb18-14" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> stale link from <span class="st">&#39;/nix/var/nix/gcroots/auto/71r3b6wrbg9vayigzgakq4bpsc0j0s5m&#39;</span> to <span class="st">&#39;/home/ibizaman/Projects/selfhostblocks/result-vm_nextcloud_basic&#39;</span></span>
<span id="cb18-15"><a href="#cb18-15" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> stale link from <span class="st">&#39;/nix/var/nix/gcroots/auto/qc7czk9rq88iflpry9a8zmjvfrcjsq2s&#39;</span> to <span class="st">&#39;/home/ibizaman/Projects/selfhostblocks/result-vm_monitoring_auth&#39;</span></span>
<span id="cb18-16"><a href="#cb18-16" aria-hidden="true" tabindex="-1"></a><span class="ex">determining</span> live/dead paths...</span>
<span id="cb18-17"><a href="#cb18-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb18-18"><a href="#cb18-18" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">--gc</span></span>
<span id="cb18-19"><a href="#cb18-19" aria-hidden="true" tabindex="-1"></a><span class="ex">[...]</span></span>
<span id="cb18-20"><a href="#cb18-20" aria-hidden="true" tabindex="-1"></a><span class="ex">note:</span> currently hard linking saves 14657.84 MiB</span>
<span id="cb18-21"><a href="#cb18-21" aria-hidden="true" tabindex="-1"></a><span class="ex">6299</span> store paths deleted, 7420.39 MiB freed</span></code></pre></div>
<p>Let’s see where we’re at:</p>
<div class="sourceCode" id="cb19"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb19-1"><a href="#cb19-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> df <span class="at">-h</span> /</span>
<span id="cb19-2"><a href="#cb19-2" aria-hidden="true" tabindex="-1"></a><span class="ex">Filesystem</span>             Size  Used Avail Use% Mounted on</span>
<span id="cb19-3"><a href="#cb19-3" aria-hidden="true" tabindex="-1"></a><span class="ex">/dev/disk/by-uuid/...</span>  909G  754G  109G  88% /</span></code></pre></div>
<p>I continued removing some GHC roots and got up to 120Gb availability.</p>
<h2 id="analyzing-store-size-some-more">Analyzing Store Size Some More</h2>
<p>I used some pretty crude commands to figure out what roots take space.</p>
<div class="sourceCode" id="cb20"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb20-1"><a href="#cb20-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">--gc</span> <span class="at">--print-roots</span> <span class="kw">|</span> <span class="fu">grep</span> <span class="at">-v</span> <span class="st">&quot;^/proc&quot;</span></span>
<span id="cb20-2"><a href="#cb20-2" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/.cache/nix/flake-registry.json</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/5bs7lbd1fk22w7bzdd8d6fvysnyzgw35-flake-registry.json</span>
<span id="cb20-3"><a href="#cb20-3" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/.local/state/home-manager/gcroots/current-home</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/srp7f0jfjr8bq4vf030cw1jmgh2yqz34-home-manager-generation</span>
<span id="cb20-4"><a href="#cb20-4" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/blog/.direnv/flake-profile-6-link</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/sn3wz8n8md5y31w2505557ylb2ygillk-site-env-env</span>
<span id="cb20-5"><a href="#cb20-5" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/blog/result</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/ikvnbi9w7ikbwv7501v9hj4ghnc01y9i-site</span>
<span id="cb20-6"><a href="#cb20-6" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/esp/.direnv/flake-profile-2-link</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/ldbk3nd4hpkj9xiryhjy83inl5yawa9z-nix-shell-env</span>
<span id="cb20-7"><a href="#cb20-7" aria-hidden="true" tabindex="-1"></a><span class="ex">/home/ibizaman/Projects/nix-config/result</span> <span class="at">-</span><span class="op">&gt;</span> /nix/store/6dbvmllgim249w566fikwxy6yza37i7g-nixos-24.05.20240421.6143fc5-x86_64-linux.iso</span>
<span id="cb20-8"><a href="#cb20-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb20-9"><a href="#cb20-9" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> du <span class="at">-sch</span> <span class="va">$(</span><span class="ex">nix-store</span> <span class="at">--gc</span> <span class="at">--print-roots</span> <span class="kw">|</span> <span class="fu">grep</span> <span class="at">-v</span> <span class="st">&quot;^/proc&quot;</span> <span class="kw">|</span> <span class="fu">awk</span> <span class="at">-F</span> <span class="st">&#39; -&gt; &#39;</span> <span class="st">&#39;{ print $2 }&#39;</span><span class="va">)</span></span>
<span id="cb20-10"><a href="#cb20-10" aria-hidden="true" tabindex="-1"></a><span class="ex">8.0K</span>    /nix/store/5bs7lbd1fk22w7bzdd8d6fvysnyzgw35-flake-registry.json</span>
<span id="cb20-11"><a href="#cb20-11" aria-hidden="true" tabindex="-1"></a><span class="ex">40K</span>     /nix/store/srp7f0jfjr8bq4vf030cw1jmgh2yqz34-home-manager-generation</span>
<span id="cb20-12"><a href="#cb20-12" aria-hidden="true" tabindex="-1"></a><span class="ex">68K</span>     /nix/store/pvxnmywrjn7l3rw35zwd87ixd10lkicj-nix-shell-env</span>
<span id="cb20-13"><a href="#cb20-13" aria-hidden="true" tabindex="-1"></a><span class="ex">13M</span>     /nix/store/ikvnbi9w7ikbwv7501v9hj4ghnc01y9i-site</span>
<span id="cb20-14"><a href="#cb20-14" aria-hidden="true" tabindex="-1"></a><span class="ex">80K</span>     /nix/store/ldbk3nd4hpkj9xiryhjy83inl5yawa9z-nix-shell-env</span>
<span id="cb20-15"><a href="#cb20-15" aria-hidden="true" tabindex="-1"></a><span class="ex">1002M</span>   /nix/store/6dbvmllgim249w566fikwxy6yza37i7g-nixos-24.05.20240421.6143fc5-x86_64-linux.iso</span></code></pre></div>
<p>That last one can go away!</p>
<div class="sourceCode" id="cb21"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb21-1"><a href="#cb21-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> rm /home/ibizaman/Projects/nix-config/result</span>
<span id="cb21-2"><a href="#cb21-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb21-3"><a href="#cb21-3" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-store <span class="at">--gc</span></span>
<span id="cb21-4"><a href="#cb21-4" aria-hidden="true" tabindex="-1"></a><span class="ex">finding</span> garbage collector roots...</span>
<span id="cb21-5"><a href="#cb21-5" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> stale link from <span class="st">&#39;/nix/var/nix/gcroots/auto/8xisdidbngs2s2y8y5hizxpvhydns4df&#39;</span> to <span class="st">&#39;/home/ibizaman/Projects/nix-config/result&#39;</span></span>
<span id="cb21-6"><a href="#cb21-6" aria-hidden="true" tabindex="-1"></a><span class="ex">deleting</span> garbage...</span>
<span id="cb21-7"><a href="#cb21-7" aria-hidden="true" tabindex="-1"></a><span class="ex">[...]</span></span>
<span id="cb21-8"><a href="#cb21-8" aria-hidden="true" tabindex="-1"></a><span class="ex">deleting</span> unused links...</span>
<span id="cb21-9"><a href="#cb21-9" aria-hidden="true" tabindex="-1"></a><span class="ex">note:</span> currently hard linking saves 8562.92 MiB</span>
<span id="cb21-10"><a href="#cb21-10" aria-hidden="true" tabindex="-1"></a><span class="ex">389</span> store paths deleted, 1128.68 MiB freed</span>
<span id="cb21-11"><a href="#cb21-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb21-12"><a href="#cb21-12" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> df <span class="at">-h</span> /</span>
<span id="cb21-13"><a href="#cb21-13" aria-hidden="true" tabindex="-1"></a><span class="ex">Filesystem</span>             Size  Used Avail Use% Mounted on</span>
<span id="cb21-14"><a href="#cb21-14" aria-hidden="true" tabindex="-1"></a><span class="ex">/dev/disk/by-uuid/...</span>  909G  741G  122G  86% /</span></code></pre></div>
<p>Still 741G used?</p>
<h2 id="lets-step-back">Let’s step back</h2>
<p>I’m not likely to make any progress cleaning up the Nix store anymore.
So what’s clogging up the disk space?</p>
<div class="sourceCode" id="cb22"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb22-1"><a href="#cb22-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> du <span class="at">-hs</span> /nix/store/</span>
<span id="cb22-2"><a href="#cb22-2" aria-hidden="true" tabindex="-1"></a><span class="ex">75G</span>     /nix/store/</span></code></pre></div>
<p>Not the nix store, that’s for sure.</p>
<p>I ran <code>ncdu</code> on my <code>/home</code> directory and found roughly 150Gb of usage there.
That doesn’t explain the used 741Gb.
Where does that live?</p>
<p>Well, after searching here and there, I found it.
It was the trash!
So obvious in hindsight but I never cleaned the KDE trash.
500Gb was living in there.</p>
<p>Lesson: I should’ve taken a step back earlier.
On the other hand, this blog post wouldn’t exist if I did.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Improving Git Speed with Large Repositories</title>
    <link href="https://blog.tiserbox.com//posts/2024-07-10-improving-git-speed-with-large-repositories.html" />
    <id>https://blog.tiserbox.com//posts/2024-07-10-improving-git-speed-with-large-repositories.html</id>
    <published>2024-07-10T00:00:00Z</published>
    <updated>2024-07-10T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on July 10, 2024
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;git&#39;." href="/tags/git.html">git</a>, <a title="All pages tagged &#39;performance&#39;." href="/tags/performance.html">performance</a>
	  
	</section>
    <section>
        <p>It’s no secret that the <a href="https://github.com/NixOS/nixpkgs/">nixpkgs</a> repo is quite big.
Big enough to slow down git operations to noticeable lags.</p>
<p>I use Emacs and Magit and with those, loading the status page takes - I kid you not - 15 seconds.
Now, I don’t mind waiting a couple seconds, but here it’s just over my patience threshold.</p>
<p>I must say, the conclusion will be obvious in retrospective but since we use layers upon layers of software - here Magit over Emacs over Git - it’s not always obvious where the fix should happen.
And that’s what prompted me to write this blog post, so I can showcase how to debug such a situation.</p>
<p>So, to investigate what’s going on, I toggled verbose logging with <code>M-x magit-toggle-verbose-refresh</code> so that loading the <code>magit-status</code> prints loading time.
This is what I get:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="ex">Refreshing</span> magit...</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="ex">Running</span> magit-pre-refresh-hook...done <span class="er">(</span><span class="ex">0.012s</span><span class="kw">)</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Refreshing</span> buffer ‘magit: nixpkgs’...</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-error-header</span>                0.000006 </span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-diff-filter-header</span>          0.009790 </span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-head-branch-header</span>          0.012734 !</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-upstream-branch-header</span>      0.000090 </span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-push-branch-header</span>          0.012852 !</span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-tags-header</span>                 8.128425 !!</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-status-headers</span>              8.173127 !!</span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-merge-log</span>                   0.004723 </span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-rebase-sequence</span>             0.000119 </span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-am-sequence</span>                 0.000066 </span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-sequencer-sequence</span>          0.000206 </span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-bisect-output</span>               0.000063 </span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-bisect-rest</span>                 0.000054 </span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-bisect-log</span>                  0.000055 </span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-untracked-files</span>             0.394513 !!</span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-unstaged-changes</span>            0.055459 !!</span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-staged-changes</span>              0.036347 !!</span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-stashes</span>                     0.021789 !</span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a>  <span class="ex">m..-in..-unpushed-to-pushremote</span>          0.038461 !!</span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a>  <span class="ex">m..-in..-unpushed-to-upstream-or-recent</span>  0.344540 !!</span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a>  <span class="ex">m..-in..-unpulled-from-pushremote</span>        0.039289 !!</span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-unpulled-from-upstream</span>      0.000005 </span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-local-branches</span>              6.681158 !!</span>
<span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a>  <span class="ex">forge-insert-issues</span>                      0.013407 !</span>
<span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a>  <span class="ex">forge-insert-pullreqs</span>                    0.002832 </span>
<span id="cb1-29"><a href="#cb1-29" aria-hidden="true" tabindex="-1"></a><span class="ex">Refreshing</span> buffer ‘magit: nixpkgs’...done <span class="er">(</span><span class="ex">15.884s</span><span class="kw">)</span></span>
<span id="cb1-30"><a href="#cb1-30" aria-hidden="true" tabindex="-1"></a><span class="ex">Running</span> magit-post-refresh-hook...done <span class="er">(</span><span class="ex">0.009s</span><span class="kw">)</span></span>
<span id="cb1-31"><a href="#cb1-31" aria-hidden="true" tabindex="-1"></a><span class="ex">Refreshing</span> magit...done <span class="er">(</span><span class="ex">15.907s,</span> cached 64/93 <span class="er">(</span><span class="ex">69%</span><span class="kw">))</span></span></code></pre></div>
<p>Several steps take a long time. The first being <code>magit-insert-tags-header</code>.</p>
<p>Thanks to the <a href="https://github.com/Wilfred/helpful">helpful</a> package, I can <code>C-h h magit-insert-tags-header</code> and see the source code of the function directly in the help buffer.
And here it is:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode lisp"><code class="sourceCode commonlisp"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>(<span class="kw">defun</span><span class="fu"> magit-insert-tags-header </span>()</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;Insert a header line about the current and/or next tag.&quot;</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">let*</span> ((this-tag (magit-get-current-tag <span class="kw">nil</span> <span class="kw">t</span>))</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>         (next-tag (magit-get-next-tag <span class="kw">nil</span> <span class="kw">t</span>))</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>         (this-cnt (<span class="kw">cadr</span> this-tag))</span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>         (next-cnt (<span class="kw">cadr</span> next-tag))</span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>         (this-tag (<span class="kw">car</span> this-tag))</span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>         (next-tag (<span class="kw">car</span> next-tag))</span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a>         (both-tags (<span class="kw">and</span> this-tag next-tag <span class="kw">t</span>)))</span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a>    (<span class="kw">when</span> (<span class="kw">or</span> this-tag next-tag)</span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a>      (magit-insert-section (tag (<span class="kw">or</span> this-tag next-tag))</span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a>        (insert (<span class="kw">format</span> <span class="st">&quot;%-10s&quot;</span> (<span class="kw">if</span> both-tags <span class="st">&quot;Tags: &quot;</span> <span class="st">&quot;Tag: &quot;</span>)))</span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a>        (cl-flet ((insert-count (tag <span class="kw">count</span> face)</span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a>                    (insert (concat (propertize tag <span class="dt">&#39;font-lock-face</span> <span class="dt">&#39;magit-tag</span>)</span>
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a>                                    (<span class="kw">and</span> (<span class="op">&gt;</span> <span class="kw">count</span> <span class="dv">0</span>)</span>
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a>                                         (<span class="kw">format</span> <span class="st">&quot; (%s)&quot;</span></span>
<span id="cb2-17"><a href="#cb2-17" aria-hidden="true" tabindex="-1"></a>                                                 (propertize</span>
<span id="cb2-18"><a href="#cb2-18" aria-hidden="true" tabindex="-1"></a>                                                  (<span class="kw">format</span> <span class="st">&quot;%s&quot;</span> <span class="kw">count</span>)</span>
<span id="cb2-19"><a href="#cb2-19" aria-hidden="true" tabindex="-1"></a>                                                  <span class="dt">&#39;font-lock-face</span> face)))))))</span>
<span id="cb2-20"><a href="#cb2-20" aria-hidden="true" tabindex="-1"></a>          (<span class="kw">when</span> this-tag  (insert-count this-tag this-cnt <span class="dt">&#39;magit-branch-local</span>))</span>
<span id="cb2-21"><a href="#cb2-21" aria-hidden="true" tabindex="-1"></a>          (<span class="kw">when</span> both-tags (insert <span class="st">&quot;, &quot;</span>))</span>
<span id="cb2-22"><a href="#cb2-22" aria-hidden="true" tabindex="-1"></a>          (<span class="kw">when</span> next-tag  (insert-count next-tag next-cnt <span class="dt">&#39;magit-tag</span>)))</span>
<span id="cb2-23"><a href="#cb2-23" aria-hidden="true" tabindex="-1"></a>        (insert ?\n)))))</span></code></pre></div>
<p>From looking at all the functions in there, the two that call to git and are likely the slow ones are in the first two lines: <code>(magit-get-current-tag nil t)</code> and <code>(magit-get-next-tag nil t)</code>.</p>
<p>So I copy pasted the two expressions one by one then ran them with <code>Alt-:</code> followed by <code>C-y</code> and <code>Enter</code>.
The <code>magit-get-current-tag</code> is the one actually taking those 8 seconds.
The second one takes no time to run.</p>
<p>Going one level deeper, <code>C-h h</code> reveals the source code for <code>magit-get-current-tag</code>:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode lisp"><code class="sourceCode commonlisp"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>(<span class="kw">defun</span><span class="fu"> magit-get-current-tag </span>(&amp;optional rev with-distance)</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;Return the closest tag reachable from REV.</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="st">If optional REV is nil, then default to `HEAD&#39;.</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="st">If optional WITH-DISTANCE is non-nil then return (TAG COMMITS),</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="st">if it is `dirty&#39; return (TAG COMMIT DIRTY). COMMITS is the number</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="st">of commits in `HEAD&#39; but not in TAG and DIRTY is t if there are</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="st">uncommitted changes, nil otherwise.&quot;</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>  (and-let* ((str (magit-git-str <span class="st">&quot;describe&quot;</span> <span class="st">&quot;--long&quot;</span> <span class="st">&quot;--tags&quot;</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>                                 (<span class="kw">and</span> (<span class="kw">eq</span> with-distance <span class="dt">&#39;dirty</span>) <span class="st">&quot;--dirty&quot;</span>)</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>                                 rev)))</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>    (save-match-data</span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>      (string-match</span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>       <span class="st">&quot;</span><span class="sc">\\</span><span class="st">(.+</span><span class="sc">\\</span><span class="st">)-</span><span class="sc">\\</span><span class="st">(?:0[0-9]*</span><span class="sc">\\</span><span class="st">|</span><span class="sc">\\</span><span class="st">([0-9]+</span><span class="sc">\\</span><span class="st">)</span><span class="sc">\\</span><span class="st">)-g[0-9a-z]+</span><span class="sc">\\</span><span class="st">(-dirty</span><span class="sc">\\</span><span class="st">)?$&quot;</span> str)</span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>      (<span class="kw">if</span> with-distance</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>          `(,(match-string <span class="dv">1</span> str)</span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a>            ,(string-to-number (<span class="kw">or</span> (match-string <span class="dv">2</span> str) <span class="st">&quot;0&quot;</span>))</span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a>            ,@(<span class="kw">and</span> (match-string <span class="dv">3</span> str) (<span class="kw">list</span> <span class="kw">t</span>)))</span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a>        (match-string <span class="dv">1</span> str)))))</span></code></pre></div>
<p>Here too the probable slow call is easy to spot, it’s in the first line, the call to <code>magit-git-str</code>.
I ran the expression and indeed, it’s the slow one.</p>
<p>This means the slowness is with git!
I thus ran the command in the terminal directly: <code>git describe --long --tags</code> and indeed, that took roughly 8 seconds to run.
I tried without arguments but no improvements.</p>
<p>Next step, how to make git faster?
I found <a href="https://stackoverflow.com/a/3339609/1013628">this StackOverflow answer</a> and tried it.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="fu">git</span> gc <span class="at">--aggressive</span></span></code></pre></div>
<p>But the terminal got killed after running for around 5 minutes because my machine was out of memory!</p>
<p>So, how to make git take less memory?
I found <a href="https://stackoverflow.com/questions/8214321/git-gc-using-excessive-memory-unable-to-complete">this other StackOverflow question</a> with the answer highlighted at the top.
I thus added the following snippet to the local <code>.git/config</code> file:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode ini"><code class="sourceCode ini"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="kw">[pack]</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="dt">    packSizeLimit </span><span class="ot">=</span><span class="st"> 64m</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="dt">    threads </span><span class="ot">=</span><span class="st"> </span><span class="dv">6</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="kw">[gc]</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a><span class="dt">    aggressiveWindow </span><span class="ot">=</span><span class="st"> </span><span class="dv">150</span></span></code></pre></div>
<p>I bumped <code>pack.threads</code> from 1 as recommended in the post to 6 just to speed things up a bit.
I have no idea what the best number would be, I just tried a few.
The process now took at most half of CPU time and 70% of 16Gb of memory.
At least it was in check and could complete.</p>
<p>Same for the other fields.
I must admit I didn’t look up what they meant nor did check if they were good values.
I just wanted to fix this quickly.</p>
<p>So with that, I could run <code>git gc --aggressive</code> again which, after about 45 minutes, gave me:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="ex">Enumerating</span> objects: 5289057, done.</span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="ex">Counting</span> objects: 100% <span class="er">(</span><span class="ex">5289057/5289057</span><span class="kw">)</span><span class="ex">,</span> done.</span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Delta</span> compression using up to 6 threads</span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a><span class="ex">Compressing</span> objects: 100% <span class="er">(</span><span class="ex">4911761/4911761</span><span class="kw">)</span><span class="ex">,</span> done.</span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a><span class="ex">Writing</span> objects: 100% <span class="er">(</span><span class="ex">5289057/5289057</span><span class="kw">)</span><span class="ex">,</span> done.</span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a><span class="ex">Total</span> 5289057 <span class="er">(</span><span class="ex">delta</span> 3262154<span class="kw">)</span><span class="ex">,</span> reused 1315941 <span class="er">(</span><span class="ex">delta</span> 0<span class="kw">)</span><span class="ex">,</span> pack-reused 0 <span class="er">(</span><span class="ex">from</span> 0<span class="kw">)</span></span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a><span class="ex">Checking</span> connectivity: 5289057, done.</span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a><span class="ex">Expanding</span> reachable commits in commit graph: 717447, done.</span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a><span class="ex">Writing</span> out commit graph in 5 passes: 100% <span class="er">(</span><span class="ex">3587235/3587235</span><span class="kw">)</span><span class="ex">,</span> done.</span></code></pre></div>
<p>Yes, that took 45 minutes.</p>
<p>But at least, the <code>git describe</code> operation was an order of magnitude faster now:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> time git describe <span class="at">--long</span> <span class="at">--tags</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a><span class="ex">24.05-pre-59983-g7041e60248e5</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a><span class="ex">real</span>    0m0.621s</span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a><span class="ex">user</span>    0m0.521s</span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a><span class="ex">sys</span>     0m0.099s</span></code></pre></div>
<p>And indeed, refreshing the Magit status buffer takes around 3 seconds only now:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="ex">Refreshing</span> buffer ‘magit: nixpkgs’...</span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-error-header</span>                0.000812 </span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-diff-filter-header</span>          0.000052 </span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-head-branch-header</span>          0.022620 !</span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-upstream-branch-header</span>      0.000064 </span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-push-branch-header</span>          0.017885 !</span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-tags-header</span>                 0.660671 !!</span>
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-status-headers</span>              0.707210 !!</span>
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-merge-log</span>                   0.000586 </span>
<span id="cb8-10"><a href="#cb8-10" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-rebase-sequence</span>             0.000657 </span>
<span id="cb8-11"><a href="#cb8-11" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-am-sequence</span>                 0.000045 </span>
<span id="cb8-12"><a href="#cb8-12" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-sequencer-sequence</span>          0.000126 </span>
<span id="cb8-13"><a href="#cb8-13" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-bisect-output</span>               0.000477 </span>
<span id="cb8-14"><a href="#cb8-14" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-bisect-rest</span>                 0.000030 </span>
<span id="cb8-15"><a href="#cb8-15" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-bisect-log</span>                  0.000024 </span>
<span id="cb8-16"><a href="#cb8-16" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-untracked-files</span>             0.329094 !!</span>
<span id="cb8-17"><a href="#cb8-17" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-unstaged-changes</span>            0.046189 !!</span>
<span id="cb8-18"><a href="#cb8-18" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-staged-changes</span>              0.036653 !!</span>
<span id="cb8-19"><a href="#cb8-19" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-stashes</span>                     0.035565 !!</span>
<span id="cb8-20"><a href="#cb8-20" aria-hidden="true" tabindex="-1"></a>  <span class="ex">m..-in..-unpushed-to-pushremote</span>          0.023885 !</span>
<span id="cb8-21"><a href="#cb8-21" aria-hidden="true" tabindex="-1"></a>  <span class="ex">m..-in..-unpushed-to-upstream-or-recent</span>  0.077200 !!</span>
<span id="cb8-22"><a href="#cb8-22" aria-hidden="true" tabindex="-1"></a>  <span class="ex">m..-in..-unpulled-from-pushremote</span>        0.022968 !</span>
<span id="cb8-23"><a href="#cb8-23" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-unpulled-from-upstream</span>      0.000074 </span>
<span id="cb8-24"><a href="#cb8-24" aria-hidden="true" tabindex="-1"></a>  <span class="ex">magit-insert-local-branches</span>              1.507013 !!</span>
<span id="cb8-25"><a href="#cb8-25" aria-hidden="true" tabindex="-1"></a>  <span class="ex">forge-insert-issues</span>                      0.024168 !</span>
<span id="cb8-26"><a href="#cb8-26" aria-hidden="true" tabindex="-1"></a>  <span class="ex">forge-insert-pullreqs</span>                    0.014356 !</span>
<span id="cb8-27"><a href="#cb8-27" aria-hidden="true" tabindex="-1"></a><span class="ex">Refreshing</span> buffer ‘magit: nixpkgs’...done <span class="er">(</span><span class="ex">3.228s</span><span class="kw">)</span></span></code></pre></div>
<p>There could be more optimizations to be done, but that’s enough for me for now.</p>
<p>Conclusion: let’s not forget to run <code>git gc</code> from time to time, especially for big repositories.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Make Good Stubs with Stateful Property Testing</title>
    <link href="https://blog.tiserbox.com//posts/2024-07-08-make-good-stubs-with-stateful-property-testing.html" />
    <id>https://blog.tiserbox.com//posts/2024-07-08-make-good-stubs-with-stateful-property-testing.html</id>
    <published>2024-07-08T00:00:00Z</published>
    <updated>2024-07-08T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on July  8, 2024
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;go&#39;." href="/tags/go.html">go</a>, <a title="All pages tagged &#39;testing&#39;." href="/tags/testing.html">testing</a>
	  
	</section>
    <section>
        <!--toc:start-->
<ul>
<li><a href="#context">Context</a></li>
<li><a href="#integration-tests">Integration Tests</a></li>
<li><a href="#where-mocks-fail">Where Mocks Fail</a></li>
<li><a href="#where-stubs-shine">Where Stubs Shine</a></li>
<li><a href="#conclusion">Conclusion</a>
<!--toc:end--></li>
</ul>
<h2 id="context">Context</h2>
<p>In a previous post, I <a href="https://blog.tiserbox.com/posts/2024-02-27-stateful-property-testing-in-go.html">wrote about how using stateful property testing</a> helped us find test cases that were hard to find by hand.
This helped us complete a project in a timely fashion.
But some other surprising benefits came from introducing stateful property testing.</p>
<p>To switch from a deprecated MongoDB driver to the official one, we created a regression test suite.
We first added stateful property tests, ensuring we covered as much behavior of the deprecated driver as we could.
We then could make the new driver behave the same way as the the old one by making it pass those same tests.
This gave us the confidence that both drivers behaved the same, even in edge cases, and that we could switch.</p>
<p>Well, having now these stateful property tests acting as a regression test suite, what else can we use them for?
Creating stubs.</p>
<h2 id="integration-tests">Integration Tests</h2>
<p>Integration tests comprises a broad spectrum of test implementation.
On one side, you have the tests using real components, like a full MongoDB or Elasticsearch cluster.
Those suffer, from my experience, from flakiness, hard to reproduce differences on local dev and CI and are resource intensive and slow.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode go"><code class="sourceCode go"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">import</span> <span class="st">&quot;testing&quot;</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> TestUser<span class="op">(</span>t <span class="op">*</span>testing<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>  db <span class="op">:=</span> CreateMongoDatabase<span class="op">(</span>t<span class="op">)</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> CreateMongoDatabse<span class="op">(</span>t <span class="op">*</span>testing<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>  <span class="co">// ?</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>Also, those are harder to setup.
The test there needs a MongoDB database, but who starts the MongoDB instance?
Is it done through the test suite, with for example <a href="https://testcontainers.com/">testcontainers</a>?
This has advantages, like full isolation between tests, but makes starting tests much slower since they each need to start a container.
Another possibility is to start them outside of the test suite, often in a not well loved shell script with its own flaws.</p>
<p>On the other side, there are tests that use fake components - mocks - where the tester decides of the return value of those mocks during testing time.
These are fast but suffer from a lack of faithfulness as the bigger the mocked component, the harder it is to reproduce its real behavior.
This is even more true in a company with multiple engineering teams where one would mock a component made by another team.
Extensive documentation of behavior and edge cases is necessary for the mock to act like the real thing, but that’s a hard to reach goal that’s even harder to justify spending time on.</p>
<h2 id="where-mocks-fail">Where Mocks Fail</h2>
<p>I’d like to insist on the team aspect here and let’s take an example for that.
I’ll use the same example as in the previous blog post.
A Go service that encapsulates CRUD operations on a <code>User</code> struct representing a row in a MongoDB collection.
We’ll focus on just one method, <code>GetByNameOrEmail</code>, which, although seemingly simple, already has quite a few edge cases.</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode go"><code class="sourceCode go"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">package</span> userservice</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="kw">type</span> UserInterface <span class="kw">interface</span> <span class="op">{</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>  <span class="co">// GetByNameOrEmail retrieves a User from the database</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>  <span class="co">// by matching either on their name or email field.</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>  GetByNameOrEmail<span class="op">(</span>nameOrEmail <span class="dt">string</span><span class="op">)</span> <span class="op">(*</span>User<span class="op">,</span> <span class="dt">error</span><span class="op">)</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="co">// User struct represents a row or record in the</span></span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a><span class="co">// database.</span></span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a><span class="kw">type</span> User <span class="kw">struct</span><span class="op">{</span></span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a>  ID          bson<span class="op">.</span>ID</span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a>  Name        <span class="dt">string</span></span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a>  Email       <span class="dt">string</span></span>
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a>  Deleted     <span class="dt">bool</span></span>
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a>  DeletedTime time<span class="op">.</span>Time</span>
<span id="cb2-17"><a href="#cb2-17" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>Let’s imagine this interface is given to us by another team.
We don’t have access to the source code.
Well, we do, we’re the same company, but maybe the code is too complicated for us and we don’t understand it,
maybe it was created a long time ago and no one knows how it works anymore,
or maybe we actually can’t read it because it’s in a repo we don’t have access to and getting access will require a few days to process.
So for this contrived exercise, we can’t see the code.</p>
<p>Now we must program against this method and test our code.
We need to create a test for some code related to billing and we must retrieve a user at some point.
The point is, we’re not on the team that maintains this user service.
Let’s create a mock:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode go"><code class="sourceCode go"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="kw">type</span> MockService<span class="op">()</span> <span class="kw">struct</span> <span class="op">{</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>    GetByNameOrEmailNextCallReturn GetByNameOrEmailReturn</span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="kw">type</span> GetByNameOrEmailReturn <span class="kw">struct</span> <span class="op">{</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>    User <span class="op">*</span>User</span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>    Err  <span class="dt">error</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>ms MockService<span class="op">)</span> GetByNameOrEmail<span class="op">(</span>nameOrEmail <span class="dt">string</span><span class="op">)</span> <span class="op">(*</span>User<span class="op">,</span> <span class="dt">error</span><span class="op">)</span> <span class="op">{</span></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>    user<span class="op">,</span> err <span class="op">:=</span> ms<span class="op">.</span>GetByNameOrEmailNextCallReturn</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> user<span class="op">,</span> err</span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>Here you see how the mock works: at testing time, we instantiate the <code>MockService</code>
and then, before calling <code>GetByNameOrEmail</code>, we must ensure we set the <code>GetByNameOrEmailNextCallReturn</code> field to what we want that method to return.</p>
<p>But the follow-up question is what does this function returns?
In case of success, we can probably assume it returns a pointer to a filled out <code>User</code> struct and a <code>nil</code> error.
But we can’t be sure of it without reading the source code.
Also, what if the <code>User</code> is not deleted, is the <code>DeletedTime</code> field set to the zero value of <code>time.Time</code> or to something else?</p>
<p>In case of error, we can probably assume the function returns a nil pointer for <code>User</code> and non-<code>nil</code> error.
Or maybe it’s a pointer to a empty <code>User</code> struct?
Also, what will be the error?
Can we differentiate between a deleted and not deleted user?
Does the function return an error if the user is deleted?</p>
<p>So many questions that only the source code can tell us.
Actually, even by reading the other team’s source code, we can’t always tell what the error will look like.
If the code just returns the code from the MongoDB driver, we then need to go read the driver’s documentation.
That’s one good example of leaky abstraction.</p>
<p>Now, the worst part in my opinion is if you went the extra mile and actually managed to encapsulate the original function’s behavior in a mock.
And then, the behavior of the real service changes.
You will never now and your test will still pass!
You will probably learn about it during the next deploy.</p>
<h2 id="where-stubs-shine">Where Stubs Shine</h2>
<p>Enter stubs.
They are still fast to create as they are just a Go struct.
The difference with mocks is that actually do try to encapsulate the behavior of the real object they’re replacing.
They do this by managing an internal model that behaves the same way as the real deal.
A stub for the <code>User</code> service would look like so:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode go"><code class="sourceCode go"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="kw">type</span> StubService<span class="op">()</span> <span class="kw">struct</span> <span class="op">{</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>    Model <span class="kw">map</span><span class="op">[</span>bson<span class="op">.</span>ID<span class="op">]</span>User</span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> NewStubService<span class="op">()</span> StubService <span class="op">{</span></span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> StubService<span class="op">{</span></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>    Model<span class="op">:</span> <span class="bu">make</span><span class="op">(</span><span class="kw">map</span><span class="op">[</span>bson<span class="op">.</span>ID<span class="op">]</span>User<span class="op">),</span></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>sus StubService<span class="op">)</span> GetByNameOrEmail<span class="op">(</span>nameOrEmail <span class="dt">string</span><span class="op">)</span> <span class="op">(*</span>User<span class="op">,</span> <span class="dt">error</span><span class="op">)</span> <span class="op">{</span></span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Assume we have a getByNameOrEmail function that searches in the map.</span></span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a>  user<span class="op">,</span> ok <span class="op">:=</span> getByNameOrEmail<span class="op">(</span>sus<span class="op">.</span>model<span class="op">,</span> nameOrEmail<span class="op">)</span></span>
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> <span class="op">!</span>ok <span class="op">{</span></span>
<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> <span class="ot">nil</span><span class="op">,</span> errors<span class="op">.</span>New<span class="op">(</span><span class="st">&quot;No user found&quot;</span><span class="op">)</span></span>
<span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span> <span class="cf">else</span> <span class="cf">if</span> user<span class="op">.</span>Deleted <span class="op">{</span></span>
<span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> <span class="op">&amp;</span>user<span class="op">,</span> errors<span class="op">.</span>New<span class="op">(</span><span class="st">&quot;User is deleted&quot;</span><span class="op">)</span></span>
<span id="cb4-18"><a href="#cb4-18" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span> <span class="cf">else</span> <span class="op">{</span></span>
<span id="cb4-19"><a href="#cb4-19" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> <span class="op">&amp;</span>user<span class="op">,</span> <span class="ot">nil</span></span>
<span id="cb4-20"><a href="#cb4-20" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb4-21"><a href="#cb4-21" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>Now let’s be clear, the code inside that function does not follow best practices for error handling.
But maybe that’s what we got because it matches the behavior of the real service.
And that service was made that way because of historical reasons.
Changing the service’s behavior is tricky now and will definitely not be done by the team managing the billing system.</p>
<p>Anyway, the important part here is there is no mucking about figuring out what the method should return.
We instead insert the <code>User</code> we want in the model, then call the <code>GetByNameOrEmail</code> function.
The possibly weird behavior is encapsulated by the stub.</p>
<p>Of course, the issue now is how to create this stub?
This is where our earlier state property test acting as a regression test suite comes into play.
If the stub passes the same tests as the real service accessing MongoDB, then we can be quite sure it’s acting like the real one!</p>
<p>Ideally, this stub is maintained by the team maintaining the real MongoDB service.
The additional work to do this is, by my experience, much smaller than the work needed to setup the stateful property tests anyway.
But even if that’s not the case, as long as we have access to the tests, anyone can create a well behaved stub.</p>
<p>This is something we didn’t have with mocks: we now have tests to test the stub.
And then we can use the stub in our tests.
It seems a bit roundabout but this chain of tests has a very nice property: it’s a chain.
If one step in the chain changes, we are guaranteed to know if the rest of the chain breaks or not, before deploying to prod.
And if that’s the case, we can assess if we want to pursue that change now or later and communicate about it before breaking prod.</p>
<h2 id="conclusion">Conclusion</h2>
<p>So are stubs always superior to mocks?
No, but in my experience, the usefulness of mocks is limited.
If you work alone on your own codebase, there are probably no big differences between a stub and a mock.
But if you’re talking about multiple teams maintaining and evolving a codebase over multiple years, you want stubs first.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Using an External Git Repo for my Emacs Config with Nix</title>
    <link href="https://blog.tiserbox.com//posts/2024-07-03-use-external-git-repo-for-editor-config-with-nix.html" />
    <id>https://blog.tiserbox.com//posts/2024-07-03-use-external-git-repo-for-editor-config-with-nix.html</id>
    <published>2024-07-03T00:00:00Z</published>
    <updated>2024-07-03T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on July  3, 2024
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <!--toc:start-->
<ul>
<li><a href="#context">Context</a></li>
<li><a href="#flake-input">Flake Input</a></li>
<li><a href="#give-the-inputs-to-the-nixos-configuration">Give the Inputs to the NixOS Configuration</a>
<ul>
<li><a href="#nixos-with-nixos-rebuild">NixOS with nixos-rebuild</a></li>
<li><a href="#darwin">Darwin</a></li>
<li><a href="#nixos-with-colmena">NixOS with colmena</a></li>
</ul></li>
<li><a href="#common-config">Common Config</a></li>
<li><a href="#in-practice">In Practice</a>
<!--toc:end--></li>
</ul>
<h2 id="context">Context</h2>
<p>So I have <a href="https://github.com/ibizaman/emacs-conf">this repository</a> for my Emacs config.
How can I make NixOS aware of it and copy the files in the correct location?</p>
<h2 id="flake-input">Flake Input</h2>
<p>First step, putting the repo as a Nix flake input:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">inputs</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>    <span class="va">nixpkgs</span>.<span class="va">url</span> <span class="op">=</span> <span class="st">&quot;github:nixos/nixpkgs/nixos-unstable&quot;</span><span class="op">;</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>    <span class="va">emacs-conf</span>.<span class="va">url</span> <span class="op">=</span> <span class="st">&quot;github:ibizaman/emacs-conf&quot;</span><span class="op">;</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>    <span class="va">emacs-conf</span>.<span class="va">flake</span> <span class="op">=</span> <span class="cn">false</span><span class="op">;</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>    <span class="va">emacs-overlay</span>.<span class="va">url</span> <span class="op">=</span> <span class="st">&quot;https://github.com/nix-community/emacs-overlay/archive/master.tar.gz&quot;</span><span class="op">;</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>    <span class="va">emacs-overlay</span>.<span class="va">inputs</span>.<span class="va">nixpkgs</span>.<span class="va">follows</span> <span class="op">=</span> <span class="st">&quot;nixpkgs&quot;</span><span class="op">;</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>I’m using the <a href="https://github.com/nix-community/emacs-overlay">Emacs Overlay</a> because it has quite a lot of goodies, like native compilation of elisp files.</p>
<h2 id="give-the-inputs-to-the-nixos-configuration">Give the Inputs to the NixOS Configuration</h2>
<p>It’s not exactly obvious how to take these inputs and give them to the modules.
Here’s how to do it in some common situations.</p>
<h3 id="nixos-with-nixos-rebuild">NixOS with nixos-rebuild</h3>
<p>For a NixOS box:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">outputs</span> <span class="op">=</span> inputs@<span class="op">{</span> <span class="va">self</span><span class="op">,</span> <span class="va">nixpkgs</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>: <span class="op">{</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>    <span class="va">nixosConfigurations</span>.<span class="va">machine</span> <span class="op">=</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>      nixpkgs.lib.nixosSystem <span class="op">{</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>        <span class="va">system</span> <span class="op">=</span> <span class="st">&quot;x86_64-linux&quot;</span><span class="op">;</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>        <span class="va">specialArgs</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>          <span class="kw">inherit</span> <span class="op">(</span>inputs<span class="op">)</span> emacs-conf<span class="op">;</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>        <span class="op">};</span></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a>        <span class="va">modules</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a>          <span class="ss">./machine.nix</span></span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a>        <span class="op">];</span></span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a>      <span class="op">};</span></span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<h3 id="darwin">Darwin</h3>
<p>For a Darwin box, first you need a new input:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">inputs</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>    <span class="va">nix-darwin</span>.<span class="va">url</span> <span class="op">=</span> <span class="st">&quot;github:LnL7/nix-darwin/master&quot;</span><span class="op">;</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>    <span class="va">nix-darwin</span>.<span class="va">inputs</span>.<span class="va">nixpkgs</span>.<span class="va">follows</span> <span class="op">=</span> <span class="st">&quot;nixpkgs&quot;</span><span class="op">;</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>Then you can define it like so.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">outputs</span> <span class="op">=</span> inputs@<span class="op">{</span> <span class="va">self</span><span class="op">,</span> <span class="va">nixpkgs</span><span class="op">,</span> <span class="va">nix-darwin</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>: <span class="op">{</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>    <span class="va">darwinConfigurations</span>.<span class="va">machine</span> <span class="op">=</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>      nix<span class="op">-</span>darwin.lib.darwinSystem <span class="op">{</span></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>        <span class="va">system</span> <span class="op">=</span> <span class="st">&quot;x86_64-linux&quot;</span><span class="op">;</span></span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>        <span class="va">specialArgs</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>          <span class="kw">inherit</span> <span class="op">(</span>inputs<span class="op">)</span> emacs-conf<span class="op">;</span></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>        <span class="op">};</span></span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a>        <span class="va">modules</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a>          <span class="ss">./machine.nix</span></span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a>        <span class="op">];</span></span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a>      <span class="op">};</span></span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<h3 id="nixos-with-colmena">NixOS with colmena</h3>
<div class="sourceCode" id="cb5"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">outputs</span> <span class="op">=</span> inputs@<span class="op">{</span> <span class="va">self</span><span class="op">,</span> <span class="va">nixpkgs</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>: <span class="op">{</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>    <span class="va">colmena</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>      <span class="va">meta</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>        <span class="va">specialArgs</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>          <span class="kw">inherit</span> <span class="op">(</span>inputs<span class="op">)</span> emacs-conf<span class="op">;</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a>        <span class="op">};</span></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a>      <span class="op">};</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a>      <span class="va">machine</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a>        <span class="va">imports</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a>          <span class="ss">./machine.nix</span></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a>        <span class="op">];</span></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a>      <span class="op">};</span></span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<h2 id="common-config">Common Config</h2>
<p>The content of <code>machine.nix</code> is:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span> <span class="va">config</span><span class="op">,</span> <span class="va">pkgs</span><span class="op">,</span> <span class="va">lib</span><span class="op">,</span> <span class="va">inputs</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>:</span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span></span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">emacsWithPackages</span> <span class="op">=</span> pkgs.emacsWithPackagesFromUsePackage <span class="op">{</span></span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a>    <span class="va">config</span> <span class="op">=</span> inputs.emacs<span class="op">-</span>conf <span class="op">+</span> <span class="st">&quot;/init.el&quot;</span><span class="op">;</span></span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a>    <span class="va">defaultInitFile</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a>    <span class="va">package</span> <span class="op">=</span> pkgs.emacs<span class="op">-</span>unstable<span class="op">;</span></span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a><span class="kw">in</span></span>
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb6-12"><a href="#cb6-12" aria-hidden="true" tabindex="-1"></a>  <span class="va">config</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb6-13"><a href="#cb6-13" aria-hidden="true" tabindex="-1"></a>    <span class="va">environment</span>.<span class="va">systemPackages</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb6-14"><a href="#cb6-14" aria-hidden="true" tabindex="-1"></a>      emacsWithPackages</span>
<span id="cb6-15"><a href="#cb6-15" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb6-16"><a href="#cb6-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-17"><a href="#cb6-17" aria-hidden="true" tabindex="-1"></a>    <span class="va">services</span>.<span class="va">emacs</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb6-18"><a href="#cb6-18" aria-hidden="true" tabindex="-1"></a>      <span class="va">enable</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb6-19"><a href="#cb6-19" aria-hidden="true" tabindex="-1"></a>      <span class="va">package</span> <span class="op">=</span> emacsWithPackages<span class="op">;</span></span>
<span id="cb6-20"><a href="#cb6-20" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb6-21"><a href="#cb6-21" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb6-22"><a href="#cb6-22" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<h2 id="in-practice">In Practice</h2>
<p>To update my Emacs config on my NixOS machine, I:</p>
<ul>
<li>Push a new commit to the Emacs repo.</li>
<li>Run <code>nix flake lock --update-input emacs-conf</code></li>
<li>Run <code>nixos-rebuild switch</code></li>
</ul>
<p>Assuming I cloned my Emacs repo locally at <code>~/Projects/emacs-conf</code>,
I can also test a change by modifying the <code>init.el</code> file
then updating my config with the following command:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="ex">nix</span> flake lock <span class="dt">\</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>  <span class="at">--update-input</span> emacs-conf <span class="dt">\</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>  <span class="at">--override-input</span> emacs-conf ~/Projects/emacs-conf</span></code></pre></div>
<p>For Emacs configs specifically, I actually also test my config just by loading the code I modified.
Emacs being a Lisp machine, updating a function definition is easy.</p>
<p>In an elisp file, I can <code>C-c C-c</code> on a Lisp expression to evaluate it and load it with the following snippet:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode lisp"><code class="sourceCode commonlisp"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a>(<span class="kw">defun</span><span class="fu"> eval-point-region-and-deactivate </span>()</span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;Evaluate region or expanded region and deactivates region when done.&quot;</span></span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a>  (interactive)</span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a>  (use-region-or-expand-region)</span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a>  (condition-case-unless-debug err</span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a>      (message <span class="st">&quot;%s&quot;</span> (call-interactively <span class="dt">&#39;eval-region</span>))</span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a>    (<span class="kw">error</span> (deactivate-mark)</span>
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a>           (<span class="kw">signal</span> (<span class="kw">car</span> err) (<span class="kw">cdr</span> err))))</span>
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a>  (deactivate-mark))</span>
<span id="cb8-10"><a href="#cb8-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-11"><a href="#cb8-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-12"><a href="#cb8-12" aria-hidden="true" tabindex="-1"></a>(<span class="kw">use-package</span> elisp-mode</span>
<span id="cb8-13"><a href="#cb8-13" aria-hidden="true" tabindex="-1"></a>  :config</span>
<span id="cb8-14"><a href="#cb8-14" aria-hidden="true" tabindex="-1"></a>  (define-key lisp-mode-map (kbd <span class="st">&quot;C-c C-c&quot;</span>)</span>
<span id="cb8-15"><a href="#cb8-15" aria-hidden="true" tabindex="-1"></a>              <span class="op">#&#39;</span>eval-point-region-and-deactivate))</span></code></pre></div>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Recover NixOS System</title>
    <link href="https://blog.tiserbox.com//posts/2024-06-13-recover-nix-os-system.html" />
    <id>https://blog.tiserbox.com//posts/2024-06-13-recover-nix-os-system.html</id>
    <published>2024-06-13T00:00:00Z</published>
    <updated>2024-06-13T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on June 13, 2024
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <!--toc:start-->
<ul>
<li><a href="#context">Context</a>
<ul>
<li><a href="#potential-harmful-change">Potential Harmful Change</a></li>
<li><a href="#impossible-to-recover">Impossible to Recover</a></li>
<li><a href="#did-not-test">Did not Test</a></li>
<li><a href="#bonus-stress-inducing-factor">Bonus Stress Inducing Factor</a></li>
</ul></li>
<li><a href="#recovery-overview">Recovery Overview</a>
<ul>
<li><a href="#nixos-recovery-usb-stick">NixOS Recovery USB stick</a></li>
<li><a href="#nixos-enter">nixos-enter</a></li>
<li><a href="#flake">flake</a></li>
<li><a href="#update-configuration">Update Configuration</a></li>
<li><a href="#nixos-rebuild">nixos-rebuild</a></li>
<li><a href="#unmount-cleanly">Unmount Cleanly</a></li>
</ul></li>
<li><a href="#takeaway">Takeaway</a>
<!--toc:end--></li>
</ul>
<h1 id="context">Context</h1>
<p>I managed to brick my NixOS server. The recipe to achieve that is quite simple, just throw all best
practices out the window by doing the following three things at the same time:</p>
<ol type="1">
<li>Make a potentially harmful change.</li>
<li>Make it impossible to recover.</li>
<li>Do not test.</li>
</ol>
<p>Pretty easy, right?</p>
<h2 id="potential-harmful-change">Potential Harmful Change</h2>
<p>My server has its root partition on an encrypted ZFS partition. This means I need to enter the
passphrase for my server to boot. To be able to ssh into the server and enter the passphrase
remotely, I had the <a href="https://search.nixos.org/options?query=boot.initrd">boot.initrd</a> options setup
correctly.</p>
<p>But I wasn’t happy with my setup. See, when I sshed in, the prompt to type the passphrase was shown,
I could then enter the passphrase and press Enter to mount the root partition, but I then had the
prompt still open and I needed to exit the prompt manually. This extra exit step was not to my liking. So I changed:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="bu">echo</span> <span class="st">&quot;zfs load-key zroot; killall zfs&quot;</span> <span class="dt">\</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="op">&gt;&gt;</span> /root/.profile</span></code></pre></div>
<p>to this:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="bu">echo</span> <span class="st">&quot;zfs load-key zroot; killall zfs&quot;</span> <span class="dt">\</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="op">&gt;&gt;</span> /root/.profile<span class="kw">;</span> <span class="bu">exit</span></span></code></pre></div>
<p>instead of this:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="bu">echo</span> <span class="st">&quot;zfs load-key zroot; killall zfs; exit&quot;</span> <span class="dt">\</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  <span class="op">&gt;&gt;</span> /root/.profile</span></code></pre></div>
<p>See the mistake? The exit was not in the correct location! So instead of showing me the prompt to
unencrypt the partition, the startup would fail and I couldn’t do anything about it.</p>
<h2 id="impossible-to-recover">Impossible to Recover</h2>
<p>I had been playing around with tweaking the boot initrd options for some time. Each time, it creates
a new voluminous file that lines inside <code>/boot</code>. I gave less than 1Gb disk space for that partition
so it fills up quickly during my tweaks.</p>
<p>I even made <a href="https://blog.tiserbox.com/posts/2024-04-15-how-to-fix-boot-volume-running-out-of-disk-space-in-nix-os.html">another blog post</a> about how to recover from a filled up <code>/boot</code> partition already.</p>
<p>Anyway, I did what I wrote in that blog post and ran a command to remove old generations:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> nix profile wipe-history <span class="dt">\</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>    <span class="at">--profile</span> /nix/var/nix/profiles/system <span class="dt">\</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>    <span class="at">--older-than</span> 14d</span></code></pre></div>
<p>But since that was not enough, I removed the <code>older-than</code> argument. This removed all previous
generations, as I wanted, but not as I should have done.</p>
<h2 id="did-not-test">Did not Test</h2>
<p><code>nixos-rebuild build-vm</code> is <a href="https://wiki.nixos.org/wiki/NixOS:nixos-rebuild_build-vm">a thing</a> and I should learn to use this tremendously useful
feature. I already use NixOS VM tests quite extensively in my <a href="https://github.com/ibizaman/selfhostblocks">Self Host Blocks</a> project so I
have no excuse.</p>
<h2 id="bonus-stress-inducing-factor">Bonus Stress Inducing Factor</h2>
<p>Of course, I did this 24 hours before leaving on a trip. Why not?</p>
<h1 id="recovery-overview">Recovery Overview</h1>
<p>So, how do you recover from this? The quick overview that <code>@k900</code> from Matrix gave me is:</p>
<ol type="1">
<li>Boot on a NixOS Recovery USB stick</li>
<li>nixos-enter</li>
<li>change configuration</li>
<li>nixos-rebuild</li>
</ol>
<p>I knew <code>nixos-enter</code> was a thing from the NixOS installation procedure but never would’ve thought
about using it to recover a system!</p>
<p>That being said, because I was using flakes, ZFS for the partitions and colmena to deploy, each step
had unforeseen complications. This blog posts goes over those and how I proceeded for each step.</p>
<h2 id="nixos-recovery-usb-stick">NixOS Recovery USB stick</h2>
<p>Also called live CD. But how to make such a thing? <a href="https://wiki.nixos.org/wiki/Creating_a_NixOS_live_CD">The wiki</a> explains how to do this quite
well. And it’s so easy, I love nix.</p>
<p>Here’s exactly how <em>I</em> created the live CD:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>nixosConfigurations.recovery<span class="op">-</span>iso = <span class="kw">let</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">inherit</span> (<span class="va">inputs</span>) <span class="va">nixpkgs</span>;</span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">system</span> <span class="op">=</span> <span class="st">&quot;x86_64-linux&quot;</span><span class="op">;</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="kw">in</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>  <span class="op">(</span>nixpkgs.lib.nixosSystem <span class="op">{</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>    <span class="kw">inherit</span> system<span class="op">;</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a>    <span class="va">modules</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a>      <span class="st">&quot;</span><span class="sc">${</span>nixpkgs<span class="sc">}</span><span class="st">/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix&quot;</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a>      <span class="st">&quot;</span><span class="sc">${</span>nixpkgs<span class="sc">}</span><span class="st">/nixos/modules/installer/cd-dvd/channel.nix&quot;</span></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a>      <span class="op">({</span> <span class="va">config</span><span class="op">,</span> <span class="va">pkgs</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>: <span class="op">{</span></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a>        <span class="va">boot</span>.<span class="va">kernelPackages</span> <span class="op">=</span> config.boot.zfs.package.latestCompatibleLinuxPackages<span class="op">;</span></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a>        <span class="va">environment</span>.<span class="va">systemPackages</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a>          pkgs.colmena</span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a>        <span class="op">];</span></span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a>      <span class="op">})</span></span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a>  <span class="op">})</span>.config.system.build.isoImage;</span></code></pre></div>
<p>Since I wanted to work on ZFS partitions, I changed the kernel for a ZFS compatible one.</p>
<p>I also added the <code>colmena</code> package. You could avoid doing that as long as the machine you want to
recover has internet by running <code>nix shell nixpkgs#colmena</code> when booted in the recovery environment.</p>
<p>To build the CD:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="ex">nix</span> build .#nixosConfigurations.recovery-iso</span></code></pre></div>
<p>And finally to copy it on a USB stick on <code>/dev/sdb</code>:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> dd <span class="dt">\</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>  bs=4M <span class="dt">\</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>  status=progress <span class="dt">\</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a>  conv=fdatasync <span class="dt">\</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a>  if=./result/iso/nixos-24.05.20240421.6143fc5-x86_64-linux.iso <span class="dt">\</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a>  of=/dev/sdb</span></code></pre></div>
<h2 id="nixos-enter">nixos-enter</h2>
<p>Again, the <a href="https://wiki.nixos.org/wiki/Change_root">wiki</a> explains well what to do in the general case.</p>
<p>For this step, I needed to mount the ZFS partition called <code>zroot</code> and unencrypt it:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> zpool import zroot <span class="at">-f</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> zfs load-key zroot</span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;enter passphrase&gt;</span></span></code></pre></div>
<p>I could then mount the 3 required filesystems. That being said, because I did let ZFS mount them by
setting the <code>mountpoint</code> option, I couldn’t just use <code>zfs mount</code> to mount them under the <code>/mnt</code>
directory. I needed instead
<a href="https://github.com/openzfs/zfs/issues/4553#issuecomment-632068563">this</a>:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> mount <span class="at">-t</span> zfs <span class="at">-o</span> zfsutil zpool/local/root /mnt</span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> mount <span class="at">-t</span> zfs <span class="at">-o</span> zfsutil zpool/local/nix /mnt/nix</span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> mount <span class="at">-t</span> zfs <span class="at">-o</span> zfsutil zpool/safe/home /mnt/home</span></code></pre></div>
<p>Now, I could run <code>nixos-enter</code> and get chrooted under <code>/mnt</code>.</p>
<h2 id="flake">flake</h2>
<p>At this point, I should have been able to change <code>configuration.nix</code> but there is no such file when
using flakes! So instead, I needed a copy of my repository used to deploy this machine.</p>
<p>I could have copied it from my laptop but instead I went back out of the chrooted environment and
copied it over from an external hard drive used to store the repositories:</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> zpool import data <span class="at">-f</span></span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="ex">zfs</span> get keylocation data</span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a><span class="fu">cp</span> /mnt/persist/data_passphrase /persist</span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> zfs load-key data</span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> mount <span class="at">-t</span> zfs <span class="at">-o</span> zfsutil data/nextcloud /mnt/srv/nextcloud</span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a><span class="co"># Repo is now under /mnt/srv/nextcloud/.../nix-config</span></span>
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a><span class="fu">cp</span> <span class="at">-r</span> /mnt/srv/nextcloud/.../nix-config /mnt/root</span></code></pre></div>
<p>The instructions above have additional steps compared to the root partition because those external
hard drives use a different passphrase that’s only located on the ZFS root system. So I needed to
copy over the key to the location expected by ZFS.</p>
<h2 id="update-configuration">Update Configuration</h2>
<p>Now, I could finally make the change to the repository!</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="ex">nixos-enter</span></span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a><span class="bu">cd</span> /root/nix-config</span></code></pre></div>
<p>then change the incriminated line to:</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="bu">echo</span> <span class="st">&quot;zfs load-key zroot; killall zfs; exit&quot;</span> <span class="dt">\</span></span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a>  <span class="op">&gt;&gt;</span> /root/.profile</span></code></pre></div>
<h2 id="nixos-rebuild">nixos-rebuild</h2>
<p>Since I was not using <code>nixos-rebuild</code> to deploy in the first place, I could not run <code>nixos-rebuild</code>
to deploy locally either because there is no <code>nixosConfigurations</code> flake output. When using
<code>colmena</code> there is instead a <code>colmena</code> output. This meant I needed to:</p>
<ol type="1">
<li><p>Allow colmena to deploy locally this machine by enabling the
<a href="https://colmena.cli.rs/unstable/features/apply-local.html">option</a>:</p>
<div class="sourceCode" id="cb13"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a>deployment.allowLocalDeployment = <span class="cn">true</span>;</span></code></pre></div>
<p>I had not enabled that option yet for this machine since I never needed to deploy this
configuration locally.</p></li>
<li><p>Run <code>colmena apply-local --node &lt;mymachine&gt; boot</code>.</p></li>
</ol>
<p>But that failed because I did not mount the boot partition!</p>
<p>So, after getting out of the chroot once more, I mounted the correct boot partition:</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="fu">cat</span> /mnt/etc/fstab <span class="kw">|</span> <span class="fu">grep</span> boot</span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> mount /dev/disk/by-partlabel/disk-x-ESP /mnt/boot</span></code></pre></div>
<p>Then I re-entered the chroot with <code>nixos-enter</code> and ran the following command which finally
succeeded.</p>
<div class="sourceCode" id="cb15"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="ex">colmena</span> apply-local <span class="at">--node</span> <span class="op">&lt;</span>mymachine<span class="op">&gt;</span> boot</span></code></pre></div>
<h2 id="unmount-cleanly">Unmount Cleanly</h2>
<p>But wait! If you reboot now, the system will not be able to mount the ZFS partitions. You first need
to export the zpools:</p>
<div class="sourceCode" id="cb16"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> zpool export zroot</span>
<span id="cb16-2"><a href="#cb16-2" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> zpool export data</span></code></pre></div>
<p>If you forget this last step, just reboot on the USB drive and re-import the pools then export them.</p>
<p>Of course I forgot to do this on my first try.</p>
<h1 id="takeaway">Takeaway</h1>
<p>What was the takeaway here? It was never wracking and exhausting. Never again.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Bypass Cached Failure</title>
    <link href="https://blog.tiserbox.com//posts/2024-05-24-bypass-cached-failure.html" />
    <id>https://blog.tiserbox.com//posts/2024-05-24-bypass-cached-failure.html</id>
    <published>2024-05-24T00:00:00Z</published>
    <updated>2024-05-24T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on May 24, 2024
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <p>When evaluating a nix expression, sometimes you get a failure. But then, on next evaluation, you get
this error message:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix build .#checks.x86_64-linux.modules</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="ex">error:</span> cached failure of attribute <span class="st">&#39;checks.x86_64-linux.modules&#39;</span></span></code></pre></div>
<p>The failure got cached! How do you see the error message again? By adding <code>--option eval-cache false</code>:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix build .#checks.x86_64-linux.modules <span class="at">--option</span> eval-cache false</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="ex">...</span></span></code></pre></div>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>How to fix /boot volume running out of disk space in NixOS</title>
    <link href="https://blog.tiserbox.com//posts/2024-04-15-how-to-fix-boot-volume-running-out-of-disk-space-in-nix-os.html" />
    <id>https://blog.tiserbox.com//posts/2024-04-15-how-to-fix-boot-volume-running-out-of-disk-space-in-nix-os.html</id>
    <published>2024-04-15T00:00:00Z</published>
    <updated>2024-04-15T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on April 15, 2024
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <p>After a while without removing old generations, I got into a pickle.</p>
<h1 id="situation">Situation</h1>
<p>After running <code>colmena apply</code> to deploy to my server for the Nth time, I got this error:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="ex">Activation</span> failed: Child process exited with error code: 1</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="ex">[ERROR]</span>   stderr<span class="er">)</span> <span class="ex">OSError:</span> [Errno 28] No space left on</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>  <span class="ex">device:</span> <span class="st">&#39;/nix/store/cwimj1bg0dgfvngdmbrapkp5ifl3bfgy-initrd-linux-6.6.22/initrd&#39;</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>  <span class="ex">-</span><span class="op">&gt;</span> <span class="st">&#39;/boot/EFI/nixos/cwimj1bg0dgfvngdmbrapkp5ifl3bfgy-initrd-linux-6.6.22-initrd.efi&#39;</span></span></code></pre></div>
<p>Wait, no space left on the <code>/boot</code> volume? Uh oh. Taking a look, indeed it’s full:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> df <span class="at">-h</span> /boot</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="ex">Filesystem</span>      Size  Used Avail Use% Mounted on</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="ex">/dev/nvme0n1p1</span>  128M  128M  2.0K 100% /boot</span></code></pre></div>
<h1 id="fix">Fix</h1>
<p>Now, the trick is two-fold. First, we must delete old generations with old versions of the kernel.
As you can see, I didn’t properly clean this up.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix profile history <span class="dt">\</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>    <span class="at">--profile</span> /nix/var/nix/profiles/system</span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Version</span> 1 <span class="er">(</span><span class="ex">2024-03-31</span><span class="kw">)</span><span class="bu">:</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>  <span class="ex">No</span> changes.</span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="ex">Version</span> 2 <span class="er">(</span><span class="ex">2024-03-31</span><span class="kw">)</span> <span class="op">&lt;</span>- <span class="ex">1:</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>  <span class="ex">No</span> changes.</span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a><span class="ex">Version</span> 3 <span class="er">(</span><span class="ex">2024-03-31</span><span class="kw">)</span> <span class="op">&lt;</span>- <span class="ex">2:</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>  <span class="ex">No</span> changes.</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a><span class="ex">...</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a><span class="ex">Version</span> 111 <span class="er">(</span><span class="ex">2024-04-11</span><span class="kw">)</span> <span class="op">&lt;</span>- <span class="ex">110:</span></span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>  <span class="ex">No</span> changes.</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a><span class="ex">Version</span> 112 <span class="er">(</span><span class="ex">2024-04-15</span><span class="kw">)</span> <span class="op">&lt;</span>- <span class="ex">111:</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a>  <span class="ex">No</span> changes.</span></code></pre></div>
<p>To remove old generations and allow the garbage collection to kick in, run:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo nix profile wipe-history <span class="dt">\</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>    <span class="at">--profile</span> /nix/var/nix/profiles/system <span class="dt">\</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>    <span class="at">--older-than</span> 14d</span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> profile version 12</span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> profile version 11</span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> profile version 10</span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> profile version 9</span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> profile version 8</span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> profile version 7</span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> profile version 6</span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> profile version 5</span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> profile version 4</span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> profile version 3</span>
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> profile version 2</span>
<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a><span class="ex">removing</span> profile version 1</span></code></pre></div>
<p>Followed by the garbage collection:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix store gc</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="ex">1855</span> store paths deleted, 1851.81 MiB freed</span></code></pre></div>
<p>But that wasn’t enough to get rid of the extraneous kernels.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo ls <span class="at">-l</span> /boot/EFI/nixos</span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="ex">total</span> 130308</span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="ex">-rwx------</span> 1 root root 10187264 Apr 15 06:15 0d3nlv97dyyflgq7irn5wy91x2mlszl3-linux-6.6.22-bzImage.efi</span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a><span class="ex">-rwx------</span> 1 root root 27904393 Apr 15 06:15 72p297y5a780hd6r5jwz0zqv4am97vpj-initrd-linux-6.6.22-initrd.efi</span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a><span class="ex">-rwx------</span> 1 root root 19864710 Apr 15 06:15 83i5i813jhsahlf7wmlbn96bngnjinf1-initrd-linux-6.6.22-initrd.efi</span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a><span class="ex">-rwx------</span> 1 root root 28606443 Apr 15 06:15 8bv13658nk7s12qljx0lch4g2bhrkvgr-initrd-linux-6.6.22-initrd.efi</span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a><span class="ex">-rwx------</span> 1 root root  8232960 Apr 15 06:15 cwimj1bg0dgfvngdmbrapkp5ifl3bfgy-initrd-linux-6.6.22-initrd.efi</span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a><span class="ex">-rwx------</span> 1 root root 27909504 Apr 15 06:15 ignpfyssa0ma4xp4asirfaq18f50vz8k-initrd-linux-6.6.22-initrd.efi</span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a><span class="ex">-rwx------</span> 1 root root 10723840 Apr 15 06:15 zqp81gm823adj6d6rk4k04gllhvwz847-linux-6.6.22-bzImage.ef</span></code></pre></div>
<p>For that, we actually need to redeploy! So after one more <code>colmena apply</code>, which was successful this
time, I got rid of the old kernels:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo ls <span class="at">-l</span> /boot/EFI/nixos</span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a><span class="ex">total</span> 66414</span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a><span class="ex">-rwx------</span> 1 root root 10187264 Apr 15 06:35 0d3nlv97dyyflgq7irn5wy91x2mlszl3-linux-6.6.22-bzImage.efi</span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a><span class="ex">-rwx------</span> 1 root root 28577118 Apr 15 06:35 8bv13658nk7s12qljx0lch4g2bhrkvgr-initrd-linux-6.6.22-initrd.efi</span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a><span class="ex">-rwx------</span> 1 root root 29239767 Apr 15 06:35 cwimj1bg0dgfvngdmbrapkp5ifl3bfgy-initrd-linux-6.6.22-initrd.efi</span></code></pre></div>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Jellyfin Streaming Media Center with LibreElec and Orange PI 3</title>
    <link href="https://blog.tiserbox.com//posts/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3.html" />
    <id>https://blog.tiserbox.com//posts/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3.html</id>
    <published>2024-03-22T00:00:00Z</published>
    <updated>2024-03-22T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on March 22, 2024
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;jellyfin&#39;." href="/tags/jellyfin.html">jellyfin</a>, <a title="All pages tagged &#39;iot&#39;." href="/tags/iot.html">iot</a>
	  
	</section>
    <section>
        <p><a href="https://libreelec.tv/">Libreelec</a> is a Linux distribution optimized to run <a href="https://kodi.tv/">Kodi</a>. I installed it on an
<a href="https://www.amazon.com/gp/product/B09TQZH4GJ/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&amp;psc=1">Orange Pi 3 LTS</a> which is connected to my TV. I then installed the Jellyfin
plugin and can play all my media from Jellyfin on the TV.</p>
<p>To boost performance, I installed the distribution on the internal flash eMMC storage instead of
keeping it on the SD card.</p>
<p>This blog post covers everything I did to set this up.</p>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/amazon.png" title="Amazon screenshot" alt="Orange Pi 3 LTS Allwinner H6 2GB LPDDR3 8GB EMMC Flash Quad Core" />
<figcaption aria-hidden="true">Orange Pi 3 LTS Allwinner H6 2GB LPDDR3 8GB EMMC Flash Quad Core</figcaption>
</figure>
<h2 id="install-libreelec-on-an-sd-card">Install Libreelec on an SD card</h2>
<p>Go to <a href="https://libreelec.tv/">libreelec.tv</a> &gt; Downloads &gt; Manual Downloads section &gt; Allwinner &gt; Allwinner H6 &gt; Orange Pi 3 LTS</p>
<p>Of course, that’s if you have an Orange Pi 3 LTS. Otherwise, pick the <a href="https://libreelec.tv/downloads/">correct
download</a>. To burn on SD card, we will follow the <a href="https://wiki.archlinux.org/title/USB_flash_installation_medium#Using_basic_command_line_utilities">Archlinux
wiki</a>.</p>
<ol type="1">
<li><p>Put the SD card in your computer.</p></li>
<li><p>Use <code>lsblk</code> to identify the path to your SD card.</p>
<p>In the following example, the path is <code>/dev/sda</code>.</p></li>
</ol>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>$ lsblk</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>NAME        <span class="va">MAJ</span><span class="op">:</span>MIN RM   SIZE RO TYPE MOUNTPOINTS</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>sda           <span class="dv">8</span>:<span class="dv">0</span>    <span class="dv">1</span>  <span class="fl">29.1</span>G  <span class="dv">0</span> disk </span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>└─sda1        <span class="dv">8</span>:<span class="dv">1</span>    <span class="dv">1</span>  <span class="fl">29.1</span>G  <span class="dv">0</span> part</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>nvme0n1     <span class="dv">259</span>:<span class="dv">0</span>    <span class="dv">0</span> <span class="fl">931.5</span>G  <span class="dv">0</span> disk </span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>├─nvme0n1p1 <span class="dv">259</span>:<span class="dv">1</span>    <span class="dv">0</span> <span class="fl">923.6</span>G  <span class="dv">0</span> part <span class="ss">/nix/store</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>│                                     <span class="op">/</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>├─nvme0n1p2 <span class="dv">259</span>:<span class="dv">2</span>    <span class="dv">0</span>   <span class="fl">7.5</span>G  <span class="dv">0</span> part <span class="op">[</span>SWAP<span class="op">]</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>└─nvme0n1p3 <span class="dv">259</span>:<span class="dv">3</span>    <span class="dv">0</span>   487M  <span class="dv">0</span> part <span class="ss">/boot</span></span></code></pre></div>
<ol start="3" type="1">
<li><p>Burn Libreelec on the SD card.</p>
<p>Watch out, this will erase anything on the target path. In the following snippet, replace
<code>/dev/sdX</code> with the path you got from the <code>lsblk</code> output.</p></li>
</ol>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> gunzip <span class="at">--to-stdout</span> <span class="dt">\</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>    LibreELEC-H6.arm-11.0.3-orangepi-3-lts.img.gz <span class="dt">\</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  <span class="kw">|</span> <span class="fu">sudo</span> dd bs=4M conv=fsync oflag=direct <span class="dt">\</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>            status=progress of=/dev/sdX</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="ex">30+1</span> records in</span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a><span class="ex">30+1</span> records out</span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a><span class="ex">128064485</span> bytes <span class="er">(</span><span class="ex">128</span> MB, 122 MiB<span class="kw">)</span> <span class="ex">copied,</span> 10.7377 s, 11.9 MB/s</span></code></pre></div>
<ol start="4" type="1">
<li><p>Verify the partition layout looks like the following.</p>
<p>Don’t worry if the second partition does not cover the whole of the SD card, Libreelec will
resize the partition on first boot.</p></li>
</ol>
<div class="sourceCode" id="cb3"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo fdisk <span class="at">-l</span> /dev/sda</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="ex">Disk</span> /dev/sda: 29.13 GiB, 31281119232 bytes, 61095936 sectors</span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Disk</span> model: USB DISK        </span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="ex">Units:</span> sectors of 1 <span class="pp">*</span> 512 = 512 bytes</span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="ex">Sector</span> size <span class="er">(</span><span class="ex">logical/physical</span><span class="kw">)</span><span class="bu">:</span> 512 bytes / 512 bytes</span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="ex">I/O</span> size <span class="er">(</span><span class="ex">minimum/optimal</span><span class="kw">)</span><span class="bu">:</span> 512 bytes / 512 bytes</span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="ex">Disklabel</span> type: dos</span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="ex">Disk</span> identifier: 0x9a55c0c3</span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a><span class="ex">Device</span>     Boot   Start     End Sectors  Size Id Type</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a><span class="ex">/dev/sda1</span>  <span class="pp">*</span>       8192 1056767 1048576  512M  c W95 FAT32 <span class="er">(</span><span class="ex">LBA</span><span class="kw">)</span></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a><span class="ex">/dev/sda2</span>       1056768 1122303   65536   32M 83 Linux</span></code></pre></div>
<h2 id="first-boot">First boot</h2>
<p>Put the SD card in the Orange PI - or any other <a href="https://libreelec.tv/downloads/">compatible device</a> you have.</p>
<p>Connect the HDMI to your TV and connect a keyboard. The SD card will only be needed for the first
boot, the keyboard for the first and second. You could do without a keyboard by using a remote
control but that will be probably hard to type with.</p>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/libreelec_resize.jpg" title="Picture of screen with LibreElec having resized the SD card and showing a countdown before rebooting." class="zoom" alt="Upon booting LibreElec for the first time, it will resize the second partition then reboot once." />
<figcaption aria-hidden="true">Upon booting LibreElec for the first time, it will resize the second partition then reboot once.</figcaption>
</figure>
<p>You will then boot into the Jellyfin UI and the installation wizard will popup automatically.</p>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/libreelec_wizard_language.jpg" class="zoom" alt="Wizard showing language selection. Pick a language." />
<figcaption aria-hidden="true">Wizard showing language selection. Pick a language.</figcaption>
</figure>
<p>Next page will allow you to choose a hostname. I chose “tvlivingroom”. Do not use special characters
nor whitespaces as those are not accepted. The wizard is poorly done here because if you enter a not
accepted character, no error message will be shown but you will be shown again the hostname
selection screen.</p>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/libreelec_wizard_network.jpg" class="zoom" alt="Wizard showing network selection. Pick an Ethernet or WiFi connection." />
<figcaption aria-hidden="true">Wizard showing network selection. Pick an Ethernet or WiFi connection.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/libreelec_wizard_ssh.jpg" class="zoom" alt="Wizard showing ssh server toggle. Enable SSH server." />
<figcaption aria-hidden="true">Wizard showing ssh server toggle. Enable SSH server.</figcaption>
</figure>
<p>You will need to wait about 10 seconds for the popup to appear to let you change the password. Later
on, we will configure only connect with a SSH key and not password.</p>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/libreelec_wizard_ssh_password.jpg" class="zoom" alt="Wizard showing ssh server password. Choose a password." />
<figcaption aria-hidden="true">Wizard showing ssh server password. Choose a password.</figcaption>
</figure>
<p>That should be then end of the wizard.</p>
<p>Now, we need to connect through SSH but to do that, we need to find the IP of this device. To do that, go to the settings page by clicking on the gear.</p>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/libreelec_ip_gear.jpg" class="zoom" alt="Gear button to go to the settings page." />
<figcaption aria-hidden="true">Gear button to go to the settings page.</figcaption>
</figure>
<p>Then on the settings page, go to the System Information page.</p>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/libreelec_ip_menu.jpg" class="zoom" alt="Button to go to the System Information page." />
<figcaption aria-hidden="true">Button to go to the System Information page.</figcaption>
</figure>
<p>Finally, you will see the IP address of the device. In the following examples, we’ll take <code>192.168.1.10</code>.</p>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/libreelec_ip_show.jpg" class="zoom" alt="System Information page showing the IP address of the device." />
<figcaption aria-hidden="true">System Information page showing the IP address of the device.</figcaption>
</figure>
<p>Now, connect through SSH using that IP. If you kept the default password above, use <code>libreelec</code> as
the password. Otherwise, use the one you entered.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ssh root@192.168.1.10 <span class="at">-o</span> IdentitiesOnly=yes</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="ex">root@192.168.1.10</span><span class="st">&#39;s password: </span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="st">##############################################</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="st">#                 LibreELEC                  #</span></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="st">#            https://libreelec.tv            #</span></span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a><span class="st">##############################################</span></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a><span class="st">LibreELEC (official): 11.0.4 (H6.arm)</span></span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a><span class="st">tvlivingroom:~ # </span></span></code></pre></div>
<p>We will install Libreelec on the eMMC memory. You can skip this step but I recommend going through
it as it drastically improves the performance of the UI.</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="ex">tvlivingroom:~</span> <span class="co"># install2emmc</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="ex">===============================</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="ex">Installing</span> LibreELEC to eMMC</span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a><span class="ex">===============================</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a><span class="ex">eMMC</span> found at /dev/mmcblk1</span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a><span class="ex">WARNING:</span> ALL DATA ON eMMC WILL BE ERASED! Continue <span class="er">(</span><span class="ex">y/N</span><span class="kw">)</span><span class="ex">?</span>  y</span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a><span class="ex">Erasing</span> eMMC ...</span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> partitions</span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> filesystems</span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a><span class="ex">Installing</span> bootloader</span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a><span class="ex">Copying</span> system files</span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a><span class="ex">Adjusting</span> partition UUIDs</span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a><span class="ex">Done</span></span></code></pre></div>
<p>Now, shutdown the device.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="ex">tvlivingroom:~</span> <span class="co"># shutdown -hP now</span></span></code></pre></div>
<h2 id="second-boot">Second boot</h2>
<p>Remove SD card then start the device by pressing the button a couple seconds.</p>
<p>Now, the not fun part is you will need to redo the whole wizard setup. Go back to the previous
section if you need a refresher but skip the part where you install to the eMMC memory. Continue
from here when that’s done.</p>
<h2 id="ssh-access">SSH access</h2>
<p>Okay, on your laptop, generate an ssh key. I use an SSH agent to remember the passphrase of my SSH
keys so I do use a passphrase. If you don’t want to use a passphrase, just leave it empty when you
get prompted.</p>
<ol type="1">
<li>Generate a random passphrase</li>
</ol>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> openssl rand <span class="at">-hex</span> 32</span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a><span class="ex">XYZ...</span></span></code></pre></div>
<ol start="2" type="1">
<li>Generate an SSH key.</li>
</ol>
<div class="sourceCode" id="cb8"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ssh-keygen <span class="at">-t</span> ed25519 <span class="at">-f</span> ~/.ssh/tvlivingroom</span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a><span class="ex">Generating</span> public/private ed25519 key pair.</span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Enter</span> passphrase <span class="er">(</span><span class="ex">empty</span> for no passphrase<span class="kw">)</span><span class="bu">:</span> </span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a><span class="ex">Enter</span> same passphrase again:</span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a><span class="ex">[...]</span></span></code></pre></div>
<ol start="3" type="1">
<li>Add the key to the agent.</li>
</ol>
<div class="sourceCode" id="cb9"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ssh-add ~/.ssh/tvlivingroom</span></code></pre></div>
<ol start="4" type="1">
<li>Copy the key to the device.</li>
</ol>
<div class="sourceCode" id="cb10"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ssh-copy-id <span class="at">-i</span> ~/.ssh/tvlivingroom.pub <span class="at">-o</span> IdentitiesOnly=yes root@192.168.1.20</span></code></pre></div>
<ol start="5" type="1">
<li>You could SSH to the device with the following command, but it’s quite long, so skip to the next step.</li>
</ol>
<div class="sourceCode" id="cb11"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ssh <span class="at">-i</span> ~/.ssh/tvlivingroom.pub root@192.168.1.10</span></code></pre></div>
<ol start="6" type="1">
<li>Create a match block in <code>~/.ssh/config</code>:</li>
</ol>
<div class="sourceCode" id="cb12"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="ex">Host</span> salon</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a>  <span class="ex">User</span> root</span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a>  <span class="ex">HostName</span> 192.168.50.201</span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a>  <span class="ex">IdentityFile</span> /home/timi/.ssh/salon </span></code></pre></div>
<p>Or, if you’re using Home Manager, this snippet would do the trick:</p>
<div class="sourceCode" id="cb13"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="ex">home-manager.users.me</span> = {</span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a>  <span class="ex">programs.ssh</span> = {</span>
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a>    <span class="ex">matchBlocks</span> = {</span>
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a>      <span class="st">&quot;tvlivingroom&quot;</span> = {</span>
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a>        <span class="ex">user</span> = <span class="st">&quot;root&quot;</span><span class="kw">;</span></span>
<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a>        <span class="ex">identityFile</span> = <span class="st">&quot;/home/me/.ssh/tvlivingroom&quot;</span><span class="kw">;</span></span>
<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a>        <span class="fu">hostname</span> = <span class="st">&quot;192.168.1.10&quot;</span><span class="kw">;</span></span>
<span id="cb13-8"><a href="#cb13-8" aria-hidden="true" tabindex="-1"></a>      <span class="er">}</span><span class="kw">;</span></span>
<span id="cb13-9"><a href="#cb13-9" aria-hidden="true" tabindex="-1"></a>    <span class="er">}</span><span class="kw">;</span></span>
<span id="cb13-10"><a href="#cb13-10" aria-hidden="true" tabindex="-1"></a>  <span class="er">}</span><span class="kw">;</span></span>
<span id="cb13-11"><a href="#cb13-11" aria-hidden="true" tabindex="-1"></a><span class="er">}</span><span class="kw">;</span></span></code></pre></div>
<p>Success, we can now connect with:</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ssh tvlivingroom</span></code></pre></div>
<h2 id="harden-device">Harden device</h2>
<p>Now, we can change the root password and restrict access to ssh key only.</p>
<p>I generate a new login/password combo with Bitwarden, then ssh on the Orange Pi and change the
password with:</p>
<div class="sourceCode" id="cb15"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ssh tvlivingroom</span>
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a><span class="ex">tvlivingroom:~</span> <span class="co"># passwd</span></span>
<span id="cb15-3"><a href="#cb15-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Changing</span> password for root</span>
<span id="cb15-4"><a href="#cb15-4" aria-hidden="true" tabindex="-1"></a><span class="ex">New</span> password: </span>
<span id="cb15-5"><a href="#cb15-5" aria-hidden="true" tabindex="-1"></a><span class="ex">Retype</span> password: </span>
<span id="cb15-6"><a href="#cb15-6" aria-hidden="true" tabindex="-1"></a><span class="ex">passwd:</span> password for root changed by root</span></code></pre></div>
<p>Now, we can remove password login through ssh by going through the UI:</p>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/libreelec_ssh_menu.jpg" class="zoom" alt="Button to access the SSH password menu." />
<figcaption aria-hidden="true">Button to access the SSH password menu.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/libreelec_ssh_disable.jpg" class="zoom" alt="Disable SSH password access." />
<figcaption aria-hidden="true">Disable SSH password access.</figcaption>
</figure>
<h2 id="setup-jellyfin-on-kodi">Setup Jellyfin on Kodi</h2>
<p>Finally, let’s go to the fun part and connect to our Jellyfin server thanks to the <a href="https://github.com/jellyfin/jellyfin-kodi">jellyfin-kodi add-on</a>.</p>
<p>I copied the steps here from <a href="https://jellyfin.org/docs/general/clients/kodi/">the official instructions</a>, specifically the <a href="https://jellyfin.org/docs/general/clients/kodi/#embedded-devices-android-tv-firestick-and-other-tv-boxes">embedded
devices intructions</a> the the <a href="https://jellyfin.org/docs/general/clients/kodi/#install-jellyfin-for-kodi-add-on">install Jellyfin for Kodi
instructions</a> and adding my screenshots. We will install the Jellyfin for Kodi
add-on, not the JellyCon one as I find the former much better integrated in the UI.</p>
<ol type="1">
<li>Install add-on repository.</li>
</ol>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/01.jpg" class="zoom" alt="Go to the “File Manager” menu." />
<figcaption aria-hidden="true">Go to the “File Manager” menu.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/02.jpg" class="zoom" alt="Click on “Add Source”." />
<figcaption aria-hidden="true">Click on “Add Source”.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/03.jpg" class="zoom" alt="The popup “Add file source” will appear." />
<figcaption aria-hidden="true">The popup “Add file source” will appear.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/04.jpg" class="zoom" alt="Click on the “&lt;None&gt;” item." />
<figcaption aria-hidden="true">Click on the “&lt;None&gt;” item.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/05.jpg" class="zoom" alt="Enter “https://kodi.jellyfin.org” in the text box that appeared, then click on “OK”." />
<figcaption aria-hidden="true">Enter “https://kodi.jellyfin.org” in the text box that appeared, then click on “OK”.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/06.jpg" class="zoom" alt="Give a name to the source, here “Jellyfin Repo”." />
<figcaption aria-hidden="true">Give a name to the source, here “Jellyfin Repo”.</figcaption>
</figure>
<p>Then press OK. Now, we can actually install the add-on.</p>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/010.jpg" class="zoom" alt="Go to the “Add-ons” menu." />
<figcaption aria-hidden="true">Go to the “Add-ons” menu.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/011.jpg" class="zoom" alt="Click on “Install from zip file”." />
<figcaption aria-hidden="true">Click on “Install from zip file”.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/012.jpg" class="zoom" alt="Choose “Jellyfin Repo” or the name you gave to the source earlier." />
<figcaption aria-hidden="true">Choose “Jellyfin Repo” or the name you gave to the source earlier.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/013.jpg" class="zoom" alt="Select “repository.jellyfin.kodi.zip”." />
<figcaption aria-hidden="true">Select “repository.jellyfin.kodi.zip”.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/014.jpg" class="zoom" alt="A popup will appear, choose to authorize unknown sources by clicking on “Settings”." />
<figcaption aria-hidden="true">A popup will appear, choose to authorize unknown sources by clicking on “Settings”.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/015.jpg" class="zoom" alt="Choose “Yes” when the “Warning!” popup appears." />
<figcaption aria-hidden="true">Choose “Yes” when the “Warning!” popup appears.</figcaption>
</figure>
<ol start="2" type="1">
<li>Install the Jellyfin for Kodi add-on.</li>
</ol>
<p>Alright, now that the Jellyfin repository is installed, we can finally install the Jellyfin add-on!</p>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/020.jpg" class="zoom" alt="Go to the “Add-ons” menu." />
<figcaption aria-hidden="true">Go to the “Add-ons” menu.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/021.jpg" class="zoom" alt="Choose “Install from repository”." />
<figcaption aria-hidden="true">Choose “Install from repository”.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/022.jpg" class="zoom" alt="Choose “Kodi Jellyfin Addons”." />
<figcaption aria-hidden="true">Choose “Kodi Jellyfin Addons”.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/023.jpg" class="zoom" alt="Choose “Video add-ons”." />
<figcaption aria-hidden="true">Choose “Video add-ons”.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/024.jpg" class="zoom" alt="Pick “Jellyfin”, not “JellyCon”." />
<figcaption aria-hidden="true">Pick “Jellyfin”, <em>not</em> “JellyCon”.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/025.jpg" class="zoom" alt="When installed, a check mark will appear." />
<figcaption aria-hidden="true">When installed, a check mark will appear.</figcaption>
</figure>
<p>After a few seconds, a popup will appear to choose which Jellyfin server to connect to. Assuming one
is discoverable locally, you will see the following.</p>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/030.jpg" class="zoom" alt="Select main server screen." />
<figcaption aria-hidden="true">Select main server screen.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/031.jpg" class="zoom" alt="Pick user." />
<figcaption aria-hidden="true">Pick user.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/032.jpg" class="zoom" alt="Select “Add-on” playback mode." />
<figcaption aria-hidden="true">Select “Add-on” playback mode.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/033.jpg" class="zoom" alt="Choose which library to sync by selecting one or multiple libraries on the left." />
<figcaption aria-hidden="true">Choose which library to sync by selecting one or multiple libraries on the left.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/034.jpg" class="zoom" alt="Press “OK” when all libraries you want to sync are selected." />
<figcaption aria-hidden="true">Press “OK” when all libraries you want to sync are selected.</figcaption>
</figure>
<figure>
<img src="/images/2024-03-22-jellyfin-streaming-media-center-with-libre-elec-and-orange-pi-3/035.jpg" class="zoom" alt="After a few seconds, a notification will appear showing progress." />
<figcaption aria-hidden="true">After a few seconds, a notification will appear showing progress.</figcaption>
</figure>
<p>And you’re done! You can now use your Jellyfin server on the TV. A few ideas for what to do next:</p>
<ul>
<li>Automate Jellyfin/Kodi with Home Assistant. For example, I pause the room speakers when playback
starts.</li>
<li>Synchronize pictures to use as screensavers on the TV</li>
</ul>
<p>Finally, I rebooted just to make sure everything worked as intended.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Use Case for Stateful Property Testing in Go</title>
    <link href="https://blog.tiserbox.com//posts/2024-02-27-stateful-property-testing-in-go.html" />
    <id>https://blog.tiserbox.com//posts/2024-02-27-stateful-property-testing-in-go.html</id>
    <published>2024-02-27T00:00:00Z</published>
    <updated>2024-02-27T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on February 27, 2024
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;go&#39;." href="/tags/go.html">go</a>, <a title="All pages tagged &#39;testing&#39;." href="/tags/testing.html">testing</a>
	  
	</section>
    <section>
        <!--toc:start-->
<ul>
<li><a href="#our-initial-problem-a-driver-migration">Our Initial Problem - A Driver Migration</a></li>
<li><a href="#business-logic-led-to-combinatory-explosion-when-testing">Business Logic Led To Combinatory Explosion When Testing</a></li>
<li><a href="#property-testing">Property Testing</a></li>
<li><a href="#stateful-property-testing">Stateful Property Testing</a></li>
<li><a href="#stateful-property-testing-to-tame-the-combinatory-explosion">Stateful Property Testing to Tame The Combinatory Explosion</a></li>
<li><a href="#great-success">Great Success</a>
<!--toc:end--></li>
</ul>
<p>(Part 2 of this blog post is posted <a href="2024-07-08-make-good-stubs-with-stateful-property-testing.html">here</a>)</p>
<p>In this blog post, I’ll explain what problem I tried to solve by introducing stateful property
testing at work, what is stateful property testing, how well that worked out and what other extra
benefits came along with stateful property testing.</p>
<h1 id="our-initial-problem---a-driver-migration">Our Initial Problem - A Driver Migration</h1>
<p>We wanted to update our MongoDB database version but the driver we used did not support the new
version, was deprecated and did not get any updates. We needed to switch to the official MongoDB
driver first.</p>
<p>Switching drivers should not be too hard. Types and methods are pretty similar, the update is quite
mechanical. Of course, the devil is in the details and it appears there is an incompatible
representation of the ID type between both drivers. This caused issues during our first deploy where
a bug sneaked in spite of our existing tests.</p>
<p>We tried to add more tests but we couldn’t come up with extensive enough tests even while knowing
about this incompatibility issue.</p>
<p>This ultimately let to multiple failed deploys. We could revert them easily and quickly but this
experience led to diminishing confidence in our capacity to deploy without issue.</p>
<h1 id="business-logic-led-to-combinatory-explosion-when-testing">Business Logic Led To Combinatory Explosion When Testing</h1>
<p>Before understanding how stateful property testing helped us to tame our codebase, we must first
review how our codebase is structured for accessing external dependencies.</p>
<p>Access to practically all external resources - like databases, internal and external services - is
done through a dedicated Go struct and methods.</p>
<p>For example, accessing a hypothetical “user” database table would be done through the following
code. I completely invented this example for the blog post but it’s close to what we have.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode go"><code class="sourceCode go"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">package</span> userservice</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="co">// User struct represents a row or record in the</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="co">// database.</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="kw">type</span> User <span class="kw">struct</span><span class="op">{</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>  ID          bson<span class="op">.</span>ID</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>  name        <span class="dt">string</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>  email       <span class="dt">string</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>  deleted     <span class="dt">bool</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>  deletedTime time<span class="op">.</span>Time</span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="co">// NewUser struct with only those fields relevant to</span></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a><span class="co">// creating or updating a new user.</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a><span class="kw">type</span> NewUser <span class="kw">struct</span><span class="op">{</span></span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a>  name  <span class="dt">string</span></span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a>  email <span class="dt">string</span></span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a><span class="co">// Service handles reading from and writing to</span></span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a><span class="co">// the database.</span></span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a><span class="kw">type</span> Service <span class="kw">struct</span><span class="op">{</span></span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a>  dbHandle mongo<span class="op">.</span>db</span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a><span class="co">// New creates a Service.</span></span>
<span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> New<span class="op">(</span>dbHandle<span class="op">)</span> Service <span class="op">{</span></span>
<span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> Service<span class="op">{</span>dbHandle<span class="op">:</span> dbHandle<span class="op">}</span></span>
<span id="cb1-29"><a href="#cb1-29" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb1-30"><a href="#cb1-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-31"><a href="#cb1-31" aria-hidden="true" tabindex="-1"></a><span class="co">// Insert a user in the database.</span></span>
<span id="cb1-32"><a href="#cb1-32" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>us Service<span class="op">)</span> Insert<span class="op">(</span>u NewUser<span class="op">)</span> <span class="op">(</span>bson<span class="op">.</span>ID<span class="op">,</span> <span class="dt">error</span><span class="op">)</span></span>
<span id="cb1-33"><a href="#cb1-33" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-34"><a href="#cb1-34" aria-hidden="true" tabindex="-1"></a><span class="co">// GetByNameOrEmail retrieves a User from the database</span></span>
<span id="cb1-35"><a href="#cb1-35" aria-hidden="true" tabindex="-1"></a><span class="co">// by matching either on their name or email field.</span></span>
<span id="cb1-36"><a href="#cb1-36" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>us Service<span class="op">)</span> GetByNameOrEmail<span class="op">(</span>nameOrEmail <span class="dt">string</span><span class="op">)</span> <span class="op">(</span>User<span class="op">,</span> <span class="dt">error</span><span class="op">)</span></span>
<span id="cb1-37"><a href="#cb1-37" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-38"><a href="#cb1-38" aria-hidden="true" tabindex="-1"></a><span class="co">// Update a user in the database. Fails if the ID does</span></span>
<span id="cb1-39"><a href="#cb1-39" aria-hidden="true" tabindex="-1"></a><span class="co">// not correspond to an existing record.</span></span>
<span id="cb1-40"><a href="#cb1-40" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>us Service<span class="op">)</span> Update<span class="op">(</span>ID bson<span class="op">.</span>ID<span class="op">,</span> u NewUser<span class="op">)</span> <span class="dt">error</span></span>
<span id="cb1-41"><a href="#cb1-41" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-42"><a href="#cb1-42" aria-hidden="true" tabindex="-1"></a><span class="co">// SoftDelete a user by toggling its &quot;deleted&quot; flag to</span></span>
<span id="cb1-43"><a href="#cb1-43" aria-hidden="true" tabindex="-1"></a><span class="co">// True.</span></span>
<span id="cb1-44"><a href="#cb1-44" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>us Service<span class="op">)</span> SoftDelete<span class="op">(</span>ID bson<span class="op">.</span>ID<span class="op">)</span> <span class="dt">error</span></span>
<span id="cb1-45"><a href="#cb1-45" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-46"><a href="#cb1-46" aria-hidden="true" tabindex="-1"></a><span class="co">// Delete a user&#39;s record from the database. This will</span></span>
<span id="cb1-47"><a href="#cb1-47" aria-hidden="true" tabindex="-1"></a><span class="co">// fail if the user was soft-deleted less than a week</span></span>
<span id="cb1-48"><a href="#cb1-48" aria-hidden="true" tabindex="-1"></a><span class="co">// ago.</span></span>
<span id="cb1-49"><a href="#cb1-49" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>us Service<span class="op">)</span> Delete<span class="op">(</span>ID bson<span class="op">.</span>ID<span class="op">)</span> <span class="dt">error</span></span></code></pre></div>
<p>I left out the implementation as it does not matter for the purpose of the post. You can begin to
see why we had a hard time covering all the edge cases in our manual test suite. Some methods there
toggle flags that influence the expected result of others. For example in the above <code>Service</code>, the
<code>GetByNameOrEmail</code> method should only return matching users that had not been soft deleted
previously.</p>
<p>Now, we have about 25 services like the <code>UserService</code> that each handle accessing one table and each
of those have between 5 and 25 methods. This quickly leads to an unmanageable combinatory explosion
of test cases to cover.</p>
<p>We did have actual integration tests, using a real instance of a MongoDB to exercise those methods.
So we caught a lot of bugs that were introduced when migrating over to the new driver, but not all
of them.</p>
<h1 id="property-testing">Property Testing</h1>
<p>When testing a function, property testing is close to what we call in Go fuzzy testing. The idea is
to generate random inputs and assert that a certain property of the function we test is never
violated.</p>
<p>For example, let’s test a Go function to deduplicate items from a slice. There are a few properties
we can test for:</p>
<ol type="1">
<li>The output’s length has to be smaller than the input’s length.</li>
<li>The output must only contain items that are in the input.</li>
<li>The input must only contain items that are in the output.</li>
<li>The output cannot contain twice the same item.</li>
</ol>
<p>Some of those properties can overlap with others but it’s good to be thorough.</p>
<p>Caveat #1 of property testing: coming up with properties to test is not easy.</p>
<p>The library we’re using is called <a href="https://github.com/flyingmutant/rapid">rapid</a>. Here is how
testing the first properties would look like, assuming the deduplicating function is defined elsewere:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode go"><code class="sourceCode go"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">import</span> <span class="st">&quot;testing&quot;</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="kw">import</span> <span class="st">&quot;flyingmutant/rapid&quot;</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> deduplicate<span class="op">[</span>T <span class="dt">comparable</span><span class="op">]([]</span>T<span class="op">)</span> <span class="op">[]</span>T</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> TestingLength<span class="op">(</span>t <span class="op">*</span>testing<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>  rapid<span class="op">.</span>Check<span class="op">(</span>t<span class="op">,</span> <span class="kw">func</span> <span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>    input <span class="op">:=</span> rapid<span class="op">.</span>SliceOf<span class="op">(</span>rapid<span class="op">.</span>Int<span class="op">()).</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;input slice&quot;</span><span class="op">)</span></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a>    output <span class="op">:=</span> deduplicate<span class="op">(</span>input<span class="op">)</span></span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a>    <span class="cf">if</span> <span class="bu">len</span><span class="op">(</span>output<span class="op">)</span> <span class="op">&gt;</span> <span class="bu">len</span><span class="op">(</span>input<span class="op">)</span> <span class="op">{</span></span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a>      t<span class="op">.</span>Fatal<span class="op">(</span><span class="st">&quot;property violated: %d !&lt;= %d&quot;</span><span class="op">,</span> <span class="bu">len</span><span class="op">(</span>output<span class="op">),</span> <span class="bu">len</span><span class="op">(</span>input<span class="op">))</span></span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a>    <span class="op">}</span></span>
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a>  <span class="op">})</span></span>
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p><em>Note that the version used in the snippets here is for version <code>v0.7.2</code>. The interface changed a
bit since then but nothing that impacts what’s discussed here.</em></p>
<p>The <code>rapid</code> library will run the <code>func (*rapid.T)</code> function a bunch of times with different inputs.
If an error is encountered, the library will reduce the input to the minimum input still showcasing
the error. In the example above, it will try to find the minimum input slice that still fails the
property. This is super useful for us humans to have as few noise as possible when trying to
understand where the error comes from.</p>
<p>Caveat #2 of property testing: how can you be sure that an input with two or more duplicate items
got generated? In a finite amount of time and tries, you can’t with the test as written above.</p>
<p>We need to engineer the input to have a few duplicates. There are multiple ways to do that and I
usually choose the one that can be implemented with the less edge cases, in other words the one with
the less if clauses. Usually, you want to avoid needing to test your tests.</p>
<p>To generate a slice with randomly selected duplicates, I would do something like this:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode go"><code class="sourceCode go"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="co">// This creates a slice generator. Use it like so:</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="co">//</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="co">//   inputSlice[int](100, 3).Draw(t, &quot;slice with repeated items&quot;)</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="co">//</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> inputSlice<span class="op">[</span>T <span class="dt">any</span><span class="op">](</span>maxSize <span class="dt">int</span><span class="op">,</span> maxRepeat <span class="dt">int</span><span class="op">)</span> <span class="op">*</span>rapid<span class="op">.</span>Generator<span class="op">[[]</span>T<span class="op">]</span> <span class="op">{</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> rapid<span class="op">.</span>Custom<span class="op">(</span><span class="kw">func</span><span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">[]</span>T <span class="op">{</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>    <span class="co">// Randomly draw size of input</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>    inputSize <span class="op">:=</span> rapid<span class="op">.</span>IntRange<span class="op">(</span><span class="dv">0</span><span class="op">,</span> maxSize<span class="op">).</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;input slice length&quot;</span><span class="op">)</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>    input <span class="op">:=</span> <span class="bu">make</span><span class="op">([]</span>T<span class="op">,</span> <span class="dv">0</span><span class="op">,</span> inputSize<span class="op">)</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>    <span class="co">// For each item, generate 1 or more duplicate.</span></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>    <span class="cf">for</span> i <span class="op">:=</span> <span class="dv">0</span><span class="op">;</span> i <span class="op">&lt;</span> inputSize<span class="op">;</span> i<span class="op">++</span> <span class="op">{</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>      item <span class="op">:=</span> rapid<span class="op">.</span>Make<span class="op">[</span>T<span class="op">]().</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;item&quot;</span><span class="op">)</span></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>      repeat <span class="op">:=</span> rapid<span class="op">.</span>IntRange<span class="op">(</span><span class="dv">1</span><span class="op">,</span> maxRepeat<span class="op">).</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;item repeat count&quot;</span><span class="op">)</span></span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>      <span class="cf">for</span> r <span class="op">:=</span> <span class="dv">0</span><span class="op">;</span> r <span class="op">&lt;</span> repeat<span class="op">;</span> r<span class="op">++</span> <span class="op">{</span></span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>        input <span class="op">=</span> <span class="bu">append</span><span class="op">(</span>input<span class="op">,</span> item<span class="op">)</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a>      <span class="op">}</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a>    <span class="op">}</span></span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a>    <span class="co">// Shuffle the items.</span></span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a>    rand<span class="op">.</span>Shuffle<span class="op">(</span><span class="bu">len</span><span class="op">(</span>input<span class="op">),</span> <span class="kw">func</span><span class="op">(</span>i<span class="op">,</span> j <span class="dt">int</span><span class="op">)</span> <span class="op">{</span></span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a>      input<span class="op">[</span>i<span class="op">],</span> input<span class="op">[</span>j<span class="op">]</span> <span class="op">=</span> input<span class="op">[</span>j<span class="op">],</span> input<span class="op">[</span>i<span class="op">]</span></span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a>    <span class="op">})</span></span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a>    <span class="cf">return</span> input</span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a>  <span class="op">})</span></span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>This will pick up to N randomly selected items, for each item repeat it up to M times and finally
return the randomly shuffled resulting slice.</p>
<p>As you can see, generating an input that covers all edge cases we want in a finite amount of tries
is hard but important. Alas, looking at that function, I would still probably need a test for it.</p>
<h1 id="stateful-property-testing">Stateful Property Testing</h1>
<p>Stateful property testing uses the same idea as property testing but applied to a go struct with
multiple methods where some of those methods can modify the struct’s state.</p>
<p>As a first example, let’s imagine the <code>User</code> struct - the one representing a row in our table - has
methods we want to test.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode go"><code class="sourceCode go"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="co">// User struct we defined earlier. To simplify this</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="co">// section of the post, let&#39;s assume we only have a</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="co">// name and deleted field.</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="kw">type</span> User <span class="kw">struct</span><span class="op">{</span></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>  name        <span class="dt">string</span></span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>  deleted     <span class="dt">bool</span></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>u <span class="op">*</span>User<span class="op">)</span> SetName<span class="op">(</span>name <span class="dt">string</span><span class="op">)</span> <span class="op">{</span></span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a>  u<span class="op">.</span>name <span class="op">=</span> <span class="ot">true</span></span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>u <span class="op">*</span>User<span class="op">)</span> GetName<span class="op">()</span> <span class="dt">string</span> <span class="op">{</span></span>
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> u<span class="op">.</span>name</span>
<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a><span class="co">// SetDeleted modifies a flag on the User.</span></span>
<span id="cb4-18"><a href="#cb4-18" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>u <span class="op">*</span>User<span class="op">)</span> SetDeleted<span class="op">()</span> <span class="op">{</span></span>
<span id="cb4-19"><a href="#cb4-19" aria-hidden="true" tabindex="-1"></a>  u<span class="op">.</span>deleted <span class="op">=</span> <span class="ot">true</span></span>
<span id="cb4-20"><a href="#cb4-20" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb4-21"><a href="#cb4-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-22"><a href="#cb4-22" aria-hidden="true" tabindex="-1"></a><span class="co">// IsDeleted returns the state of the flag.</span></span>
<span id="cb4-23"><a href="#cb4-23" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>u User<span class="op">)</span> IsDeleted<span class="op">()</span> <span class="dt">bool</span> <span class="op">{</span></span>
<span id="cb4-24"><a href="#cb4-24" aria-hidden="true" tabindex="-1"></a>  <span class="cf">return</span> u<span class="op">.</span>deleted</span>
<span id="cb4-25"><a href="#cb4-25" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>Testing this using this <code>rapid</code> library looks like so. Since the <code>User</code> struct has a state - it
remembers it’s name and if it’s deleted - we want to check that this state behaves as we expect.</p>
<p>To do that, we create a model of this behavior - the <code>userStateMachine</code> struct in the following
example. There, we model a state machine where the delete state starts at <code>False</code> and the name
starts as the empty string. Calling <code>SetDeleted()</code> will transition the delete state to <code>True</code> and
<code>SetName()</code> will transition the name to the given value.</p>
<p>We must instruct <code>rapid</code> about this. We do it by creating this <code>userStateMachine</code> struct and adding
a few methods. We create one method for each relevant state we want to transition, here we create
<code>SetName()</code> and <code>Delete()</code> - the name itself do not matter - which will call the relevant method on
both the model and the real <code>User</code> struct we want to test. We then create a third method <code>Check()</code>
that is used to verify the real <code>User</code> struct’s state matches the model.</p>
<p>As long as the <code>Check()</code> function succeeds, <code>rapid</code> will randomly pick between the <code>SetName()</code> and
<code>Delete()</code> method, execute it then call <code>Check()</code> again. It does that cycle about a 100 times by
default. If any method fails, it stops there and tries to replay the failure with smaller and
smaller inputs and prints the smallest input that still fails the test. For <code>rapid</code>, the input
includes the <code>name</code> given to <code>SetName()</code> but also the number of calls to <code>SetName()</code> and <code>Delete()</code>.
And that’s really cool IMO.</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode go"><code class="sourceCode go"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> TestUser<span class="op">(</span>t <span class="op">*</span>testing<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Incantation needed by the rapid library.</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>  rapid<span class="op">.</span>Check<span class="op">(</span>t<span class="op">,</span> <span class="kw">func</span> <span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Repeat<span class="op">(</span>rapid<span class="op">.</span>StateMachineActions<span class="op">(&amp;</span>userStateMachine<span class="op">{}))</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>  <span class="op">})</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a><span class="co">// userStateMachine holds a User we want to test</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a><span class="co">// and a model of how the User should behave.</span></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a><span class="kw">type</span> userStateMachine <span class="kw">struct</span> <span class="op">{</span></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a>  u User</span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a>  name      <span class="dt">string</span></span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a>  isDeleted <span class="dt">bool</span></span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a><span class="co">// SetName modifies the state of the User and of</span></span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a><span class="co">// the model.</span></span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>usm <span class="op">*</span>userStateMachine<span class="op">)</span> SetName<span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a>  newName <span class="op">:=</span> rapid<span class="op">.</span>String<span class="op">().</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;new name&quot;</span><span class="op">)</span></span>
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Call the method we want to test</span></span>
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a>  usm<span class="op">.</span>u<span class="op">.</span>SetName<span class="op">(</span>newName<span class="op">)</span></span>
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Update our internal model</span></span>
<span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a>  usm<span class="op">.</span>name <span class="op">=</span> newName</span>
<span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a><span class="co">// Delete modifies the state of the User and of</span></span>
<span id="cb5-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a><span class="co">// the model.</span></span>
<span id="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>usm <span class="op">*</span>userStateMachine<span class="op">)</span> Delete<span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb5-32"><a href="#cb5-32" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Call the method we want to test</span></span>
<span id="cb5-33"><a href="#cb5-33" aria-hidden="true" tabindex="-1"></a>  usm<span class="op">.</span>u<span class="op">.</span>SetDeleted<span class="op">()</span></span>
<span id="cb5-34"><a href="#cb5-34" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-35"><a href="#cb5-35" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Update our internal model</span></span>
<span id="cb5-36"><a href="#cb5-36" aria-hidden="true" tabindex="-1"></a>  usm<span class="op">.</span>isDeleted <span class="op">=</span> <span class="ot">true</span></span>
<span id="cb5-37"><a href="#cb5-37" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb5-38"><a href="#cb5-38" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-39"><a href="#cb5-39" aria-hidden="true" tabindex="-1"></a><span class="co">// Check verifies that the User and the model agree on</span></span>
<span id="cb5-40"><a href="#cb5-40" aria-hidden="true" tabindex="-1"></a><span class="co">// the state.</span></span>
<span id="cb5-41"><a href="#cb5-41" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>usm <span class="op">*</span>userStateMachine<span class="op">)</span> Check<span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb5-42"><a href="#cb5-42" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> usm<span class="op">.</span>u<span class="op">.</span>GetName<span class="op">()</span> <span class="op">!=</span> usm<span class="op">.</span>name <span class="op">{</span></span>
<span id="cb5-43"><a href="#cb5-43" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Fatalf<span class="op">(</span><span class="st">&quot;name: got (%s) != want (%s)&quot;</span><span class="op">,</span></span>
<span id="cb5-44"><a href="#cb5-44" aria-hidden="true" tabindex="-1"></a>             usm<span class="op">.</span>u<span class="op">.</span>GetName<span class="op">(),</span> usm<span class="op">.</span>name<span class="op">)</span></span>
<span id="cb5-45"><a href="#cb5-45" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb5-46"><a href="#cb5-46" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> usm<span class="op">.</span>u<span class="op">.</span>IsDeleted<span class="op">()</span> <span class="op">!=</span> usm<span class="op">.</span>isDeleted <span class="op">{</span></span>
<span id="cb5-47"><a href="#cb5-47" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Fatalf<span class="op">(</span><span class="st">&quot;deleted: got (%t) != want (%t)&quot;</span><span class="op">,</span></span>
<span id="cb5-48"><a href="#cb5-48" aria-hidden="true" tabindex="-1"></a>             usm<span class="op">.</span>u<span class="op">.</span>IsDeleted<span class="op">(),</span> usm<span class="op">.</span>isDeleted<span class="op">)</span></span>
<span id="cb5-49"><a href="#cb5-49" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb5-50"><a href="#cb5-50" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<h1 id="stateful-property-testing-to-tame-the-combinatory-explosion">Stateful Property Testing to Tame The Combinatory Explosion</h1>
<p>Now, let’s apply what we just saw to the <code>Service</code> we defined earlier. Since the <code>Service</code> handles
access to a MongoDB collection, we will model the state with a map of IDs to <code>User</code> struct.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode go"><code class="sourceCode go"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="co">// User struct we defined earlier.</span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="kw">type</span> User <span class="kw">struct</span><span class="op">{</span></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a>  ID          bson<span class="op">.</span>ID</span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a>  name        <span class="dt">string</span></span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a>  email       <span class="dt">string</span></span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a>  deleted     <span class="dt">bool</span></span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a>  deletedTime time<span class="op">.</span>Time</span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a><span class="co">// NewUser struct with only those fields relevant to</span></span>
<span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a><span class="co">// creating a new user.</span></span>
<span id="cb6-12"><a href="#cb6-12" aria-hidden="true" tabindex="-1"></a><span class="kw">type</span> NewUser <span class="kw">struct</span><span class="op">{</span></span>
<span id="cb6-13"><a href="#cb6-13" aria-hidden="true" tabindex="-1"></a>  name  <span class="dt">string</span></span>
<span id="cb6-14"><a href="#cb6-14" aria-hidden="true" tabindex="-1"></a>  email <span class="dt">string</span></span>
<span id="cb6-15"><a href="#cb6-15" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb6-16"><a href="#cb6-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-17"><a href="#cb6-17" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> TestService<span class="op">(</span>t <span class="op">*</span>testing<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb6-18"><a href="#cb6-18" aria-hidden="true" tabindex="-1"></a>  rapid<span class="op">.</span>Check<span class="op">(</span>t<span class="op">,</span> <span class="kw">func</span> <span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb6-19"><a href="#cb6-19" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Repeat<span class="op">(</span>rapid<span class="op">.</span>StateMachineActions<span class="op">(&amp;</span>serviceStateMachine<span class="op">{</span></span>
<span id="cb6-20"><a href="#cb6-20" aria-hidden="true" tabindex="-1"></a>      <span class="co">// Give the initialized Service to the state machine.</span></span>
<span id="cb6-21"><a href="#cb6-21" aria-hidden="true" tabindex="-1"></a>      us<span class="op">:</span> New<span class="op">(</span><span class="co">/* setup db access code */</span><span class="op">),</span></span>
<span id="cb6-22"><a href="#cb6-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-23"><a href="#cb6-23" aria-hidden="true" tabindex="-1"></a>      <span class="co">// Start with an empty state.</span></span>
<span id="cb6-24"><a href="#cb6-24" aria-hidden="true" tabindex="-1"></a>      state<span class="op">:</span> <span class="bu">make</span><span class="op">(</span><span class="kw">map</span><span class="op">[</span>bson<span class="op">.</span>ID<span class="op">]</span>User<span class="op">),</span></span>
<span id="cb6-25"><a href="#cb6-25" aria-hidden="true" tabindex="-1"></a>    <span class="op">}))</span></span>
<span id="cb6-26"><a href="#cb6-26" aria-hidden="true" tabindex="-1"></a>  <span class="op">})</span></span>
<span id="cb6-27"><a href="#cb6-27" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb6-28"><a href="#cb6-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-29"><a href="#cb6-29" aria-hidden="true" tabindex="-1"></a><span class="co">// serviceStateMachine holds a User we want to test</span></span>
<span id="cb6-30"><a href="#cb6-30" aria-hidden="true" tabindex="-1"></a><span class="co">// and a model of how the User should behave.</span></span>
<span id="cb6-31"><a href="#cb6-31" aria-hidden="true" tabindex="-1"></a><span class="kw">type</span> serviceStateMachine <span class="kw">struct</span> <span class="op">{</span></span>
<span id="cb6-32"><a href="#cb6-32" aria-hidden="true" tabindex="-1"></a>  <span class="co">// We test the Service.</span></span>
<span id="cb6-33"><a href="#cb6-33" aria-hidden="true" tabindex="-1"></a>  us Service</span>
<span id="cb6-34"><a href="#cb6-34" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-35"><a href="#cb6-35" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Against a model.</span></span>
<span id="cb6-36"><a href="#cb6-36" aria-hidden="true" tabindex="-1"></a>  state <span class="kw">map</span><span class="op">[</span>bson<span class="op">.</span>ID<span class="op">]</span>User</span>
<span id="cb6-37"><a href="#cb6-37" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb6-38"><a href="#cb6-38" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-39"><a href="#cb6-39" aria-hidden="true" tabindex="-1"></a><span class="co">// Insert User, expects the insertion to always succeed</span></span>
<span id="cb6-40"><a href="#cb6-40" aria-hidden="true" tabindex="-1"></a><span class="co">// because MongoDB chooses the ID and there are no unique</span></span>
<span id="cb6-41"><a href="#cb6-41" aria-hidden="true" tabindex="-1"></a><span class="co">// contraint.</span></span>
<span id="cb6-42"><a href="#cb6-42" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>ssm serviceStateMachine<span class="op">)</span> Insert<span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb6-43"><a href="#cb6-43" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Generate a NewUser with random name and email.</span></span>
<span id="cb6-44"><a href="#cb6-44" aria-hidden="true" tabindex="-1"></a>  newUser <span class="op">:=</span> rapid<span class="op">.</span>Make<span class="op">[</span>NewUser<span class="op">]().</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;new user&quot;</span><span class="op">)</span></span>
<span id="cb6-45"><a href="#cb6-45" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-46"><a href="#cb6-46" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Insert the new User in the database.</span></span>
<span id="cb6-47"><a href="#cb6-47" aria-hidden="true" tabindex="-1"></a>  newID<span class="op">,</span> err <span class="op">:=</span> ssm<span class="op">.</span>us<span class="op">.</span>Insert<span class="op">(</span>newUser<span class="op">)</span></span>
<span id="cb6-48"><a href="#cb6-48" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> err <span class="op">!=</span> <span class="ot">nil</span> <span class="op">{</span></span>
<span id="cb6-49"><a href="#cb6-49" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Fatal<span class="op">(</span>err<span class="op">)</span></span>
<span id="cb6-50"><a href="#cb6-50" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-51"><a href="#cb6-51" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-52"><a href="#cb6-52" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Add the new User to the state.</span></span>
<span id="cb6-53"><a href="#cb6-53" aria-hidden="true" tabindex="-1"></a>  ssm<span class="op">.</span>state<span class="op">[</span>newId<span class="op">]</span> <span class="op">=</span> User<span class="op">{</span></span>
<span id="cb6-54"><a href="#cb6-54" aria-hidden="true" tabindex="-1"></a>    ID<span class="op">:</span> newId<span class="op">,</span></span>
<span id="cb6-55"><a href="#cb6-55" aria-hidden="true" tabindex="-1"></a>    name<span class="op">:</span> newUser<span class="op">.</span>name<span class="op">,</span></span>
<span id="cb6-56"><a href="#cb6-56" aria-hidden="true" tabindex="-1"></a>    email<span class="op">:</span> newUser<span class="op">.</span>email<span class="op">,</span></span>
<span id="cb6-57"><a href="#cb6-57" aria-hidden="true" tabindex="-1"></a>    deleted<span class="op">:</span> <span class="ot">false</span><span class="op">,</span></span>
<span id="cb6-58"><a href="#cb6-58" aria-hidden="true" tabindex="-1"></a>    deletedTime<span class="op">:</span> time<span class="op">.</span>Time<span class="op">{},</span></span>
<span id="cb6-59"><a href="#cb6-59" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-60"><a href="#cb6-60" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb6-61"><a href="#cb6-61" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-62"><a href="#cb6-62" aria-hidden="true" tabindex="-1"></a><span class="co">// GetByNameOrEmailExisting tries to find a User we know</span></span>
<span id="cb6-63"><a href="#cb6-63" aria-hidden="true" tabindex="-1"></a><span class="co">// exists either by its name or its email.</span></span>
<span id="cb6-64"><a href="#cb6-64" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>ssm serviceStateMachine<span class="op">)</span> GetByNameOrEmailExisting<span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb6-65"><a href="#cb6-65" aria-hidden="true" tabindex="-1"></a>  <span class="co">// We purposely pick the user ID and not the User</span></span>
<span id="cb6-66"><a href="#cb6-66" aria-hidden="true" tabindex="-1"></a>  <span class="co">// struct here because then the only thing the rapid</span></span>
<span id="cb6-67"><a href="#cb6-67" aria-hidden="true" tabindex="-1"></a>  <span class="co">// library will log is the ID and not the full User</span></span>
<span id="cb6-68"><a href="#cb6-68" aria-hidden="true" tabindex="-1"></a>  <span class="co">// struct. It doesn&#39;t seem important at first, but</span></span>
<span id="cb6-69"><a href="#cb6-69" aria-hidden="true" tabindex="-1"></a>  <span class="co">// trust me when the User struct gets more fields</span></span>
<span id="cb6-70"><a href="#cb6-70" aria-hidden="true" tabindex="-1"></a>  <span class="co">// and you need to navigate through a lot of log</span></span>
<span id="cb6-71"><a href="#cb6-71" aria-hidden="true" tabindex="-1"></a>  <span class="co">// lines, reducing the log lines is a substantial</span></span>
<span id="cb6-72"><a href="#cb6-72" aria-hidden="true" tabindex="-1"></a>  <span class="co">// quality of life improvement.</span></span>
<span id="cb6-73"><a href="#cb6-73" aria-hidden="true" tabindex="-1"></a>  existingID <span class="op">:=</span> rapid<span class="op">.</span>SampleFrom<span class="op">(</span>lo<span class="op">.</span>Keys<span class="op">(</span>ssm<span class="op">.</span>state<span class="op">)).</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;user ID&quot;</span><span class="op">)</span></span>
<span id="cb6-74"><a href="#cb6-74" aria-hidden="true" tabindex="-1"></a>  existingUser <span class="op">:=</span> ssm<span class="op">.</span>state<span class="op">[</span>existingID<span class="op">]</span></span>
<span id="cb6-75"><a href="#cb6-75" aria-hidden="true" tabindex="-1"></a>  nameOrEmail <span class="op">:=</span> rapid<span class="op">.</span>OneOf<span class="op">(</span></span>
<span id="cb6-76"><a href="#cb6-76" aria-hidden="true" tabindex="-1"></a>    rapid<span class="op">.</span>Just<span class="op">(</span>existingUser<span class="op">.</span>name<span class="op">),</span></span>
<span id="cb6-77"><a href="#cb6-77" aria-hidden="true" tabindex="-1"></a>    rapid<span class="op">.</span>Just<span class="op">(</span>existingUser<span class="op">.</span>email<span class="op">),</span></span>
<span id="cb6-78"><a href="#cb6-78" aria-hidden="true" tabindex="-1"></a>  <span class="op">).</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;name or email&quot;</span><span class="op">)</span></span>
<span id="cb6-79"><a href="#cb6-79" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-80"><a href="#cb6-80" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Get the User from the database.</span></span>
<span id="cb6-81"><a href="#cb6-81" aria-hidden="true" tabindex="-1"></a>  gotUser<span class="op">,</span> err <span class="op">:=</span> ssm<span class="op">.</span>us<span class="op">.</span>GetByNameOrEmail<span class="op">(</span>nameOrEmail<span class="op">)</span></span>
<span id="cb6-82"><a href="#cb6-82" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> err <span class="op">!=</span> <span class="ot">nil</span> <span class="op">{</span></span>
<span id="cb6-83"><a href="#cb6-83" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Fatal<span class="op">(</span>err<span class="op">)</span></span>
<span id="cb6-84"><a href="#cb6-84" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-85"><a href="#cb6-85" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-86"><a href="#cb6-86" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Assert the user we got is the one we want.</span></span>
<span id="cb6-87"><a href="#cb6-87" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> diff <span class="op">:=</span> cmp<span class="op">.</span>Diff<span class="op">(</span>existingUser<span class="op">,</span> gotUser<span class="op">);</span> diff <span class="op">!=</span> <span class="st">&quot;&quot;</span> <span class="op">{</span></span>
<span id="cb6-88"><a href="#cb6-88" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Fatalf<span class="op">(</span><span class="st">&quot;(-want, +got):</span><span class="ch">\n</span><span class="st">%s&quot;</span><span class="op">,</span> diff<span class="op">)</span></span>
<span id="cb6-89"><a href="#cb6-89" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-90"><a href="#cb6-90" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb6-91"><a href="#cb6-91" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-92"><a href="#cb6-92" aria-hidden="true" tabindex="-1"></a><span class="co">// GetByNameOrEmailNotExisting tries to find a User using</span></span>
<span id="cb6-93"><a href="#cb6-93" aria-hidden="true" tabindex="-1"></a><span class="co">// a random string and expects to fail.</span></span>
<span id="cb6-94"><a href="#cb6-94" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>ssm serviceStateMachine<span class="op">)</span> GetByNameOrEmailNotExisting<span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb6-95"><a href="#cb6-95" aria-hidden="true" tabindex="-1"></a>  nameOrEmail <span class="op">:=</span> rapid<span class="op">.</span>String<span class="op">().</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;random name or email&quot;</span><span class="op">)</span></span>
<span id="cb6-96"><a href="#cb6-96" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-97"><a href="#cb6-97" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Get the User from the database and assert we didn&#39;t</span></span>
<span id="cb6-98"><a href="#cb6-98" aria-hidden="true" tabindex="-1"></a>  <span class="co">// find it.</span></span>
<span id="cb6-99"><a href="#cb6-99" aria-hidden="true" tabindex="-1"></a>  _<span class="op">,</span> err <span class="op">:=</span> ssm<span class="op">.</span>us<span class="op">.</span>GetByNameOrEmail<span class="op">(</span>nameOrEmail<span class="op">)</span></span>
<span id="cb6-100"><a href="#cb6-100" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> err <span class="op">!=</span> NotFoundError <span class="op">{</span></span>
<span id="cb6-101"><a href="#cb6-101" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Fatal<span class="op">(</span>err<span class="op">)</span></span>
<span id="cb6-102"><a href="#cb6-102" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-103"><a href="#cb6-103" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb6-104"><a href="#cb6-104" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-105"><a href="#cb6-105" aria-hidden="true" tabindex="-1"></a><span class="co">// UpdateExisting tries to Update an existing User and</span></span>
<span id="cb6-106"><a href="#cb6-106" aria-hidden="true" tabindex="-1"></a><span class="co">// expects to succeed.</span></span>
<span id="cb6-107"><a href="#cb6-107" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>ssm servicesStateMachine<span class="op">)</span> UpdateExisting<span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb6-108"><a href="#cb6-108" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> <span class="bu">len</span><span class="op">(</span>ssm<span class="op">.</span>state<span class="op">)</span> <span class="op">==</span> <span class="dv">0</span> <span class="op">{</span></span>
<span id="cb6-109"><a href="#cb6-109" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Skip<span class="op">(</span><span class="st">&quot;No existing user.&quot;</span><span class="op">)</span></span>
<span id="cb6-110"><a href="#cb6-110" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-111"><a href="#cb6-111" aria-hidden="true" tabindex="-1"></a>  existingID <span class="op">:=</span> rapid<span class="op">.</span>SampleFrom<span class="op">(</span>lo<span class="op">.</span>Keys<span class="op">(</span>ssm<span class="op">.</span>state<span class="op">)).</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;user ID&quot;</span><span class="op">)</span></span>
<span id="cb6-112"><a href="#cb6-112" aria-hidden="true" tabindex="-1"></a>  existingUser <span class="op">:=</span> ssm<span class="op">.</span>state<span class="op">[</span>existingID<span class="op">]</span></span>
<span id="cb6-113"><a href="#cb6-113" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-114"><a href="#cb6-114" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Generate a NewUser with either an existing or randomly picked</span></span>
<span id="cb6-115"><a href="#cb6-115" aria-hidden="true" tabindex="-1"></a>  <span class="co">// name and email.</span></span>
<span id="cb6-116"><a href="#cb6-116" aria-hidden="true" tabindex="-1"></a>  updatedUser <span class="op">:=</span> NewUser<span class="op">{</span></span>
<span id="cb6-117"><a href="#cb6-117" aria-hidden="true" tabindex="-1"></a>    name <span class="op">=</span> rapid<span class="op">.</span>OneOf<span class="op">(</span>rapid<span class="op">.</span>String<span class="op">(),</span> rapid<span class="op">.</span>Just<span class="op">(</span>existingUser<span class="op">.</span>name<span class="op">)).</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;new user&quot;</span><span class="op">)</span></span>
<span id="cb6-118"><a href="#cb6-118" aria-hidden="true" tabindex="-1"></a>    email <span class="op">=</span> rapid<span class="op">.</span>OneOf<span class="op">(</span>rapid<span class="op">.</span>String<span class="op">(),</span> rapid<span class="op">.</span>Just<span class="op">(</span>existingUser<span class="op">.</span>email<span class="op">)).</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;new email&quot;</span><span class="op">)</span></span>
<span id="cb6-119"><a href="#cb6-119" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-120"><a href="#cb6-120" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-121"><a href="#cb6-121" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Update the User in the database.</span></span>
<span id="cb6-122"><a href="#cb6-122" aria-hidden="true" tabindex="-1"></a>  err <span class="op">:=</span> ssm<span class="op">.</span>us<span class="op">.</span>Update<span class="op">(</span>existingID<span class="op">,</span> updatedUser<span class="op">)</span></span>
<span id="cb6-123"><a href="#cb6-123" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> err <span class="op">!=</span> <span class="ot">nil</span> <span class="op">{</span></span>
<span id="cb6-124"><a href="#cb6-124" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Fatal<span class="op">(</span>err<span class="op">)</span></span>
<span id="cb6-125"><a href="#cb6-125" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-126"><a href="#cb6-126" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-127"><a href="#cb6-127" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Update the state.</span></span>
<span id="cb6-128"><a href="#cb6-128" aria-hidden="true" tabindex="-1"></a>  u <span class="op">:=</span> ssm<span class="op">.</span>state<span class="op">[</span>existingID<span class="op">]</span></span>
<span id="cb6-129"><a href="#cb6-129" aria-hidden="true" tabindex="-1"></a>  u<span class="op">.</span>name <span class="op">=</span> updatedUser<span class="op">.</span>name</span>
<span id="cb6-130"><a href="#cb6-130" aria-hidden="true" tabindex="-1"></a>  u<span class="op">.</span>email <span class="op">=</span> updatedUser<span class="op">.</span>email</span>
<span id="cb6-131"><a href="#cb6-131" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb6-132"><a href="#cb6-132" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-133"><a href="#cb6-133" aria-hidden="true" tabindex="-1"></a><span class="co">// UpdateNotExisting tries to Update an User that does not</span></span>
<span id="cb6-134"><a href="#cb6-134" aria-hidden="true" tabindex="-1"></a><span class="co">// exist and expects to fail.</span></span>
<span id="cb6-135"><a href="#cb6-135" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>ssm servicesStateMachine<span class="op">)</span> UpdateNotExisting<span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb6-136"><a href="#cb6-136" aria-hidden="true" tabindex="-1"></a>  randomID <span class="op">:=</span> rapid<span class="op">.</span>Make<span class="op">[</span>bson<span class="op">.</span>ID<span class="op">]().</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;user ID&quot;</span><span class="op">)</span></span>
<span id="cb6-137"><a href="#cb6-137" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-138"><a href="#cb6-138" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Generate a NewUser with randomly picked name and email.</span></span>
<span id="cb6-139"><a href="#cb6-139" aria-hidden="true" tabindex="-1"></a>  updatedUser <span class="op">:=</span> NewUser<span class="op">{</span></span>
<span id="cb6-140"><a href="#cb6-140" aria-hidden="true" tabindex="-1"></a>    name <span class="op">=</span> rapid<span class="op">.</span>String<span class="op">().</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;new user&quot;</span><span class="op">)</span></span>
<span id="cb6-141"><a href="#cb6-141" aria-hidden="true" tabindex="-1"></a>    email <span class="op">=</span> rapid<span class="op">.</span>String<span class="op">().</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;new email&quot;</span><span class="op">)</span></span>
<span id="cb6-142"><a href="#cb6-142" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-143"><a href="#cb6-143" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-144"><a href="#cb6-144" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Update the User in the database.</span></span>
<span id="cb6-145"><a href="#cb6-145" aria-hidden="true" tabindex="-1"></a>  err <span class="op">:=</span> ssm<span class="op">.</span>us<span class="op">.</span>Update<span class="op">(</span>randomID<span class="op">,</span> updatedUser<span class="op">)</span></span>
<span id="cb6-146"><a href="#cb6-146" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> err <span class="op">==</span> <span class="ot">nil</span> <span class="op">{</span></span>
<span id="cb6-147"><a href="#cb6-147" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Fatal<span class="op">(</span><span class="st">&quot;Expected error while updating the user&quot;</span><span class="op">)</span></span>
<span id="cb6-148"><a href="#cb6-148" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-149"><a href="#cb6-149" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb6-150"><a href="#cb6-150" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-151"><a href="#cb6-151" aria-hidden="true" tabindex="-1"></a><span class="co">// SoftDeleteExisting tries to soft delete an existing User and</span></span>
<span id="cb6-152"><a href="#cb6-152" aria-hidden="true" tabindex="-1"></a><span class="co">// expects to succeed.</span></span>
<span id="cb6-153"><a href="#cb6-153" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>ssm servicesStateMachine<span class="op">)</span> SoftDeleteExisting<span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb6-154"><a href="#cb6-154" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> <span class="bu">len</span><span class="op">(</span>ssm<span class="op">.</span>state<span class="op">)</span> <span class="op">==</span> <span class="dv">0</span> <span class="op">{</span></span>
<span id="cb6-155"><a href="#cb6-155" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Skip<span class="op">(</span><span class="st">&quot;No existing user.&quot;</span><span class="op">)</span></span>
<span id="cb6-156"><a href="#cb6-156" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-157"><a href="#cb6-157" aria-hidden="true" tabindex="-1"></a>  existingID <span class="op">:=</span> rapid<span class="op">.</span>SampleFrom<span class="op">(</span>lo<span class="op">.</span>Keys<span class="op">(</span>ssm<span class="op">.</span>state<span class="op">)).</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;user ID&quot;</span><span class="op">)</span></span>
<span id="cb6-158"><a href="#cb6-158" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-159"><a href="#cb6-159" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Update the User in the database.</span></span>
<span id="cb6-160"><a href="#cb6-160" aria-hidden="true" tabindex="-1"></a>  err <span class="op">:=</span> ssm<span class="op">.</span>us<span class="op">.</span>SoftDelete<span class="op">(</span>existingID<span class="op">)</span></span>
<span id="cb6-161"><a href="#cb6-161" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> err <span class="op">!=</span> <span class="ot">nil</span> <span class="op">{</span></span>
<span id="cb6-162"><a href="#cb6-162" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Fatal<span class="op">(</span>err<span class="op">)</span></span>
<span id="cb6-163"><a href="#cb6-163" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-164"><a href="#cb6-164" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-165"><a href="#cb6-165" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Update the state. For the model, soft deleting is equivalent</span></span>
<span id="cb6-166"><a href="#cb6-166" aria-hidden="true" tabindex="-1"></a>  <span class="co">// to a plain delete.</span></span>
<span id="cb6-167"><a href="#cb6-167" aria-hidden="true" tabindex="-1"></a>  <span class="bu">delete</span><span class="op">(</span>ssm<span class="op">.</span>state<span class="op">,</span> existingID<span class="op">)</span></span>
<span id="cb6-168"><a href="#cb6-168" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb6-169"><a href="#cb6-169" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-170"><a href="#cb6-170" aria-hidden="true" tabindex="-1"></a><span class="co">// SoftDeleteNotExisting tries to soft delete an User that does</span></span>
<span id="cb6-171"><a href="#cb6-171" aria-hidden="true" tabindex="-1"></a><span class="co">// not exist and expects to fail.</span></span>
<span id="cb6-172"><a href="#cb6-172" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>ssm servicesStateMachine<span class="op">)</span> SoftDeleteNotExisting<span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb6-173"><a href="#cb6-173" aria-hidden="true" tabindex="-1"></a>  randomID <span class="op">:=</span> rapid<span class="op">.</span>Make<span class="op">[</span>bson<span class="op">.</span>ID<span class="op">]().</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;user ID&quot;</span><span class="op">)</span></span>
<span id="cb6-174"><a href="#cb6-174" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-175"><a href="#cb6-175" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Update the User in the database.</span></span>
<span id="cb6-176"><a href="#cb6-176" aria-hidden="true" tabindex="-1"></a>  err <span class="op">:=</span> ssm<span class="op">.</span>us<span class="op">.</span>SoftDelete<span class="op">(</span>randomID<span class="op">)</span></span>
<span id="cb6-177"><a href="#cb6-177" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> err <span class="op">==</span> <span class="ot">nil</span> <span class="op">{</span></span>
<span id="cb6-178"><a href="#cb6-178" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Fatal<span class="op">(</span><span class="st">&quot;Expected error while deleting the user&quot;</span><span class="op">)</span></span>
<span id="cb6-179"><a href="#cb6-179" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-180"><a href="#cb6-180" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb6-181"><a href="#cb6-181" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-182"><a href="#cb6-182" aria-hidden="true" tabindex="-1"></a><span class="co">// DeleteExisting tries to delete an existing User and expects</span></span>
<span id="cb6-183"><a href="#cb6-183" aria-hidden="true" tabindex="-1"></a><span class="co">// to succeed.</span></span>
<span id="cb6-184"><a href="#cb6-184" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>ssm servicesStateMachine<span class="op">)</span> DeleteExisting<span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb6-185"><a href="#cb6-185" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> <span class="bu">len</span><span class="op">(</span>ssm<span class="op">.</span>state<span class="op">)</span> <span class="op">==</span> <span class="dv">0</span> <span class="op">{</span></span>
<span id="cb6-186"><a href="#cb6-186" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Skip<span class="op">(</span><span class="st">&quot;No existing user.&quot;</span><span class="op">)</span></span>
<span id="cb6-187"><a href="#cb6-187" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-188"><a href="#cb6-188" aria-hidden="true" tabindex="-1"></a>  existingID <span class="op">:=</span> rapid<span class="op">.</span>SampleFrom<span class="op">(</span>lo<span class="op">.</span>Keys<span class="op">(</span>ssm<span class="op">.</span>state<span class="op">)).</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;user ID&quot;</span><span class="op">)</span></span>
<span id="cb6-189"><a href="#cb6-189" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-190"><a href="#cb6-190" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Update the User in the database.</span></span>
<span id="cb6-191"><a href="#cb6-191" aria-hidden="true" tabindex="-1"></a>  err <span class="op">:=</span> ssm<span class="op">.</span>us<span class="op">.</span>Delete<span class="op">(</span>existingID<span class="op">)</span></span>
<span id="cb6-192"><a href="#cb6-192" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> err <span class="op">!=</span> <span class="ot">nil</span> <span class="op">{</span></span>
<span id="cb6-193"><a href="#cb6-193" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Fatal<span class="op">(</span>err<span class="op">)</span></span>
<span id="cb6-194"><a href="#cb6-194" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-195"><a href="#cb6-195" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-196"><a href="#cb6-196" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Update the state.</span></span>
<span id="cb6-197"><a href="#cb6-197" aria-hidden="true" tabindex="-1"></a>  <span class="bu">delete</span><span class="op">(</span>ssm<span class="op">.</span>state<span class="op">,</span> existingID<span class="op">)</span></span>
<span id="cb6-198"><a href="#cb6-198" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span>
<span id="cb6-199"><a href="#cb6-199" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-200"><a href="#cb6-200" aria-hidden="true" tabindex="-1"></a><span class="co">// DeleteNotExisting tries to soft delete an User that does</span></span>
<span id="cb6-201"><a href="#cb6-201" aria-hidden="true" tabindex="-1"></a><span class="co">// not exist and expects to fail.</span></span>
<span id="cb6-202"><a href="#cb6-202" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>ssm servicesStateMachine<span class="op">)</span> DeleteNotExisting<span class="op">(</span>t <span class="op">*</span>rapid<span class="op">.</span>T<span class="op">)</span> <span class="op">{</span></span>
<span id="cb6-203"><a href="#cb6-203" aria-hidden="true" tabindex="-1"></a>  randomID <span class="op">:=</span> rapid<span class="op">.</span>Make<span class="op">[</span>bson<span class="op">.</span>ID<span class="op">]().</span>Draw<span class="op">(</span>t<span class="op">,</span> <span class="st">&quot;user ID&quot;</span><span class="op">)</span></span>
<span id="cb6-204"><a href="#cb6-204" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-205"><a href="#cb6-205" aria-hidden="true" tabindex="-1"></a>  <span class="co">// Update the User in the database.</span></span>
<span id="cb6-206"><a href="#cb6-206" aria-hidden="true" tabindex="-1"></a>  err <span class="op">:=</span> ssm<span class="op">.</span>us<span class="op">.</span>Delete<span class="op">(</span>randomID<span class="op">)</span></span>
<span id="cb6-207"><a href="#cb6-207" aria-hidden="true" tabindex="-1"></a>  <span class="cf">if</span> err <span class="op">==</span> <span class="ot">nil</span> <span class="op">{</span></span>
<span id="cb6-208"><a href="#cb6-208" aria-hidden="true" tabindex="-1"></a>    t<span class="op">.</span>Fatal<span class="op">(</span><span class="st">&quot;Expected error while deleting the user&quot;</span><span class="op">)</span></span>
<span id="cb6-209"><a href="#cb6-209" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb6-210"><a href="#cb6-210" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>As you can see from the snippets above, we still need to define, for each method we test, every
possible edge case. Usually for services that access databases, a common edge case is if a user
exists or not. Depending on that, the test expects the function call to succeed or not.</p>
<p>I like the snippet above because it shows that although we call different methods on the <code>Service</code>
for deletion and soft deletion, we expect the behavior to be the same in both cases - the affected
<code>User</code> should not be found anymore. The model reflects that as in both cases we <code>delete</code> the <code>User</code>.</p>
<p>Then, stateful property testing kicks in by randomly trying combinations of the edge cases we
defined. The good news is we can focus on each method in isolation, just thinking about how each of
the <code>User</code> field interact with the expected result of the methods on the <code>Service</code> struct. The
<code>rapid</code> library takes care of taming the combinatory explosion.</p>
<p>There’s a few more things I’d like to call out from the snippet above.</p>
<ul>
<li>This is a lot of code. More than me and my team were initially comfortable to write for tests. In
the end, it was worth it.</li>
<li>In the long run, logging too much becomes annoying as it is just noise. That’s why when randomly
choosing a User that exists, it’s better to pick the ID than the User struct directly, otherwise
you print fields that you don’t care about.</li>
<li>We usually split the various edge cases in multiple methods. In the snippet, we have the
<code>Existing</code> and <code>NotExisting</code> suffixed methods. You could do the same in one function but then you
need a way to know if you picked an existing user or not and you begin to fiddle with if clauses
everywhere. In the end, splitting into multiple methods makes the test code easier to understand.</li>
<li>Since we only have a finite time to run the test, the more edge cases you have, the less chance
you’ll have to actually exercise all the edge case combinations in one test run. There are two
ways to go about that: input generation engineering and classification. We covered the former
above.</li>
</ul>
<p>Classification means we add a label to each edge case. Each time an edge case gets reached, we
increment the related label’s counter by 1. At the end of the test, we generate a report that shows
the distribution of edge case. We can then analyze this distribution to ensure all the edge cases
got reached a sufficient amount of times. If not, we need to either run the tests longer or to
either change how we generate the inputs. Classification is in general useful when we have a lot of
edge cases.</p>
<h1 id="great-success">Great Success</h1>
<p>Setting up the stateful property tests to cover most if not all of the edge cases we could think of
was no easy task, but it was really rewarding as it discovered all remaining incompatibility issues.
Although the team was not enthusiastic in introducing that kind of testing in the first place - and
rightfully, it’s quite a lot of code - the result put everyone on board and boosted the confidence
in deploying our changes.</p>
<p>In a following post, I will talk about an additional benefit of the stateful property tests as we
wrote them: they allowed to switch from mocks to stubs. We will see how they helped and just why
switching to stub is a good idea in the first place.
This next blog post is now posted <a href="2024-07-08-make-good-stubs-with-stateful-property-testing.html">here</a>.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Mounting Webdav Folder in NixOS</title>
    <link href="https://blog.tiserbox.com//posts/2024-02-23-mounting-webdav-folder-in-nix-os.html" />
    <id>https://blog.tiserbox.com//posts/2024-02-23-mounting-webdav-folder-in-nix-os.html</id>
    <published>2024-02-23T00:00:00Z</published>
    <updated>2024-02-23T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on February 23, 2024
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>, <a title="All pages tagged &#39;nextcloud&#39;." href="/tags/nextcloud.html">nextcloud</a>
	  
	</section>
    <section>
        <p>I want <a href="https://www.digikam.org">Digikam</a> to access Nextcloud storage using WebDav. But Digikam needs this folder to be
actually mounted on the filesystem, so the setup implies some fiddling.</p>
<h1 id="internet-search">Internet Search</h1>
<p>Here are all the interesting links that helped me figure out how this should work.</p>
<p>From the <a href="https://docs.digikam.org/en/setup_application/collections_settings.html#setup-root-album-folders">Digikam manual</a>:</p>
<blockquote>
<p>Collections on Network Shares: these are root album folders stored remote file systems as Samba or
NFS and mounted as native on your system.</p>
</blockquote>
<p>Clearly, this means the folder must be mounted.</p>
<p>Searching on mounting a webdav folder on linux poped up the <a href="https://wiki.archlinux.org/title/Davfs2">Archlinux wiki</a> on
<code>davfs2</code>, giving me one new keyword to search for.</p>
<p>In the <a href="https://search.nixos.org/options?channel=23.05&amp;size=50&amp;sort=relevance&amp;type=packages&amp;query=davfs2">NixOS options search</a>, there is already a <code>davfs2</code> service, which is
implemented <a href="https://github.com/NixOS/nixpkgs/blob/nixos-23.05/nixos/modules/services/network-filesystems/davfs2.nix">here</a> and sets up a configuration file, user and (<code>un</code>)<code>mount.davfs2</code>
programs. Perfect!</p>
<p>A last search returned the <a href="https://docs.nextcloud.com/server/20/user_manual/en/files/access_webdav.html#creating-webdav-mounts-on-the-linux-command-line">Nextcloud manual</a> which shows how to manage the davfs2 secrets. This
is not handled by the NixOS options above.</p>
<h1 id="lets-try-the-existing-service">Let’s try the existing service</h1>
<p>On my NixOS machine, I just added the following line and deployed.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>services.davfs2.enable = <span class="cn">true</span>;</span></code></pre></div>
<p>Let’s see what we gained:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> mount.davfs <span class="at">--help</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="ex">Usage:</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>    <span class="ex">mount.davfs</span> <span class="at">-V,--version</span>   : print version string</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>    <span class="ex">mount.davfs</span> <span class="at">-h,--help</span>      : print this message</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="ex">To</span> mount a WebDAV-resource don<span class="st">&#39;t call mount.davfs directly, but use</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a><span class="st">`mount&#39;</span> instead.</span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>    <span class="fu">mount</span> <span class="op">&lt;</span>mountpoint<span class="op">&gt;</span>  : or</span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a>    <span class="fu">mount</span> <span class="op">&lt;</span>server-url<span class="op">&gt;</span>  : mount the WebDAV-resource as specified in</span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a>                          <span class="ex">/etc/fstab.</span></span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a>    <span class="fu">mount</span> <span class="at">-t</span> davfs <span class="op">&lt;</span>server-url<span class="op">&gt;</span> <span class="op">&lt;</span>mountpoint<span class="op">&gt;</span> [-o options]</span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a>                        <span class="bu">:</span> mount the WebDAV-resource <span class="op">&lt;</span>server-url<span class="op">&gt;</span></span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a>                          <span class="ex">on</span> mountpoint <span class="op">&lt;</span>mountpoint<span class="op">&gt;</span>. Only root</span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a>                          <span class="ex">is</span> allowed to do this. options is a</span>
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a>                          <span class="ex">comma</span> separated list of options.</span>
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-17"><a href="#cb2-17" aria-hidden="true" tabindex="-1"></a><span class="ex">Recognised</span> options:</span>
<span id="cb2-18"><a href="#cb2-18" aria-hidden="true" tabindex="-1"></a>    <span class="va">conf</span><span class="op">=</span>        <span class="bu">:</span> absolute path of user configuration file</span>
<span id="cb2-19"><a href="#cb2-19" aria-hidden="true" tabindex="-1"></a>    <span class="va">uid</span><span class="op">=</span>         <span class="bu">:</span> owner of the filesystem <span class="er">(</span><span class="ex">username</span> or numeric id<span class="kw">)</span></span>
<span id="cb2-20"><a href="#cb2-20" aria-hidden="true" tabindex="-1"></a>    <span class="va">gid</span><span class="op">=</span>         <span class="bu">:</span> group of the filesystem <span class="er">(</span><span class="ex">group</span> name or numeric id<span class="kw">)</span></span>
<span id="cb2-21"><a href="#cb2-21" aria-hidden="true" tabindex="-1"></a>    <span class="va">file_mode</span><span class="op">=</span>   <span class="bu">:</span> default file mode <span class="er">(</span><span class="ex">octal</span><span class="kw">)</span></span>
<span id="cb2-22"><a href="#cb2-22" aria-hidden="true" tabindex="-1"></a>    <span class="va">dir_mode</span><span class="op">=</span>    <span class="bu">:</span> default directory mode <span class="er">(</span><span class="ex">octal</span><span class="kw">)</span></span>
<span id="cb2-23"><a href="#cb2-23" aria-hidden="true" tabindex="-1"></a>    <span class="ex">ro</span>           : mount read-only</span>
<span id="cb2-24"><a href="#cb2-24" aria-hidden="true" tabindex="-1"></a>    <span class="ex">rw</span>           : mount read-write</span>
<span id="cb2-25"><a href="#cb2-25" aria-hidden="true" tabindex="-1"></a>    <span class="ex">[no]exec</span>     : <span class="er">(</span><span class="ex">don</span><span class="st">&#39;t) allow execution of binaries</span></span>
<span id="cb2-26"><a href="#cb2-26" aria-hidden="true" tabindex="-1"></a><span class="st">    [no]suid     : (don&#39;</span><span class="ex">t</span><span class="kw">)</span> <span class="ex">allow</span> suid and sgid bits to take effect</span>
<span id="cb2-27"><a href="#cb2-27" aria-hidden="true" tabindex="-1"></a>    <span class="ex">[no]grpid</span>    : new files <span class="er">(</span><span class="ex">don</span><span class="st">&#39;t) get the group id of the directory</span></span>
<span id="cb2-28"><a href="#cb2-28" aria-hidden="true" tabindex="-1"></a><span class="st">                   in which they are created.</span></span>
<span id="cb2-29"><a href="#cb2-29" aria-hidden="true" tabindex="-1"></a><span class="st">    [no]_netdev  : (no) network connection needed</span></span></code></pre></div>
<p>The exact command I tried that works with Nextcloud is:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo mount <span class="dt">\</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>      <span class="at">-t</span> davfs <span class="dt">\</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>      <span class="at">-o</span> uid=1000 <span class="dt">\</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>      https://<span class="va">$fqdn</span>/remote.php/dav/files/<span class="va">$myuser</span> <span class="dt">\</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>      mnt</span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="ex">Please</span> enter the username to authenticate with server</span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="ex">https://</span><span class="va">$fqdn</span><span class="ex">/remote.php/dav/files/</span><span class="va">$myuser</span> or hit enter for none.</span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>  <span class="ex">Username:</span> <span class="va">$username</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a><span class="ex">Please</span> enter the password to authenticate user timi with server</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a><span class="ex">https://</span><span class="va">$fqdn</span><span class="ex">/remote.php/dav/files/</span><span class="va">$myuser</span> or hit enter for none.</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>  <span class="ex">Password:</span> <span class="va">$password</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a><span class="ex">warning:</span> the server does not support locks</span></code></pre></div>
<p>I just needed to enter my Nextcloud username and password when prompted.</p>
<h1 id="create-a-secret-file">Create a secret file</h1>
<p>I highly recommend creating an App password for mounting the webdav folder, this is much faster than
using your main account password as can be seen in <a href="https://github.com/nextcloud/server/issues/32729#issuecomment-1556667151">this benchmark</a>.</p>
<p><code>davfs2</code> reads secrets from a file called <code>/etc/davfs2/secrets</code>. Using <code>sops-nix</code>, you can create
this file with:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a>sops.secrets.<span class="st">&quot;webdav/nextcloud&quot;</span> = <span class="op">{</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">sopsFile</span> <span class="op">=</span> <span class="ss">./secrets.yaml</span><span class="op">;</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">mode</span> <span class="op">=</span> <span class="st">&quot;0600&quot;</span><span class="op">;</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">path</span> <span class="op">=</span> <span class="st">&quot;/etc/davfs2/secrets&quot;</span><span class="op">;</span></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>The secret in the sops file must follow a specific format, one line per webdav folder to mount.</p>
<pre><code>&lt;what&gt; &lt;user&gt; &lt;password&gt;</code></pre>
<p>From our example above, we would have something like this:</p>
<pre><code>https://$fqdn/remote.php/dav/files/$myuser $username $password</code></pre>
<h1 id="make-a-systemd-service-out-of-it">Make a Systemd service out of it</h1>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="ex">systemd.mounts</span> = [</span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>  <span class="kw">{</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>    <span class="bu">enable</span> = true<span class="kw">;</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a>    <span class="ex">description</span> = <span class="st">&quot;Webdav mount point&quot;</span><span class="kw">;</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a>    <span class="ex">after</span> = [ <span class="st">&quot;network-online.target&quot;</span> ]<span class="kw">;</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a>    <span class="ex">wants</span> = [ <span class="st">&quot;network-online.target&quot;</span> ]<span class="kw">;</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a>  </span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a>    <span class="ex">what</span> = <span class="st">&quot;https://</span><span class="va">$fqdn</span><span class="st">/remote.php/dav/files/</span><span class="va">$myuser</span><span class="st">&quot;</span><span class="kw">;</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a>    <span class="ex">where</span> = <span class="st">&quot;/mnt/nextcloud&quot;</span><span class="kw">;</span></span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a>    <span class="ex">options</span> = uid=1000,gid=1000,file_mode=0664,dir_mode=2775</span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a>    <span class="bu">type</span> = <span class="st">&quot;davfs&quot;</span><span class="kw">;</span></span>
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a>    <span class="ex">mountConfig.TimeoutSec</span> = 15<span class="kw">;</span></span>
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a>  <span class="kw">}</span></span>
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a><span class="ex">]</span><span class="kw">;</span></span></code></pre></div>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>ZFS on NixOS</title>
    <link href="https://blog.tiserbox.com//posts/2024-02-09-zfs-on-nix-os.html" />
    <id>https://blog.tiserbox.com//posts/2024-02-09-zfs-on-nix-os.html</id>
    <published>2024-02-09T00:00:00Z</published>
    <updated>2024-02-09T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on February  9, 2024
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>, <a title="All pages tagged &#39;zfs&#39;." href="/tags/zfs.html">zfs</a>
	  
	</section>
    <section>
        <p>I resisted long enough. Having bought two new hard drives, I had a good enough reason to switch over
from LVM + mdadm.</p>
<p>This blog post is as much a memo for myself than a terse guide to people wanting to try ZFS.</p>
<h2 id="partitioning">Partitioning</h2>
<p>From
<a href="https://openzfs.github.io/openzfs-docs/Project%20and%20Community/FAQ.html#performance-considerations">https://openzfs.github.io/openzfs-docs/Project%20and%20Community/FAQ.html#performance-considerations</a></p>
<blockquote>
<p>Create your pool using whole disks: When running zpool create use whole disk names. This will
allow ZFS to automatically partition the disk to ensure correct alignment. It will also improve
interoperability with other OpenZFS implementations which honor the wholedisk property.</p>
</blockquote>
<p>From <a href="https://wiki.archlinux.org/title/ZFS#Storage_pools">https://wiki.archlinux.org/title/ZFS#Storage_pools</a> there’s mixed messaging.</p>
<blockquote>
<p>It is not necessary to partition the drives before creating the ZFS filesystem. It is recommended
to point ZFS at an entire disk (ie. /dev/sdx rather than /dev/sdx1), which will automatically
create a GPT (GUID Partition Table) and add an 8 MB reserved partition at the end of the disk for
legacy bootloaders.</p>
</blockquote>
<p>while a bit later:</p>
<blockquote>
<p>The OS does not generate bogus partition numbers from whatever unpredictable data ZFS has written
to the partition sector, and if desired, you can easily over provision SSD drives, and slightly
over provision spindle drives to ensure that different models with slightly different sector
counts can zpool replace into your mirrors.</p>
</blockquote>
<p>From <a href="https://forums.freebsd.org/threads/zfs-whole-disk-vs-gpt-slice.62855/">https://forums.freebsd.org/threads/zfs-whole-disk-vs-gpt-slice.62855/</a></p>
<p>That user posted some good links. It seems using partitions is totally fine. That anyway, giving a
full disk to ZFS on Linux will create a GPT partition anyway.</p>
<p>I will try with partitions and by naming them with GPT labels. Labels are great because they are
unique and can convey meaning.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix shell nixpkgs#gptfdisk</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo fdisk <span class="at">-l</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="ex">...</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a><span class="ex">Disk</span> /dev/sdb: 10.91 TiB, 12000138625024 bytes, 23437770752 sectors</span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a><span class="ex">Disk</span> model: G</span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a><span class="ex">Units:</span> sectors of 1 <span class="pp">*</span> 512 = 512 bytes</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a><span class="ex">Sector</span> size <span class="er">(</span><span class="ex">logical/physical</span><span class="kw">)</span><span class="bu">:</span> 512 bytes / 4096 bytes</span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a><span class="ex">I/O</span> size <span class="er">(</span><span class="ex">minimum/optimal</span><span class="kw">)</span><span class="bu">:</span> 4096 bytes / 4096 bytes</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a><span class="ex">Disk</span> /dev/sdc: 10.91 TiB, 12000138625024 bytes, 23437770752 sectors</span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a><span class="ex">Disk</span> model: G</span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a><span class="ex">Units:</span> sectors of 1 <span class="pp">*</span> 512 = 512 bytes</span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a><span class="ex">Sector</span> size <span class="er">(</span><span class="ex">logical/physical</span><span class="kw">)</span><span class="bu">:</span> 512 bytes / 4096 bytes</span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a><span class="ex">I/O</span> size <span class="er">(</span><span class="ex">minimum/optimal</span><span class="kw">)</span><span class="bu">:</span> 4096 bytes / 4096 bytes</span></code></pre></div>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo gdisk <span class="at">-l</span> /dev/sdb</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="ex">Command</span> <span class="er">(</span><span class="ex">?</span> for help<span class="kw">)</span><span class="bu">:</span> n</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Partition</span> number <span class="er">(</span><span class="ex">1-128,</span> default 1<span class="kw">)</span><span class="bu">:</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="ex">First</span> sector <span class="er">(</span><span class="ex">34-23437770718,</span> default = 2048<span class="kw">)</span> <span class="ex">or</span> {+-}size{KMGTP}:</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="ex">Last</span> sector <span class="er">(</span><span class="ex">2048-23437770718,</span> default = 23437768703<span class="kw">)</span> <span class="ex">or</span> {+-}size{KMGTP}: <span class="at">-1G</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="ex">Current</span> type is 8300 <span class="er">(</span><span class="ex">Linux</span> filesystem<span class="kw">)</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a><span class="ex">Hex</span> code or GUID <span class="er">(</span><span class="ex">L</span> to show codes, Enter = 8300<span class="kw">)</span><span class="bu">:</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="ex">Command</span> <span class="er">(</span><span class="ex">?</span> for help<span class="kw">)</span><span class="bu">:</span> c</span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a><span class="ex">Using</span> 1</span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a><span class="ex">Enter</span> name: ZFSmirror1</span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a><span class="ex">Command</span> <span class="er">(</span><span class="ex">?</span> for help<span class="kw">)</span><span class="bu">:</span> p</span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a><span class="ex">Disk</span> /dev/sdb: 23437770752 sectors, 10.9 TiB</span>
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a><span class="ex">Model:</span> G               </span>
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a><span class="ex">Sector</span> size <span class="er">(</span><span class="ex">logical/physical</span><span class="kw">)</span><span class="bu">:</span> 512/4096 bytes</span>
<span id="cb2-17"><a href="#cb2-17" aria-hidden="true" tabindex="-1"></a><span class="ex">Disk</span> identifier <span class="er">(</span><span class="ex">GUID</span><span class="kw">)</span><span class="bu">:</span> BEA1BE33-E007-4582-B29F-7952D9EE8E26</span>
<span id="cb2-18"><a href="#cb2-18" aria-hidden="true" tabindex="-1"></a><span class="ex">Partition</span> table holds up to 128 entries</span>
<span id="cb2-19"><a href="#cb2-19" aria-hidden="true" tabindex="-1"></a><span class="ex">Main</span> partition table begins at sector 2 and ends at sector 33</span>
<span id="cb2-20"><a href="#cb2-20" aria-hidden="true" tabindex="-1"></a><span class="ex">First</span> usable sector is 34, last usable sector is 23437770718</span>
<span id="cb2-21"><a href="#cb2-21" aria-hidden="true" tabindex="-1"></a><span class="ex">Partitions</span> will be aligned on 2048-sector boundaries</span>
<span id="cb2-22"><a href="#cb2-22" aria-hidden="true" tabindex="-1"></a><span class="ex">Total</span> free space is 2099166 sectors <span class="er">(</span><span class="ex">1025.0</span> MiB<span class="kw">)</span></span>
<span id="cb2-23"><a href="#cb2-23" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-24"><a href="#cb2-24" aria-hidden="true" tabindex="-1"></a><span class="ex">Number</span>  Start <span class="er">(</span><span class="ex">sector</span><span class="kw">)</span>    <span class="ex">End</span> <span class="er">(</span><span class="ex">sector</span><span class="kw">)</span>  <span class="ex">Size</span>       Code  Name</span>
<span id="cb2-25"><a href="#cb2-25" aria-hidden="true" tabindex="-1"></a>   <span class="ex">1</span>            2048     23435673566   10.9 TiB    8300  ZFS data mirror 1</span>
<span id="cb2-26"><a href="#cb2-26" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-27"><a href="#cb2-27" aria-hidden="true" tabindex="-1"></a><span class="ex">Command</span> <span class="er">(</span><span class="ex">?</span> for help<span class="kw">)</span><span class="bu">:</span> w</span>
<span id="cb2-28"><a href="#cb2-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-29"><a href="#cb2-29" aria-hidden="true" tabindex="-1"></a><span class="ex">Final</span> checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING</span>
<span id="cb2-30"><a href="#cb2-30" aria-hidden="true" tabindex="-1"></a><span class="ex">PARTITIONS!!</span></span>
<span id="cb2-31"><a href="#cb2-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-32"><a href="#cb2-32" aria-hidden="true" tabindex="-1"></a><span class="ex">Do</span> you want to proceed<span class="pp">?</span> <span class="er">(</span><span class="ex">Y/N</span><span class="kw">)</span><span class="bu">:</span> Y</span>
<span id="cb2-33"><a href="#cb2-33" aria-hidden="true" tabindex="-1"></a><span class="ex">OK</span><span class="kw">;</span> <span class="ex">writing</span> new GUID partition table <span class="er">(</span><span class="ex">GPT</span><span class="kw">)</span> <span class="ex">to</span> /dev/sdb.</span>
<span id="cb2-34"><a href="#cb2-34" aria-hidden="true" tabindex="-1"></a><span class="ex">The</span> operation has completed successfully.</span></code></pre></div>
<p>I then did the same with <code>/dev/sdc</code>.</p>
<p>I chose a label name with no space otherwise spaces gets replaced with <code>\x20</code> which gives names
like: <code>'ZFS\x20data\x20mirror\x201'</code>. No thanks.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ls <span class="at">-1</span> /dev/disk/by-partlabel/</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="ex">ZFSmirror1</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="ex">ZFSmirror2</span></span></code></pre></div>
<h2 id="enable-zfs-on-the-machine">Enable ZFS on the machine</h2>
<p>From <a href="https://openzfs.github.io/openzfs-docs/Getting%20Started/NixOS/">https://openzfs.github.io/openzfs-docs/Getting%20Started/NixOS/</a> :</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a>boot.supportedFilesystems = <span class="op">[</span> <span class="st">&quot;zfs&quot;</span> <span class="op">]</span>;</span></code></pre></div>
<blockquote>
<p><code>boot.zfs.forceImportRoot</code> is enabled by default for backwards compatibility purposes, but it is
highly recommended to disable this option, as it bypasses some of the safeguards ZFS uses to
protect your ZFS pools.</p>
</blockquote>
<div class="sourceCode" id="cb5"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>boot.zfs.forceImportRoot = <span class="cn">false</span>;</span></code></pre></div>
<p><code>networking.hostId</code> should be unique per machine. The primary use case is to ensure when using ZFS
that a pool isn’t imported accidentally on a wrong machine.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>networking.hostId = <span class="st">&quot;3d6f479a&quot;</span>;</span></code></pre></div>
<h2 id="create-zpool">Create zpool</h2>
<p>From <a href="https://openzfs.github.io/openzfs-docs/Project%20and%20Community/FAQ.html#performance-considerations">https://openzfs.github.io/openzfs-docs/Project%20and%20Community/FAQ.html#performance-considerations</a> I got that a good argument is:</p>
<ul>
<li><code>-o ashift=12</code></li>
</ul>
<p>Also:</p>
<blockquote>
<p>Have enough memory: A minimum of 2GB of memory is recommended for ZFS. Additional memory is
strongly recommended when the compression and deduplication features are enabled.</p>
</blockquote>
<p>From <a href="https://jrs-s.net/2018/08/17/zfs-tuning-cheat-sheet/,">https://jrs-s.net/2018/08/17/zfs-tuning-cheat-sheet/,</a> I got:</p>
<ul>
<li><code>-o ashift=12</code></li>
<li><code>-o xattr=sa</code></li>
<li><code>-o compression=lz4</code></li>
<li><code>-o atime=off</code></li>
<li><code>-o recordsize=1M</code></li>
</ul>
<p>I won’t be using a SLOG as this zpool will hold pictures and documents, so large files. Also no
L2ARV for me as I don’t have that much RAM available.</p>
<p>From <a href="https://openzfs.github.io/openzfs-docs/Getting%20Started/NixOS/Root%20on%20ZFS.html">https://openzfs.github.io/openzfs-docs/Getting%20Started/NixOS/Root%20on%20ZFS.html</a></p>
<ul>
<li><code>-o ashift=12</code></li>
<li><code>-o autotrim=on</code></li>
<li><code>-O acltype=posixacl</code></li>
<li><code>-O canmount=off</code></li>
<li><code>-O dnodesize=auto</code></li>
<li><code>-O normalization=formD</code></li>
<li><code>-O relatime=on</code></li>
<li><code>-O xattr=sa</code></li>
<li><code>-O mountpoint=none</code></li>
</ul>
<p>About dnodesize:</p>
<blockquote>
<p>Consider setting dnodesize to auto if the dataset uses the xattr=sa property setting and the
workload makes heavy use of extended attributes. This may be applicable to SELinux-enabled
systems, Lustre servers, and Samba servers, for example. Literal values are supported for cases
where the optimal size is known in advance and for performance testing.</p>
</blockquote>
<blockquote>
<p>Leave dnodesize set to legacy if you need to receive a send stream of this dataset on a pool that
doesn’t enable the large_dnode feature, or if you need to import this pool on a system that
doesn’t support the large_dnode feature.</p>
</blockquote>
<p>My final command was:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix shell nixpkgs#gptfdisk</span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> zpool create <span class="dt">\</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>    <span class="at">-o</span> ashift=12 <span class="dt">\</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a>    <span class="at">-o</span> autotrim=on <span class="dt">\</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a>    <span class="at">-O</span> encryption=on <span class="dt">\</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a>    <span class="at">-O</span> keyformat=passphrase <span class="dt">\</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a>    <span class="at">-O</span> keylocation=prompt <span class="dt">\</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a>    <span class="at">-O</span> compression=lz4 <span class="dt">\</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a>    <span class="at">-O</span> canmount=off <span class="dt">\</span></span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a>    <span class="at">-O</span> mountpoint=/srv <span class="dt">\</span></span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a>    <span class="at">-O</span> xattr=sa <span class="dt">\</span></span>
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a>    <span class="at">-O</span> atime=off <span class="dt">\</span></span>
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a>    <span class="at">-O</span> acltype=posixacl <span class="dt">\</span></span>
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a>    <span class="at">-O</span> recordsize=1M <span class="dt">\</span></span>
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a>    data <span class="dt">\</span></span>
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a>    mirror <span class="dt">\</span></span>
<span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a>    /dev/disk/by-partlabel/ZFSmirror1 <span class="dt">\</span></span>
<span id="cb7-18"><a href="#cb7-18" aria-hidden="true" tabindex="-1"></a>    /dev/disk/by-partlabel/ZFSmirror2</span></code></pre></div>
<p>This command will ask you for a passphrase.</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> zpool status <span class="at">-v</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>  <span class="ex">pool:</span> data</span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a> <span class="ex">state:</span> ONLINE</span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a><span class="ex">config:</span></span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a>        <span class="ex">NAME</span>          STATE     READ WRITE CKSUM</span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a>        <span class="ex">data</span>          ONLINE       0     0     0</span>
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a>          <span class="ex">ZFSmirror1</span>  ONLINE       0     0     0</span>
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a>          <span class="ex">ZFSmirror2</span>  ONLINE       0     0     0</span>
<span id="cb8-10"><a href="#cb8-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-11"><a href="#cb8-11" aria-hidden="true" tabindex="-1"></a><span class="ex">errors:</span> No known data errors</span></code></pre></div>
<p>I chose legacy mountpoint just so I could mount them manually in a temporary directory and transfer
over what needed to be.</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo zfs create <span class="at">-o</span> mountpoint=legacy data/nextcloud</span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo zfs create <span class="at">-o</span> mountpoint=legacy data/git</span></code></pre></div>
<h2 id="quotas">Quotas</h2>
<p>Nothing fancy here but I wanted to put some.</p>
<p>From <a href="https://docs.oracle.com/cd/E23823_01/html/819-5461/gazvb.html:">https://docs.oracle.com/cd/E23823_01/html/819-5461/gazvb.html:</a></p>
<div class="sourceCode" id="cb10"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo zfs set quota=4T data/nextcloud</span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo zfs set quota=40G data/git</span></code></pre></div>
<p>It can also be done at creation by using <code>-o quote=&lt;size&gt;</code>.</p>
<h2 id="reservation-for-performance">Reservation for Performance</h2>
<p>From <a href="https://web.archive.org/web/20161028084224/http://www.solarisinternals.com/wiki/index.php/ZFS_Best_Practices_Guide#Storage_Pool_Performance_Considerations">https://web.archive.org/web/20161028084224/http://www.solarisinternals.com/wiki/index.php/ZFS_Best_Practices_Guide#Storage_Pool_Performance_Considerations</a> :</p>
<blockquote>
<p>Keep pool space under 80% utilization to maintain pool performance. Currently, pool performance
can degrade when a pool is very full and file systems are updated frequently, such as on a busy
mail server. Full pools might cause a performance penalty, but no other issues. If the primary
workload is immutable files (write once, never remove), then you can keep a pool in the 95-96%
utilization range. Keep in mind that even with mostly static content in the 95-96% range, write,
read, and resilvering performance might suffer.</p>
</blockquote>
<p>For my workload, I chose roughly 90%, so I’ll reserve 1Tb from the ~12Tb available.</p>
<p>From <a href="https://docs.oracle.com/cd/E23823_01/html/819-5461/gazvb.html">https://docs.oracle.com/cd/E23823_01/html/819-5461/gazvb.html</a> :</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo zfs set reservation=1T data</span></code></pre></div>
<h2 id="transfer-data">Transfer data</h2>
<p>Mount new pool to a temporary directory under a temporary root <code>/pool</code> directory, then copy over
with <code>rsync</code>.</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo mount <span class="at">-t</span> zfs data/nextcloud /pool/nextcloud</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo rsync <span class="at">-aHP</span> /srv/data/nextcloud/ /pool/nextcloud</span></code></pre></div>
<h2 id="mount-in-correct-location">Mount in correct location</h2>
<div class="sourceCode" id="cb13"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo umount /srv/nextcloud</span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo zfs set mountpoint=/srv/nextcloud data/nextcloud</span></code></pre></div>
<p>Setting the mountpoint this way will let ZFS mount the pool.</p>
<h2 id="reboot">Reboot</h2>
<p>Because we encrypted the zpool, On reboot, you need to run:</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo zpool import data</span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo zfs load-key data</span>
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo zfs mount <span class="at">-a</span></span></code></pre></div>
<p>There are ways to automate this like <a href="https://wiki.nixos.org/wiki/ZFS#Remote_unlock">remote unlocking</a>.</p>
<h2 id="next">Next</h2>
<p>Next will be snapshots and automating mounting after reboots.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Automated Flake Lock Update Pull Requests and Merging</title>
    <link href="https://blog.tiserbox.com//posts/2023-12-25-automated-flake-lock-update-pull-requests-and-merging.html" />
    <id>https://blog.tiserbox.com//posts/2023-12-25-automated-flake-lock-update-pull-requests-and-merging.html</id>
    <published>2023-12-25T00:00:00Z</published>
    <updated>2023-12-25T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on December 25, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>, <a title="All pages tagged &#39;ci&#39;." href="/tags/ci.html">ci</a>
	  
	</section>
    <section>
        <p>This is pretty cool. Thanks to two GitHub workflows, I managed to automate updating the flake.lock
of <a href="https://github.com/ibizaman/selfhostblocks">my project</a>.</p>
<p>Every day at midnight, the first workflow will run, try to update the <code>flake.lock</code> file and if
there’s an update, it will create a Pull Request with the <code>automerge</code> label. The second workflow
runs every time a Pull Request gets updated (opened, labeled, etc.) and enabled auto-merging of the
Pull Request if the <code>automerge</code> label is set.</p>
<h1 id="create-your-personal-access-token">Create Your Personal Access Token</h1>
<p>First thing, you will need to create a Personal Access Token (PAT) to enable both workflow to work.</p>
<p>Go to <a href="https://github.com/settings/personal-access-tokens">your PAT settings</a> and create a Fine Grained PAT. Select which repository the PAT should
be granted for then set <code>Read and Write</code> access for both <code>Contents</code> and <code>Pull Requests</code> permissions.
The resulting PAT should look like this:</p>
<figure>
<img src="/images/2023-12-25-automated-flake-lock-update-pull-requests-and-merging/pat.png" class="zoom" alt="PAT with Read and Write access set for Contents (code) and Pull Requests permissions." />
<figcaption aria-hidden="true">PAT with <code>Read and Write</code> access set for <code>Contents</code> (code) and <code>Pull Requests</code> permissions.</figcaption>
</figure>
<p>The <code>Contents</code> permission is needed to create a branch and push commits to it and the <code>Pull Requests</code> one to allow to create a pull request. Both are needed for the first workflow but
admittedly only the latter is needed for the second workflow. You could create a second PAT with
only the <code>Pull Requests</code> permission for the second workflow but I didn’t go that far.</p>
<p>Now, add the PAT as a repository secret named <code>GH_TOKEN_FOR_UPDATES</code> in the secrets page of your
repo, at <code>https://github.com/&lt;user&gt;/&lt;repo&gt;/settings/secrets/actions</code>:</p>
<figure>
<img src="/images/2023-12-25-automated-flake-lock-update-pull-requests-and-merging/repo_secrets.png" class="zoom" alt="GH_TOKEN_FOR_UPDATES repository secret." />
<figcaption aria-hidden="true"><code>GH_TOKEN_FOR_UPDATES</code> repository secret.</figcaption>
</figure>
<h1 id="workflow-1-create-pr-with-updated-flake.lock">Workflow 1: Create PR with Updated <code>flake.lock</code></h1>
<p>The workflow uses the <a href="https://github.com/marketplace/actions/update-nix-flake-lock">update-nix-flake-lock</a> action from <a href="https://determinate.systems">Determinate Systems</a>:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">name</span><span class="kw">:</span><span class="at"> Update Flake Lock</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="fu">on</span><span class="kw">:</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">workflow_dispatch</span><span class="kw">:</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">schedule</span><span class="kw">:</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="kw">-</span><span class="at"> </span><span class="fu">cron</span><span class="kw">:</span><span class="at"> </span><span class="st">&#39;0 * * * 0&#39;</span><span class="co"> # runs daily at 00:00</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a><span class="fu">jobs</span><span class="kw">:</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">lockfile</span><span class="kw">:</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">runs-on</span><span class="kw">:</span><span class="at"> ubuntu-latest</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">steps</span><span class="kw">:</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Checkout repository</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> actions/checkout@v3</span></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Install Nix</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> DeterminateSystems/nix-installer-action@main</span></span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">with</span><span class="kw">:</span></span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a><span class="at">          </span><span class="fu">github-token</span><span class="kw">:</span><span class="at"> ${{ secrets.GITHUB_TOKEN }}</span></span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a><span class="at">          </span><span class="fu">extra-conf</span><span class="kw">:</span><span class="at"> </span><span class="st">&quot;system-features = nixos-test benchmark big-parallel kvm&quot;</span></span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Update flake.lock</span></span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> DeterminateSystems/update-flake-lock@main</span></span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">with</span><span class="kw">:</span></span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a><span class="at">          </span><span class="fu">token</span><span class="kw">:</span><span class="at"> ${{ secrets.GH_TOKEN_FOR_UPDATES }}</span></span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a><span class="fu">          pr-labels</span><span class="kw">: </span><span class="ch">|</span></span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a>            automerge</span></code></pre></div>
<p>As stated in the introduction, this workflow runs daily at midnight, calls the
<a href="https://github.com/marketplace/actions/update-nix-flake-lock">DeterminateSystems/update-flake-lock</a> action that updates the <code>flake.lock</code> file and creates a
Pull Request with the <code>automerge</code> label.</p>
<p>Although not necessary, I kept the <code>workflow_dispatch</code> trigger as this allows me to run the action
manually. I wasn’t going to wait for midnight to test the workflow! :D</p>
<p>By the way, the PAT was necessary to trick GitHub in thinking <em>I</em> created the Pull Request.
Otherwise, GitHub actions are not run if a Pull Request gets created from another action. See <a href="https://github.com/marketplace/actions/update-nix-flake-lock#running-github-actions-ci">this
section</a> of the README for more details.</p>
<h1 id="workflow-2-auto-merge-pr">Workflow 2: Auto-Merge PR</h1>
<p>You cannot (yet?) enable auto-merging by default in GitHub, so we need instead to enable it
ourselves whenever a Pull Request gets created or updated.</p>
<p>Well, “ourselves” is by using the <a href="https://github.com/marketplace/actions/auto-merge-pull-request">auto-merge-pull-request</a> action:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="fu">name</span><span class="kw">:</span><span class="at"> Auto Merge</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="fu">on</span><span class="kw">:</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="co">  # Try enabling auto-merge for a pull request when a draft is marked as “ready for review”, when</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="co">  # a required label is applied or when a “do not merge” label is removed, or when a pull request</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="co">  # is updated in any way (opened, synchronized, reopened, edited).</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">pull_request_target</span><span class="kw">:</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">types</span><span class="kw">:</span></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> opened</span></span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> synchronize</span></span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> reopened</span></span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> edited</span></span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> labeled</span></span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> unlabeled</span></span>
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> ready_for_review</span></span>
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-17"><a href="#cb2-17" aria-hidden="true" tabindex="-1"></a><span class="co">  # Try enabling auto-merge for the specified pull request or all open pull requests if none is</span></span>
<span id="cb2-18"><a href="#cb2-18" aria-hidden="true" tabindex="-1"></a><span class="co">  # specified.</span></span>
<span id="cb2-19"><a href="#cb2-19" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">workflow_dispatch</span><span class="kw">:</span></span>
<span id="cb2-20"><a href="#cb2-20" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">inputs</span><span class="kw">:</span></span>
<span id="cb2-21"><a href="#cb2-21" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="fu">pull-request</span><span class="kw">:</span></span>
<span id="cb2-22"><a href="#cb2-22" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">description</span><span class="kw">:</span><span class="at"> Pull Request Number</span></span>
<span id="cb2-23"><a href="#cb2-23" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">required</span><span class="kw">:</span><span class="at"> </span><span class="ch">false</span></span>
<span id="cb2-24"><a href="#cb2-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb2-25"><a href="#cb2-25" aria-hidden="true" tabindex="-1"></a><span class="fu">jobs</span><span class="kw">:</span></span>
<span id="cb2-26"><a href="#cb2-26" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">automerge</span><span class="kw">:</span></span>
<span id="cb2-27"><a href="#cb2-27" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">runs-on</span><span class="kw">:</span><span class="at"> ubuntu-latest</span></span>
<span id="cb2-28"><a href="#cb2-28" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">steps</span><span class="kw">:</span></span>
<span id="cb2-29"><a href="#cb2-29" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> reitermarkus/automerge@v2</span></span>
<span id="cb2-30"><a href="#cb2-30" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">with</span><span class="kw">:</span></span>
<span id="cb2-31"><a href="#cb2-31" aria-hidden="true" tabindex="-1"></a><span class="at">          </span><span class="fu">token</span><span class="kw">:</span><span class="at"> ${{ secrets.GH_TOKEN_FOR_UPDATES }}</span></span>
<span id="cb2-32"><a href="#cb2-32" aria-hidden="true" tabindex="-1"></a><span class="at">          </span><span class="fu">merge-method</span><span class="kw">:</span><span class="at"> rebase</span></span>
<span id="cb2-33"><a href="#cb2-33" aria-hidden="true" tabindex="-1"></a><span class="at">          </span><span class="fu">do-not-merge-labels</span><span class="kw">:</span><span class="at"> never-merge</span></span>
<span id="cb2-34"><a href="#cb2-34" aria-hidden="true" tabindex="-1"></a><span class="at">          </span><span class="fu">required-labels</span><span class="kw">:</span><span class="at"> automerge</span></span>
<span id="cb2-35"><a href="#cb2-35" aria-hidden="true" tabindex="-1"></a><span class="at">          </span><span class="fu">pull-request</span><span class="kw">:</span><span class="at"> ${{ github.event.inputs.pull-request }}</span></span>
<span id="cb2-36"><a href="#cb2-36" aria-hidden="true" tabindex="-1"></a><span class="at">          </span><span class="fu">review</span><span class="kw">:</span><span class="at"> ${{ github.event.inputs.review }}</span></span>
<span id="cb2-37"><a href="#cb2-37" aria-hidden="true" tabindex="-1"></a><span class="at">          </span><span class="fu">dry-run</span><span class="kw">:</span><span class="at"> </span><span class="ch">false</span></span></code></pre></div>
<p>Again, I left the <code>workflow_dispatch</code> trigger for testing, although it wasn’t necessary in the end
since I could just toggle the <code>automerge</code> label on a Pull Request.</p>
<h1 id="result">Result</h1>
<p><a href="https://github.com/ibizaman/selfhostblocks/pull/83">This Pull Request</a> was created and merged automatically.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Investigate Nextcloud Slowness Part 2</title>
    <link href="https://blog.tiserbox.com//posts/2023-12-11-investigate-nextcloud-slowness-part-2.html" />
    <id>https://blog.tiserbox.com//posts/2023-12-11-investigate-nextcloud-slowness-part-2.html</id>
    <published>2023-12-11T00:00:00Z</published>
    <updated>2023-12-11T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on December 11, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nextcloud&#39;." href="/tags/nextcloud.html">nextcloud</a>, <a title="All pages tagged &#39;postgresql&#39;." href="/tags/postgresql.html">postgresql</a>, <a title="All pages tagged &#39;performance&#39;." href="/tags/performance.html">performance</a>
	  
	</section>
    <section>
        <p>My Nextcloud instance feels… sluggish, for lack of better term. It’s working, but it definitely
feels slow. Pages take seconds to load. Let’s continue our <a href="/posts/2023-08-12-what%27s-up-with-nextcloud-webdav-slowness.html#lets-profile-nextcloud">previous investigation</a> of Nextcloud
slowness and try to fix this.</p>
<p>I will warn you, not all investigations did lead to an optimization, most were dead ends. But I
think the process is still valuable as I will show you interesting techniques. So it’s worth to read
along.</p>
<p>If you had to take out one thing from this post, it should be <em>know your whole system</em>. As you’ll
see, I dived deep into profiling which was helpful, but in the end what helped the most was a macro
view of my server.</p>
<h1 id="finding-interesting-profiles">Finding Interesting Profiles</h1>
<p>Last time, we enabled
<a href="/posts/2023-08-12-what%27s-up-with-nextcloud-webdav-slowness.html#lets-profile-nextcloud">debugging</a>
in Nextcloud by having PHP write a profile for every request having the appropriate header. At the
time, we used <code>curl</code> to issue select requests to Nextcloud.</p>
<p>This time, we want to do it with a more realistic load through the browser by simply loading various
pages and seeing what requests take a long time to run, then check their profiles.</p>
<p>For Firefox, I use the extension
<a href="https://github.com/BrianGilbert/xdebug-helper-for-firefox">BrianGilbert/xdebug-helper-for-firefox</a>
which adds the required trigger header to outgoing requests. In the extension’s settings page we set
the “Trace Trigger Value” and “Profile Trigger Value” to <code>debug me</code> which must be the value we set
for the <code>xdebug.trigger_value</code> PHP option.</p>
<p>For reference, here are the PHP option we set in the previous blog post (in nix syntax):</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>services.nextcloud.phpOptions = <span class="op">{</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;</span>xdebug.trigger_value<span class="st">&quot;</span> <span class="op">=</span> <span class="st">&quot;debug_me&quot;</span><span class="op">;</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>  </span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;xdebug.mode&quot;</span> <span class="op">=</span> <span class="st">&quot;profile&quot;</span><span class="op">;</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;xdebug.output_dir&quot;</span> <span class="op">=</span> <span class="st">&quot;/var/log/xdebug&quot;</span><span class="op">;</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;xdebug.start_with_request&quot;</span> <span class="op">=</span> <span class="st">&quot;trigger&quot;</span><span class="op">;</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>A good candidate for interesting profiles is the Photos main page under <code>/apps/photos/</code>. We go to
that page then enable profiling in the extension by clicking on the little icon in the URL bar:</p>
<figure>
<img src="/images/2023-12-11-investigate-nextcloud-slowness-part-2/browser_enable_profiling.png" alt="Enable profiling in browser." />
<figcaption aria-hidden="true">Enable profiling in browser.</figcaption>
</figure>
<p>Now, we open the Web Developer Tools and go to the Network tab and reload the page. We can see all
the requests being made. To pick an interesting one, we can simply sort by duration:</p>
<figure>
<img src="/images/2023-12-11-investigate-nextcloud-slowness-part-2/browser_network_duration.png" class="zoom" alt="List all requests sorted by duration." />
<figcaption aria-hidden="true">List all requests sorted by duration.</figcaption>
</figure>
<p>Ah! The main request to <code>/apps/photos</code> is second on the list, let’s start with that one. To know
what the corresponding profile is, we can just look at the <code>x-xdebug-profile-filename</code> response
header:</p>
<figure>
<img src="/images/2023-12-11-investigate-nextcloud-slowness-part-2/browser_network_profile_name.png" alt="Profile name corresponding to request." />
<figcaption aria-hidden="true">Profile name corresponding to request.</figcaption>
</figure>
<p>Finally, let’s download the profiles locally and open them with kachegrind:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="fu">rsync</span> <span class="at">-va</span> <span class="at">--delete</span> myserver:/var/log/xdebug .</span></code></pre></div>
<p>Another request that was very slow - 12 seconds! - was accessing a folder through webdav:</p>
<figure>
<img src="/images/2023-12-11-investigate-nextcloud-slowness-part-2/browser_network_webdav_slow_request.png" class="zoom" alt="Timing of another slow request." />
<figcaption aria-hidden="true">Timing of another slow request.</figcaption>
</figure>
<p>We will check the profile for that one later.</p>
<h1 id="analyzing-the-first-profile---its-the-database">Analyzing the First Profile - It’s the Database!</h1>
<p><em>Spoiler: result of this analysis is mixed.</em></p>
<p>Let’s analyze the profile with KCacheGrind:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="ex">nix</span> run nixpkgs#kcachegrind xdebug/cachegrind.out.43532</span></code></pre></div>
<p>With KCacheGrind open, we can then sort all the function calls by the <code>self</code> parameter. This will show us where most of the time is spent.</p>
<figure>
<img src="/images/2023-12-11-investigate-nextcloud-slowness-part-2/kcachegrind_top_self.png" alt="Function calls sorted by self." />
<figcaption aria-hidden="true">Function calls sorted by <code>self</code>.</figcaption>
</figure>
<p>And we see the culprit: calls to the database. This line is very promising as it shows the query was
I/O bound (because call to database) and it has all the attributes of something we can optimize:</p>
<ul>
<li>We have knobs to tune the database. Changing the Nextcloud code would’ve been harder.</li>
<li>There are only 20 calls that take 42.29% of the time. We know from looking at the network tab in
the browser that the whole query took 920ms. So that’s <code>920ms * 42.29/100 / 20 calls = 19.45 ms   per call</code>.</li>
</ul>
<p>I’m not sure if 20 ms per call is slow or not, but loading the page feels sluggish for sure so we’ll
see if there’s anything we can do.</p>
<!-- # Another Interesting Profile -->
<h2 id="diving-into-the-database-call">Diving Into the Database Call</h2>
<p>There are several ways we can log slow queries. Here, we will print all queries that take longer
than one second by setting the <code>log_min_duration</code> <a href="https://www.postgresql.org/docs/current/runtime-config-logging.html#GUC-LOG-MIN-DURATION-STATEMENT">parameter</a> to 1000 (1 second).</p>
<p>There are also <a href="https://www.postgresql.org/docs/current/config-setting.html">multiple ways</a> to set this value. Here, I’ll do it through SQL:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo <span class="at">-u</span> nextcloud psql</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="ex">psql</span> <span class="er">(</span><span class="ex">14.10</span><span class="kw">)</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Type</span> <span class="st">&quot;help&quot;</span> for help.</span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="ex">UPDATE</span> pg_settings</span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>  <span class="ex">SET</span> setting = 1000</span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>  <span class="ex">WHERE</span> name = <span class="st">&#39;log_min_duration_statement&#39;</span><span class="kw">;</span></span></code></pre></div>
<p>Now, after reloading a few times the page at <code>/apps/photos</code>, I see sometimes printed in the logs a
query fetching from the <code>oc_filecache</code> table (formatting mine):</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>[<span class="dv">35320</span>] <span class="fu">LOG</span>:  duration: <span class="fl">4894.961</span> ms <span class="kw">execute</span> <span class="op">&lt;</span>unnamed<span class="op">&gt;</span>:</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>  <span class="kw">SELECT</span> <span class="ot">&quot;file&quot;</span>.<span class="ot">&quot;fileid&quot;</span>, <span class="ot">&quot;storage&quot;</span>,</span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>    <span class="ot">&quot;path&quot;</span>, <span class="ot">&quot;path_hash&quot;</span>, <span class="ot">&quot;file&quot;</span>.<span class="ot">&quot;parent&quot;</span>, <span class="ot">&quot;file&quot;</span>.<span class="ot">&quot;name&quot;</span>,</span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>    <span class="ot">&quot;mimetype&quot;</span>, <span class="ot">&quot;mimepart&quot;</span>, <span class="ot">&quot;size&quot;</span>, <span class="ot">&quot;mtime&quot;</span>,</span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>    <span class="ot">&quot;storage_mtime&quot;</span>, <span class="ot">&quot;encrypted&quot;</span>, <span class="ot">&quot;etag&quot;</span>, <span class="ot">&quot;permissions&quot;</span>,</span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>    <span class="ot">&quot;checksum&quot;</span>, <span class="ot">&quot;unencrypted_size&quot;</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a>  <span class="kw">FROM</span> <span class="ot">&quot;oc_filecache&quot;</span> <span class="ot">&quot;file&quot;</span></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a>  <span class="kw">WHERE</span> ((<span class="ot">&quot;file&quot;</span>.<span class="ot">&quot;name&quot;</span> <span class="op">=</span> $<span class="dv">1</span>) <span class="kw">OR</span> (<span class="ot">&quot;file&quot;</span>.<span class="ot">&quot;name&quot;</span> <span class="op">=</span> $<span class="dv">2</span>))</span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a>    <span class="kw">AND</span> ((<span class="ot">&quot;storage&quot;</span> <span class="op">=</span> $<span class="dv">3</span>)</span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a>    <span class="kw">AND</span> ((<span class="ot">&quot;path&quot;</span> <span class="op">=</span> $<span class="dv">4</span>) <span class="kw">OR</span> (<span class="ot">&quot;path&quot;</span> <span class="kw">LIKE</span> $<span class="dv">5</span>)))</span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a>[<span class="dv">35320</span>] DETAIL: <span class="kw">parameters</span>:</span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a>  $<span class="dv">1</span> <span class="op">=</span> <span class="st">&#39;.nomedia&#39;</span>,</span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a>  $<span class="dv">2</span> <span class="op">=</span> <span class="st">&#39;.noimage&#39;</span>,</span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a>  $<span class="dv">3</span> <span class="op">=</span> <span class="st">&#39;5&#39;</span>,</span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a>  $<span class="dv">4</span> <span class="op">=</span> <span class="st">&#39;files&#39;</span>,</span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a>  $<span class="dv">5</span> <span class="op">=</span> <span class="st">&#39;files/%&#39;</span></span></code></pre></div>
<p>This is super useful because we can run an <code>EXPLAIN ANALYZE</code> on it. But before that, we will enable
<code>track_io_timing</code> which will print “time spent reading and writing data file blocks and temporary
file blocks (in milliseconds)”:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="kw">UPDATE</span> pg_settings</span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>  <span class="kw">SET</span> setting <span class="op">=</span> <span class="st">&#39;on&#39;</span></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a>  <span class="kw">WHERE</span> name <span class="op">=</span> <span class="st">&#39;track_io_timing&#39;</span>;</span></code></pre></div>
<p>Now, the explain:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a>nextcloud<span class="op">=&gt;</span> <span class="kw">EXPLAIN</span> (<span class="kw">ANALYZE</span>, BUFFERS)</span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>  <span class="kw">SELECT</span> <span class="ot">&quot;file&quot;</span>.<span class="ot">&quot;fileid&quot;</span>, <span class="ot">&quot;storage&quot;</span>,</span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>    <span class="ot">&quot;path&quot;</span>, <span class="ot">&quot;path_hash&quot;</span>, <span class="ot">&quot;file&quot;</span>.<span class="ot">&quot;parent&quot;</span>, <span class="ot">&quot;file&quot;</span>.<span class="ot">&quot;name&quot;</span>,</span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a>    <span class="ot">&quot;mimetype&quot;</span>, <span class="ot">&quot;mimepart&quot;</span>, <span class="ot">&quot;size&quot;</span>, <span class="ot">&quot;mtime&quot;</span>,</span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a>    <span class="ot">&quot;storage_mtime&quot;</span>, <span class="ot">&quot;encrypted&quot;</span>, <span class="ot">&quot;etag&quot;</span>, <span class="ot">&quot;permissions&quot;</span>,</span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a>    <span class="ot">&quot;checksum&quot;</span>, <span class="ot">&quot;unencrypted_size&quot;</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a>  <span class="kw">FROM</span> <span class="ot">&quot;oc_filecache&quot;</span> <span class="ot">&quot;file&quot;</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a>  <span class="kw">WHERE</span> ((<span class="ot">&quot;file&quot;</span>.<span class="ot">&quot;name&quot;</span> <span class="op">=</span> <span class="st">&#39;.nomedia&#39;</span>)</span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a>         <span class="kw">OR</span> (<span class="ot">&quot;file&quot;</span>.<span class="ot">&quot;name&quot;</span> <span class="op">=</span> <span class="st">&#39;.noimage&#39;</span>))</span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a>    <span class="kw">AND</span> ((<span class="ot">&quot;storage&quot;</span> <span class="op">=</span> <span class="dv">5</span>)</span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a>    <span class="kw">AND</span> ((<span class="ot">&quot;path&quot;</span> <span class="op">=</span> <span class="st">&#39;files&#39;</span>) <span class="kw">OR</span> (<span class="ot">&quot;path&quot;</span> <span class="kw">LIKE</span> <span class="st">&#39;files/%&#39;</span>)));</span></code></pre></div>
<p>Which outputs:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a>Gather  (<span class="kw">cost</span><span class="op">=</span><span class="dv">9808</span>.<span class="dv">72</span><span class="op">..</span><span class="fl">112624.78</span> <span class="kw">rows</span><span class="op">=</span><span class="dv">7</span> width<span class="op">=</span><span class="dv">211</span>)</span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>        (actual <span class="dt">time</span><span class="op">=</span><span class="dv">101</span>.<span class="dv">721</span><span class="op">..</span><span class="fl">150.161</span> <span class="kw">rows</span><span class="op">=</span><span class="dv">5</span> loops<span class="op">=</span><span class="dv">1</span>)</span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a>  Workers Planned: <span class="dv">2</span></span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a>  Workers Launched: <span class="dv">2</span></span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a>  Buffers: <span class="kw">shared</span> hit<span class="op">=</span><span class="dv">49589</span> dirtied<span class="op">=</span><span class="dv">65</span></span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a>  <span class="op">-&gt;</span>  <span class="kw">Parallel</span> <span class="kw">Bitmap</span> <span class="kw">Heap</span> <span class="kw">Scan</span> <span class="kw">on</span> oc_filecache <span class="kw">file</span></span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a>      (<span class="kw">cost</span><span class="op">=</span><span class="dv">8808</span>.<span class="dv">72</span><span class="op">..</span><span class="fl">111624.08</span> <span class="kw">rows</span><span class="op">=</span><span class="dv">3</span> width<span class="op">=</span><span class="dv">211</span>)</span>
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a>      (actual <span class="dt">time</span><span class="op">=</span><span class="dv">94</span>.<span class="dv">107</span><span class="op">..</span><span class="fl">125.883</span> <span class="kw">rows</span><span class="op">=</span><span class="dv">2</span> loops<span class="op">=</span><span class="dv">3</span>)</span>
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a>    Recheck Cond: (<span class="kw">storage</span> <span class="op">=</span> <span class="dv">5</span>)</span>
<span id="cb8-10"><a href="#cb8-10" aria-hidden="true" tabindex="-1"></a>    <span class="kw">Filter</span>: ((((name):<span class="ch">:text</span> <span class="op">=</span> <span class="st">&#39;.nomedia&#39;</span>:<span class="ch">:text</span>) [<span class="op">..</span>.]</span>
<span id="cb8-11"><a href="#cb8-11" aria-hidden="true" tabindex="-1"></a>    <span class="kw">Rows</span> Removed <span class="kw">by</span> <span class="kw">Filter</span>: <span class="dv">193922</span></span>
<span id="cb8-12"><a href="#cb8-12" aria-hidden="true" tabindex="-1"></a>    <span class="kw">Heap</span> Blocks: exact<span class="op">=</span><span class="dv">16025</span></span>
<span id="cb8-13"><a href="#cb8-13" aria-hidden="true" tabindex="-1"></a>    Buffers: <span class="kw">shared</span> hit<span class="op">=</span><span class="dv">49589</span> dirtied<span class="op">=</span><span class="dv">65</span></span>
<span id="cb8-14"><a href="#cb8-14" aria-hidden="true" tabindex="-1"></a>    <span class="op">-&gt;</span>  <span class="kw">Bitmap</span> <span class="kw">Index</span> <span class="kw">Scan</span> <span class="kw">on</span> fs_storage_mimepart</span>
<span id="cb8-15"><a href="#cb8-15" aria-hidden="true" tabindex="-1"></a>        (<span class="kw">cost</span><span class="op">=</span><span class="dv">0</span>.<span class="dv">00</span><span class="op">..</span><span class="fl">8808.72</span> <span class="kw">rows</span><span class="op">=</span><span class="dv">580305</span> width<span class="op">=</span><span class="dv">0</span>)</span>
<span id="cb8-16"><a href="#cb8-16" aria-hidden="true" tabindex="-1"></a>        (actual <span class="dt">time</span><span class="op">=</span><span class="dv">30</span>.<span class="dv">398</span><span class="op">..</span><span class="fl">30.398</span> <span class="kw">rows</span><span class="op">=</span><span class="dv">622607</span> loops<span class="op">=</span><span class="dv">1</span>)</span>
<span id="cb8-17"><a href="#cb8-17" aria-hidden="true" tabindex="-1"></a>      <span class="kw">Index</span> Cond: (<span class="kw">storage</span> <span class="op">=</span> <span class="dv">5</span>)</span>
<span id="cb8-18"><a href="#cb8-18" aria-hidden="true" tabindex="-1"></a>      Buffers: <span class="kw">shared</span> hit<span class="op">=</span><span class="dv">1036</span></span>
<span id="cb8-19"><a href="#cb8-19" aria-hidden="true" tabindex="-1"></a>Planning:</span>
<span id="cb8-20"><a href="#cb8-20" aria-hidden="true" tabindex="-1"></a>  Buffers: <span class="kw">shared</span> hit<span class="op">=</span><span class="dv">315</span></span>
<span id="cb8-21"><a href="#cb8-21" aria-hidden="true" tabindex="-1"></a>Planning <span class="dt">Time</span>: <span class="fl">1.686</span> ms</span>
<span id="cb8-22"><a href="#cb8-22" aria-hidden="true" tabindex="-1"></a>Execution <span class="dt">Time</span>: <span class="fl">150.286</span> ms</span>
<span id="cb8-23"><a href="#cb8-23" aria-hidden="true" tabindex="-1"></a>(<span class="dv">17</span> <span class="kw">rows</span>)</span></code></pre></div>
<p>The execution took this time only 150ms! It also uses indices as we would want. Was the slow request a fluke?</p>
<h2 id="a-database-call-thats-slow">A Database Call That’s Slow</h2>
<p>The <code>storage</code> parameter intrigued me though so I thought I’d peek into how many rows there are per
storage value:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a>nextcloud<span class="op">=&gt;</span> <span class="kw">SELECT</span> <span class="kw">storage</span>, <span class="fu">COUNT</span>(<span class="op">*</span>)</span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a>            <span class="kw">FROM</span> oc_filecache</span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a>            <span class="kw">GROUP</span> <span class="kw">BY</span> <span class="kw">storage</span>;</span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a> <span class="kw">storage</span> |  <span class="fu">count</span></span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a><span class="co">---------+---------</span></span>
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a>       <span class="dv">1</span> |       <span class="dv">4</span></span>
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a>       <span class="dv">2</span> | <span class="dv">1604311</span></span>
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a>       <span class="dv">5</span> |  <span class="dv">583191</span></span>
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a>       <span class="dv">6</span> |       <span class="dv">4</span></span>
<span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a>       <span class="dv">7</span> |    <span class="dv">1071</span></span>
<span id="cb9-12"><a href="#cb9-12" aria-hidden="true" tabindex="-1"></a>       <span class="dv">8</span> |       <span class="dv">9</span></span>
<span id="cb9-13"><a href="#cb9-13" aria-hidden="true" tabindex="-1"></a>       <span class="dv">9</span> |  <span class="dv">149197</span></span>
<span id="cb9-14"><a href="#cb9-14" aria-hidden="true" tabindex="-1"></a>(<span class="dv">7</span> <span class="kw">rows</span>)</span></code></pre></div>
<p>Earlier, we ran the explain with <code>WHERE storage=5</code>. Let’s try <code>storage=2</code> which has more rows:</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a>Gather  (<span class="kw">cost</span><span class="op">=</span><span class="dv">1000</span>.<span class="dv">00</span><span class="op">..</span><span class="fl">120228.77</span> <span class="kw">rows</span><span class="op">=</span><span class="dv">20</span> width<span class="op">=</span><span class="dv">211</span>)</span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a>        (actual <span class="dt">time</span><span class="op">=</span><span class="dv">4971</span>.<span class="dv">440</span><span class="op">..</span><span class="fl">4995.646</span> <span class="kw">rows</span><span class="op">=</span><span class="dv">0</span> loops<span class="op">=</span><span class="dv">1</span>)</span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a>  Workers Planned: <span class="dv">2</span></span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a>  Workers Launched: <span class="dv">2</span></span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a>  Buffers: <span class="kw">shared</span> hit<span class="op">=</span><span class="dv">49096</span> <span class="kw">read</span><span class="op">=</span><span class="dv">48279</span> dirtied<span class="op">=</span><span class="dv">53</span></span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a>  I<span class="op">/</span>O Timings: <span class="kw">read</span><span class="op">=</span><span class="fl">13205.555</span></span>
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a>  <span class="op">-&gt;</span>  <span class="kw">Parallel</span> Seq <span class="kw">Scan</span> <span class="kw">on</span> oc_filecache <span class="kw">file</span></span>
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a>        (<span class="kw">cost</span><span class="op">=</span><span class="dv">0</span>.<span class="dv">00</span><span class="op">..</span><span class="fl">119226.77</span> <span class="kw">rows</span><span class="op">=</span><span class="dv">8</span> width<span class="op">=</span><span class="dv">211</span>)</span>
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a>        (actual <span class="dt">time</span><span class="op">=</span><span class="dv">4962</span>.<span class="dv">747</span><span class="op">..</span><span class="fl">4962.748</span> <span class="kw">rows</span><span class="op">=</span><span class="dv">0</span> loops<span class="op">=</span><span class="dv">3</span>)</span>
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a>    <span class="kw">Filter</span>: (((name):<span class="ch">:text</span> <span class="op">=</span> <span class="st">&#39;.nomedia&#39;</span>:<span class="ch">:text</span>) [<span class="op">..</span>.]</span>
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a>    <span class="kw">Rows</span> Removed <span class="kw">by</span> <span class="kw">Filter</span>: <span class="dv">779248</span></span>
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a>    Buffers: <span class="kw">shared</span> hit<span class="op">=</span><span class="dv">49096</span> <span class="kw">read</span><span class="op">=</span><span class="dv">48279</span> dirtied<span class="op">=</span><span class="dv">53</span></span>
<span id="cb10-13"><a href="#cb10-13" aria-hidden="true" tabindex="-1"></a>    I<span class="op">/</span>O Timings: <span class="kw">read</span><span class="op">=</span><span class="fl">13205.555</span></span>
<span id="cb10-14"><a href="#cb10-14" aria-hidden="true" tabindex="-1"></a>Planning:</span>
<span id="cb10-15"><a href="#cb10-15" aria-hidden="true" tabindex="-1"></a>  Buffers: <span class="kw">shared</span> hit<span class="op">=</span><span class="dv">315</span></span>
<span id="cb10-16"><a href="#cb10-16" aria-hidden="true" tabindex="-1"></a>Planning <span class="dt">Time</span>: <span class="fl">3.013</span> ms</span>
<span id="cb10-17"><a href="#cb10-17" aria-hidden="true" tabindex="-1"></a>Execution <span class="dt">Time</span>: <span class="fl">4995.789</span> ms</span>
<span id="cb10-18"><a href="#cb10-18" aria-hidden="true" tabindex="-1"></a>(<span class="dv">14</span> <span class="kw">rows</span>)</span></code></pre></div>
<p>This time, the query took 5 seconds to run! It’s doing a sequential scan and clearly doing some disk
I/O.</p>
<p>Usually, you’ll want to run a <code>VACUUM</code> on a table to make sure PostgresSQL can optimize the plan:</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a>nextcloud<span class="op">=&gt;</span> VACUUM <span class="kw">ANALYZE</span> oc_filecache;</span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a>VACUUM</span></code></pre></div>
<p>That didn’t change the explain significantly.</p>
<p>By the way, we’re printing the <code>BUFFERS</code> values because that will show us all buffer and I/O
timings. From the <a href="https://www.postgresql.org/docs/current/sql-explain.html">documentation</a>:</p>
<blockquote>
<p>Include the number of shared blocks hit, read, dirtied, and written, the number of local blocks
hit, read, dirtied, and written, the number of temp blocks read and written, and the time spent
reading and writing data file blocks and temporary file blocks (in milliseconds) if
track_io_timing is enabled. <em>(we did enable it!)</em></p>
</blockquote>
<blockquote>
<p><strong>A hit means that a read was avoided because the block was found already in cache when needed.</strong>
Shared blocks contain data from regular tables and indexes; local blocks contain data from
temporary tables and indexes; while temporary blocks contain short-term working data used in
sorts, hashes, Materialize plan nodes, and similar cases.</p>
</blockquote>
<p>The fast query from above, the one hitting storage 5, has only hits! No wonder it’s fast. On the
other hand, the slow one hitting storage 2 has a lot of reads.</p>
<blockquote>
<p><strong>The number of blocks dirtied indicates the number of previously unmodified blocks that were
changed by this query</strong>; while the number of blocks written indicates the number of
previously-dirtied blocks evicted from cache by this backend during query processing.</p>
</blockquote>
<p>There’s not a significant difference in the dirtied buffers between both slow and fast queries, so
we won’t be looking too much into that. Let’s instead see how to reduce shared buffer reads.</p>
<h2 id="an-insight-about-caching">An Insight About Caching</h2>
<p>To make sure the explain plan wasn’t a fluke, I ran it a few times in a row to try to load the
results in the cache. That actually made an impact. At one point, the query with <code>storage = 2</code>
managed to run in 300ms, instead of the 5 seconds we saw earlier. What changed wasn’t the shared
buffers read value but the I/O timing! I suppose this means the OS itself or the disk was caching
the results but I don’t know how to confirm this. Let’s try to tune the shared buffers to improve
caching.</p>
<p>A few things to note before we continue. The drive where the PostgreSQL database is located is an
NVMe drive. This is pretty fast already.</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> df /var/lib/postgresql</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a><span class="ex">Filesystem</span>                                             1K-blocks     Used Available Use% Mounted on</span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a><span class="ex">/dev/disk/by-uuid/6da26333-4d11-4b29-9509-6dcc8151013b</span> 114304164 51399260  57052388  48% /</span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> readlink /dev/disk/by-uuid/6da26333-4d11-4b29-9509-6dcc8151013b</span>
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a><span class="ex">../../sda1</span></span>
<span id="cb12-7"><a href="#cb12-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-8"><a href="#cb12-8" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo fdisk <span class="at">-l</span> /dev/sda</span>
<span id="cb12-9"><a href="#cb12-9" aria-hidden="true" tabindex="-1"></a><span class="ex">Disk</span> /dev/sda: 119.24 GiB, 128035676160 bytes, 250069680 sectors</span>
<span id="cb12-10"><a href="#cb12-10" aria-hidden="true" tabindex="-1"></a><span class="ex">Disk</span> model: SK hynix SC311 S</span></code></pre></div>
<p>Not the fastest NVMe hard drive but it will have to do.</p>
<p>The other thing to note is the results do not stay in the cache very long. If I wait even just 10
seconds and re-issue the explain, the query already takes more than a second again from the 300ms.
I got the feeling optimizing this is going to be tricky.</p>
<p>I found the following website that allows you to input your PostgreSQL machine specs and outputs
what settings you should set. <a href="https://pgtune.leopard.in.ua/">https://pgtune.leopard.in.ua/</a>. My server’s spec are:</p>
<ul>
<li>Intel(R) Core(TM) i5-6600T CPU @ 2.70GHz (2 core with hyper-threading, so 4 in total)</li>
<li>7 GB + 512 MB memory</li>
</ul>
<p>But since I know this server is shared by quite a few services, I only allotted 2Gb of memory and 1
CPU core. Specifically, I inputted:</p>
<div class="sourceCode" id="cb13"><pre class="sourceCode ini"><code class="sourceCode ini"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="dt">DB Version </span><span class="ot">=</span><span class="st"> </span><span class="dv">14</span></span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a><span class="dt">OS Type </span><span class="ot">=</span><span class="st"> &quot;linux&quot;</span></span>
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a><span class="dt">DB Type </span><span class="ot">=</span><span class="st"> &quot;web&quot;</span></span>
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a><span class="dt">Total Memory (RAM) </span><span class="ot">=</span><span class="st"> &quot;2 GB&quot;</span></span>
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a><span class="dt">CPUs num </span><span class="ot">=</span><span class="st"> </span><span class="dv">1</span></span>
<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a><span class="dt">Connections num </span><span class="ot">=</span><span class="st"> </span><span class="dv">100</span></span>
<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a><span class="dt">Data Storage </span><span class="ot">=</span><span class="st"> &quot;sdd&quot;</span></span></code></pre></div>
<p>I used the web application (<code>web</code>) profile as the description matched the most.</p>
<p>This gave me:</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode ini"><code class="sourceCode ini"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="dt">max_connections </span><span class="ot">=</span><span class="st"> &quot;100&quot;;</span></span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a><span class="dt">shared_buffers </span><span class="ot">=</span><span class="st"> &quot;512MB&quot;;</span></span>
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a><span class="dt">effective_cache_size </span><span class="ot">=</span><span class="st"> &quot;1536MB&quot;;</span></span>
<span id="cb14-4"><a href="#cb14-4" aria-hidden="true" tabindex="-1"></a><span class="dt">maintenance_work_mem </span><span class="ot">=</span><span class="st"> &quot;128MB&quot;;</span></span>
<span id="cb14-5"><a href="#cb14-5" aria-hidden="true" tabindex="-1"></a><span class="dt">checkpoint_completion_target </span><span class="ot">=</span><span class="st"> &quot;0.9&quot;;</span></span>
<span id="cb14-6"><a href="#cb14-6" aria-hidden="true" tabindex="-1"></a><span class="dt">wal_buffers </span><span class="ot">=</span><span class="st"> &quot;16MB&quot;;</span></span>
<span id="cb14-7"><a href="#cb14-7" aria-hidden="true" tabindex="-1"></a><span class="dt">default_statistics_target </span><span class="ot">=</span><span class="st"> &quot;100&quot;;</span></span>
<span id="cb14-8"><a href="#cb14-8" aria-hidden="true" tabindex="-1"></a><span class="dt">random_page_cost </span><span class="ot">=</span><span class="st"> &quot;1.1&quot;;</span></span>
<span id="cb14-9"><a href="#cb14-9" aria-hidden="true" tabindex="-1"></a><span class="dt">effective_io_concurrency </span><span class="ot">=</span><span class="st"> &quot;200&quot;;</span></span>
<span id="cb14-10"><a href="#cb14-10" aria-hidden="true" tabindex="-1"></a><span class="dt">work_mem </span><span class="ot">=</span><span class="st"> &quot;2621kB&quot;;</span></span>
<span id="cb14-11"><a href="#cb14-11" aria-hidden="true" tabindex="-1"></a><span class="dt">huge_pages </span><span class="ot">=</span><span class="st"> &quot;off&quot;;</span></span>
<span id="cb14-12"><a href="#cb14-12" aria-hidden="true" tabindex="-1"></a><span class="dt">min_wal_size </span><span class="ot">=</span><span class="st"> &quot;1GB&quot;;</span></span>
<span id="cb14-13"><a href="#cb14-13" aria-hidden="true" tabindex="-1"></a><span class="dt">max_wal_size </span><span class="ot">=</span><span class="st"> &quot;4GB&quot;;</span></span></code></pre></div>
<p>That didn’t change how long the query stays cached. But that’s when I found <a href="https://www.postgresql.org/docs/current/pgprewarm.html">the extension</a> <code>pg_prewarm</code>. This allows you to manually cache a whole table into RAM!</p>
<p>Let’s try it out:</p>
<div class="sourceCode" id="cb15"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a>nextcloud<span class="op">=&gt;</span> <span class="kw">SELECT</span> pg_prewarm(<span class="st">&#39;oc_filecache&#39;</span>);</span>
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a> pg_prewarm </span>
<span id="cb15-3"><a href="#cb15-3" aria-hidden="true" tabindex="-1"></a><span class="co">------------</span></span>
<span id="cb15-4"><a href="#cb15-4" aria-hidden="true" tabindex="-1"></a>      <span class="dv">97375</span></span>
<span id="cb15-5"><a href="#cb15-5" aria-hidden="true" tabindex="-1"></a>(<span class="dv">1</span> <span class="kw">row</span>)</span></code></pre></div>
<div class="sourceCode" id="cb16"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a> Seq <span class="kw">Scan</span> <span class="kw">on</span> oc_filecache <span class="kw">file</span>  (<span class="kw">cost</span><span class="op">=</span><span class="dv">0</span>.<span class="dv">00</span><span class="op">..</span><span class="fl">138449.77</span> <span class="kw">rows</span><span class="op">=</span><span class="dv">423562</span> width<span class="op">=</span><span class="dv">211</span>) (actual <span class="dt">time</span><span class="op">=</span><span class="dv">372</span>.<span class="dv">843</span><span class="op">..</span><span class="fl">372.844</span> <span class="kw">rows</span><span class="op">=</span><span class="dv">0</span> loops<span class="op">=</span><span class="dv">1</span>)</span>
<span id="cb16-2"><a href="#cb16-2" aria-hidden="true" tabindex="-1"></a>   <span class="kw">Filter</span>: ((<span class="kw">storage</span> <span class="op">=</span> <span class="dv">2</span>) <span class="kw">AND</span> (((path):<span class="ch">:text</span> <span class="op">=</span> <span class="st">&#39;files&#39;</span>:<span class="ch">:text</span>) <span class="kw">OR</span> ((path):<span class="ch">:text</span> ~~ <span class="st">&#39;files/%&#39;</span>:<span class="ch">:text</span>)))</span>
<span id="cb16-3"><a href="#cb16-3" aria-hidden="true" tabindex="-1"></a>   <span class="kw">Rows</span> Removed <span class="kw">by</span> <span class="kw">Filter</span>: <span class="dv">2338703</span></span>
<span id="cb16-4"><a href="#cb16-4" aria-hidden="true" tabindex="-1"></a>   Buffers: <span class="kw">shared</span> hit<span class="op">=</span><span class="dv">97375</span></span>
<span id="cb16-5"><a href="#cb16-5" aria-hidden="true" tabindex="-1"></a> Planning <span class="dt">Time</span>: <span class="fl">0.286</span> ms</span>
<span id="cb16-6"><a href="#cb16-6" aria-hidden="true" tabindex="-1"></a> Execution <span class="dt">Time</span>: <span class="fl">372.879</span> ms</span>
<span id="cb16-7"><a href="#cb16-7" aria-hidden="true" tabindex="-1"></a>(<span class="dv">6</span> <span class="kw">rows</span>)</span></code></pre></div>
<p>Very promising although I’m reluctant to have this table be forced into memory by a cron job running
<code>pg_prewarm</code>. It feels heavy handed and there’s probably a reason why Postgres doesn’t provide a way
to do this out of the box. Are there any knobs I could tweak though? I’m not sure how to progress
here to be honest.</p>
<h2 id="statistical-analysis">Statistical Analysis</h2>
<p>The query above is just one query though. Is continuing to investigate this query even worth it? To
be sure, let’s enable the <code>pg_stats_statement</code> extension with:</p>
<div class="sourceCode" id="cb17"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb17-1"><a href="#cb17-1" aria-hidden="true" tabindex="-1"></a>nextcloud<span class="op">=&gt;</span> <span class="kw">CREATE</span> EXTENSION pg_stats_statement;</span></code></pre></div>
<p>We can then issue the following query that is inspired by <a href="https://www.cybertec-postgresql.com/en/pg_stat_statements-the-way-i-like-it/">this blog post</a>. I made the it so
only the query’s verb (SELECT, INSERT, …) and table name are shown. Also, I order by
<code>mean_exec_time</code>.</p>
<div class="sourceCode" id="cb18"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb18-1"><a href="#cb18-1" aria-hidden="true" tabindex="-1"></a><span class="kw">SELECT</span></span>
<span id="cb18-2"><a href="#cb18-2" aria-hidden="true" tabindex="-1"></a>  <span class="fu">regexp_replace</span>(</span>
<span id="cb18-3"><a href="#cb18-3" aria-hidden="true" tabindex="-1"></a>    <span class="fu">regexp_replace</span>(</span>
<span id="cb18-4"><a href="#cb18-4" aria-hidden="true" tabindex="-1"></a>      <span class="kw">query</span>,</span>
<span id="cb18-5"><a href="#cb18-5" aria-hidden="true" tabindex="-1"></a>      <span class="st">&#39;^([^ ]+).*FROM ([^ ]+).*&#39;</span>,</span>
<span id="cb18-6"><a href="#cb18-6" aria-hidden="true" tabindex="-1"></a>      <span class="st">&#39;\1 \2&#39;</span></span>
<span id="cb18-7"><a href="#cb18-7" aria-hidden="true" tabindex="-1"></a>    ),</span>
<span id="cb18-8"><a href="#cb18-8" aria-hidden="true" tabindex="-1"></a>    <span class="st">&#39;(UPDATE|INSERT INTO) ([^ ]+).*$&#39;</span>,</span>
<span id="cb18-9"><a href="#cb18-9" aria-hidden="true" tabindex="-1"></a>    <span class="st">&#39;\1 \2&#39;</span></span>
<span id="cb18-10"><a href="#cb18-10" aria-hidden="true" tabindex="-1"></a>  ) <span class="kw">AS</span> short_query,</span>
<span id="cb18-11"><a href="#cb18-11" aria-hidden="true" tabindex="-1"></a>  <span class="fu">round</span>(total_exec_time:<span class="ch">:numeric</span>, <span class="dv">2</span>) <span class="kw">AS</span> total_exec_time,</span>
<span id="cb18-12"><a href="#cb18-12" aria-hidden="true" tabindex="-1"></a>  calls,</span>
<span id="cb18-13"><a href="#cb18-13" aria-hidden="true" tabindex="-1"></a>  <span class="fu">round</span>(mean_exec_time:<span class="ch">:numeric</span>, <span class="dv">2</span>) <span class="kw">AS</span> mean,</span>
<span id="cb18-14"><a href="#cb18-14" aria-hidden="true" tabindex="-1"></a>  <span class="fu">round</span>((<span class="dv">100</span> <span class="op">*</span> total_exec_time <span class="op">/</span></span>
<span id="cb18-15"><a href="#cb18-15" aria-hidden="true" tabindex="-1"></a>  <span class="fu">sum</span>(total_exec_time:<span class="ch">:numeric</span>) <span class="kw">OVER</span> ()):<span class="ch">:numeric</span>, <span class="dv">2</span>) <span class="kw">AS</span> percentage_cpu</span>
<span id="cb18-16"><a href="#cb18-16" aria-hidden="true" tabindex="-1"></a><span class="kw">from</span> pg_stat_statements</span>
<span id="cb18-17"><a href="#cb18-17" aria-hidden="true" tabindex="-1"></a><span class="kw">WHERE</span> <span class="kw">query</span> <span class="kw">NOT</span> <span class="kw">LIKE</span> <span class="st">&#39;%ANALYZE%&#39;</span></span>
<span id="cb18-18"><a href="#cb18-18" aria-hidden="true" tabindex="-1"></a>  <span class="kw">AND</span> <span class="kw">query</span> <span class="kw">NOT</span> <span class="kw">LIKE</span> <span class="st">&#39;%EXPLAIN%&#39;</span></span>
<span id="cb18-19"><a href="#cb18-19" aria-hidden="true" tabindex="-1"></a>  <span class="kw">AND</span> <span class="kw">query</span> <span class="kw">NOT</span> <span class="kw">LIKE</span> <span class="st">&#39;%pg_stat_statements%&#39;</span></span>
<span id="cb18-20"><a href="#cb18-20" aria-hidden="true" tabindex="-1"></a><span class="kw">ORDER</span> <span class="kw">BY</span> mean <span class="kw">DESC</span></span>
<span id="cb18-21"><a href="#cb18-21" aria-hidden="true" tabindex="-1"></a><span class="kw">LIMIT</span> <span class="dv">20</span>;</span></code></pre></div>
<p>Before running the above query, I created the extension and used Nextcloud for about a week.</p>
<p>Times are in milliseconds. This clearly tells me that indeed this table’s worth optimizing for as
it’s taking most of the places in the top 20:</p>
<div class="sourceCode" id="cb19"><pre class="sourceCode sql"><code class="sourceCode sql"><span id="cb19-1"><a href="#cb19-1" aria-hidden="true" tabindex="-1"></a>             short_query              | total_exec_time | calls |  mean   | percentage_cpu </span>
<span id="cb19-2"><a href="#cb19-2" aria-hidden="true" tabindex="-1"></a><span class="co">--------------------------------------+-----------------+-------+---------+----------------</span></span>
<span id="cb19-3"><a href="#cb19-3" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> <span class="ot">&quot;oc_filecache&quot;</span>                |      <span class="fl">1782003.82</span> |   <span class="dv">296</span> | <span class="fl">6020.28</span> |          <span class="fl">36.60</span></span>
<span id="cb19-4"><a href="#cb19-4" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> <span class="ot">&quot;oc_filecache&quot;</span>                |         <span class="fl">4758.20</span> |     <span class="dv">2</span> | <span class="fl">2379.10</span> |           <span class="fl">0.10</span></span>
<span id="cb19-5"><a href="#cb19-5" aria-hidden="true" tabindex="-1"></a> <span class="op">&lt;</span>insufficient <span class="kw">privilege</span><span class="op">&gt;</span>             |         <span class="fl">4297.53</span> |     <span class="dv">3</span> | <span class="fl">1432.51</span> |           <span class="fl">0.09</span></span>
<span id="cb19-6"><a href="#cb19-6" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> <span class="ot">&quot;oc_filecache&quot;</span>                |        <span class="fl">13057.10</span> |    <span class="dv">10</span> | <span class="fl">1305.71</span> |           <span class="fl">0.27</span></span>
<span id="cb19-7"><a href="#cb19-7" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> oc_filecache                  |         <span class="fl">1171.26</span> |     <span class="dv">1</span> | <span class="fl">1171.26</span> |           <span class="fl">0.02</span></span>
<span id="cb19-8"><a href="#cb19-8" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> <span class="ot">&quot;oc_filecache&quot;</span>                |         <span class="fl">2088.51</span> |     <span class="dv">2</span> | <span class="fl">1044.25</span> |           <span class="fl">0.04</span></span>
<span id="cb19-9"><a href="#cb19-9" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> <span class="ot">&quot;oc_filecache&quot;</span>                |        <span class="fl">21261.02</span> |    <span class="dv">24</span> |  <span class="fl">885.88</span> |           <span class="fl">0.44</span></span>
<span id="cb19-10"><a href="#cb19-10" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> <span class="ot">&quot;oc_filecache&quot;</span>                |         <span class="fl">1905.76</span> |     <span class="dv">4</span> |  <span class="fl">476.44</span> |           <span class="fl">0.04</span></span>
<span id="cb19-11"><a href="#cb19-11" aria-hidden="true" tabindex="-1"></a> <span class="op">&lt;</span>insufficient <span class="kw">privilege</span><span class="op">&gt;</span>             |          <span class="fl">458.04</span> |     <span class="dv">1</span> |  <span class="fl">458.04</span> |           <span class="fl">0.01</span></span>
<span id="cb19-12"><a href="#cb19-12" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> <span class="ot">&quot;oc_filecache&quot;</span>                |         <span class="fl">5969.22</span> |    <span class="dv">14</span> |  <span class="fl">426.37</span> |           <span class="fl">0.12</span></span>
<span id="cb19-13"><a href="#cb19-13" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> <span class="ot">&quot;oc_systemtag_object_mapping&quot;</span> |       <span class="fl">131973.41</span> |   <span class="dv">316</span> |  <span class="fl">417.64</span> |           <span class="fl">2.71</span></span>
<span id="cb19-14"><a href="#cb19-14" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> oc_filecache                  |         <span class="fl">1035.19</span> |     <span class="dv">3</span> |  <span class="fl">345.06</span> |           <span class="fl">0.02</span></span>
<span id="cb19-15"><a href="#cb19-15" aria-hidden="true" tabindex="-1"></a> <span class="kw">DELETE</span> <span class="ot">&quot;oc_activity&quot;</span>                 |         <span class="fl">4890.14</span> |    <span class="dv">16</span> |  <span class="fl">305.63</span> |           <span class="fl">0.10</span></span>
<span id="cb19-16"><a href="#cb19-16" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> <span class="ot">&quot;oc_filecache&quot;</span>                |         <span class="fl">1345.88</span> |     <span class="dv">5</span> |  <span class="fl">269.18</span> |           <span class="fl">0.03</span></span>
<span id="cb19-17"><a href="#cb19-17" aria-hidden="true" tabindex="-1"></a> <span class="kw">UPDATE</span> <span class="ot">&quot;oc_filecache&quot;</span>                |       <span class="fl">361959.17</span> |  <span class="dv">1739</span> |  <span class="fl">208.14</span> |           <span class="fl">7.43</span></span>
<span id="cb19-18"><a href="#cb19-18" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> <span class="ot">&quot;oc_file_metadata&quot;</span>            |          <span class="fl">130.37</span> |     <span class="dv">1</span> |  <span class="fl">130.37</span> |           <span class="fl">0.00</span></span>
<span id="cb19-19"><a href="#cb19-19" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> <span class="ot">&quot;oc_file_metadata&quot;</span>            |          <span class="fl">122.03</span> |     <span class="dv">1</span> |  <span class="fl">122.03</span> |           <span class="fl">0.00</span></span>
<span id="cb19-20"><a href="#cb19-20" aria-hidden="true" tabindex="-1"></a> <span class="kw">SELECT</span> <span class="ot">&quot;oc_activity&quot;</span>                 |       <span class="fl">123446.91</span> |  <span class="dv">1247</span> |   <span class="fl">99.00</span> |           <span class="fl">2.54</span></span>
<span id="cb19-21"><a href="#cb19-21" aria-hidden="true" tabindex="-1"></a> <span class="op">&lt;</span>insufficient <span class="kw">privilege</span><span class="op">&gt;</span>             |          <span class="fl">187.31</span> |     <span class="dv">2</span> |   <span class="fl">93.65</span> |           <span class="fl">0.00</span></span>
<span id="cb19-22"><a href="#cb19-22" aria-hidden="true" tabindex="-1"></a> <span class="op">&lt;</span>insufficient <span class="kw">privilege</span><span class="op">&gt;</span>             |          <span class="fl">175.60</span> |     <span class="dv">2</span> |   <span class="fl">87.80</span> |           <span class="fl">0.00</span></span></code></pre></div>
<p>But to be honest, I don’t know how to continue here. If anyone reading this has an idea, feel free
to contact me :D</p>
<p>Let’s switch to another request, maybe there’s one that I can actually optimize.</p>
<h1 id="a-profile-thats-harder-to-optimize">A Profile That’s Harder to Optimize</h1>
<p>This is an example of request that’s harder to optimize. Indeed, it is CPU bound as we can see
because most of the time is spent cycling (in a loop):</p>
<figure>
<img src="/images/2023-12-11-investigate-nextcloud-slowness-part-2/kcachegrind_hard_to_optimize.png" alt="CPU bound request." />
<figcaption aria-hidden="true">CPU bound request.</figcaption>
</figure>
<p>We’ll skip investigating this one as reducing the time this one takes likely means modifying
Nextcloud code or from one of its dependencies.</p>
<h1 id="analyzing-a-second-interesting-profile">Analyzing a Second Interesting Profile</h1>
<p>To find another one, I just clicked in the UI until I got a request that took longer than 5 seconds.</p>
<figure>
<img src="/images/2023-12-11-investigate-nextcloud-slowness-part-2/browser_network_webdav_slow_request.png" class="zoom" alt="A slow request." />
<figcaption aria-hidden="true">A slow request.</figcaption>
</figure>
<p>Unfortunately, I found one. 12 seconds! That’s a good candidate for optimization.</p>
<p>The profile doesn’t show much, just that PHP is doing some curl request, so we’ll need to look at the callers:</p>
<figure>
<img src="/images/2023-12-11-investigate-nextcloud-slowness-part-2/kcachegrind_second_top_self.png" alt="KCacheGrind top self time." />
<figcaption aria-hidden="true">KCacheGrind top self time.</figcaption>
</figure>
<p>The caller stack is fully linear which is great news because there’s no loop involved at all.</p>
<figure>
<img src="/images/2023-12-11-investigate-nextcloud-slowness-part-2/kcachegrind_second_callers.png" class="zoom-15" alt="KCacheGrind callers." />
<figcaption aria-hidden="true">KCacheGrind callers.</figcaption>
</figure>
<p>We can see Nextcloud is asking for the preview of a file which gets forwarded to the OnlyOffice
plugin which in turns makes an HTTP call to presumably the OnlyOffice service running on my server.</p>
<p>Or is it? Actually, the third to last caller is <code>DnsPinMiddleware</code> from lines 109-155 (lines elided
in the screenshot). The related code can be seen on <a href="https://github.com/nextcloud/server/blob/03f1f1ed2ea249a75bf9c51958ea088d748dafaa/lib/private/Http/Client/DnsPinMiddleware.php#L109-L155">GitHub</a>. This is clearly some DNS resolving
code. I know I already mis-configured some things here so let’s review my configuration and see if
we can spot some mistakes.</p>
<h2 id="analyzing-dns-config">Analyzing DNS Config</h2>
<p>I did set up the integration in Nextcloud to access my OnlyOffice service at <code>onlyoffice.mydomain.com</code>:</p>
<figure>
<img src="/images/2023-12-11-investigate-nextcloud-slowness-part-2/nextcloud_onlyoffice_config.png" alt="OnlyOffice configuration." />
<figcaption aria-hidden="true">OnlyOffice configuration.</figcaption>
</figure>
<p>My OnlyOffice server is accessible at <code>onlyoffice.mydomain.com</code> (it’s not really <code>mydomain.com</code> of
course). Let’s try a <code>dig</code> (I obfuscated the actual IP address and domain name):</p>
<div class="sourceCode" id="cb20"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb20-1"><a href="#cb20-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix run nixpkgs#dig onlyoffice.mydomain.com</span>
<span id="cb20-2"><a href="#cb20-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb20-3"><a href="#cb20-3" aria-hidden="true" tabindex="-1"></a><span class="kw">;</span> <span class="op">&lt;&lt;&gt;&gt;</span> DiG <span class="ex">9.18.20</span> <span class="op">&lt;&lt;&gt;&gt;</span> onlyoffice.mydomain.com</span>
<span id="cb20-4"><a href="#cb20-4" aria-hidden="true" tabindex="-1"></a><span class="kw">;;</span> <span class="ex">global</span> options: +cmd</span>
<span id="cb20-5"><a href="#cb20-5" aria-hidden="true" tabindex="-1"></a><span class="kw">;;</span> <span class="ex">Got</span> answer:</span>
<span id="cb20-6"><a href="#cb20-6" aria-hidden="true" tabindex="-1"></a><span class="kw">;;</span> <span class="ex">-</span><span class="op">&gt;&gt;</span>HEADER<span class="op">&lt;&lt;- opcode:</span> <span class="ex">QUERY,</span> status: NOERROR, id: 7924</span>
<span id="cb20-7"><a href="#cb20-7" aria-hidden="true" tabindex="-1"></a><span class="st">;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1</span></span>
<span id="cb20-8"><a href="#cb20-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb20-9"><a href="#cb20-9" aria-hidden="true" tabindex="-1"></a><span class="st">;; OPT PSEUDOSECTION:</span></span>
<span id="cb20-10"><a href="#cb20-10" aria-hidden="true" tabindex="-1"></a><span class="st">; EDNS: version: 0, flags:; udp: 4096</span></span>
<span id="cb20-11"><a href="#cb20-11" aria-hidden="true" tabindex="-1"></a><span class="st">;; QUESTION SECTION:</span></span>
<span id="cb20-12"><a href="#cb20-12" aria-hidden="true" tabindex="-1"></a><span class="st">;onlyoffice.mydomain.com.        IN  A</span></span>
<span id="cb20-13"><a href="#cb20-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb20-14"><a href="#cb20-14" aria-hidden="true" tabindex="-1"></a><span class="st">;; ANSWER SECTION:</span></span>
<span id="cb20-15"><a href="#cb20-15" aria-hidden="true" tabindex="-1"></a><span class="st">onlyoffice.mydomain.com.  19778  IN  A  WWW.XXX.YYY.ZZZ</span></span>
<span id="cb20-16"><a href="#cb20-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb20-17"><a href="#cb20-17" aria-hidden="true" tabindex="-1"></a><span class="st">;; Query time: 0 msec</span></span>
<span id="cb20-18"><a href="#cb20-18" aria-hidden="true" tabindex="-1"></a><span class="st">;; SERVER: 192.168.1.60#53(192.168.1.60) (UDP)</span></span>
<span id="cb20-19"><a href="#cb20-19" aria-hidden="true" tabindex="-1"></a><span class="st">;; WHEN: Thu Dec 14 06:01:53 UTC 2023</span></span>
<span id="cb20-20"><a href="#cb20-20" aria-hidden="true" tabindex="-1"></a><span class="st">;; MSG SIZE  rcvd: 60</span></span></code></pre></div>
<p>I have my own <code>dnsmasq</code> server listening at <code>192.168.1.60</code> and who answered us:</p>
<div class="sourceCode" id="cb21"><pre class="sourceCode ini"><code class="sourceCode ini"><span id="cb21-1"><a href="#cb21-1" aria-hidden="true" tabindex="-1"></a><span class="co">;; SERVER: 192.168.1.60#53(192.168.1.60) (UDP)</span></span></code></pre></div>
<p>It told us that the <code>onlyoffice.mydomain.com</code> address can be found at my public IP (obfuscated here
for reasons).</p>
<div class="sourceCode" id="cb22"><pre class="sourceCode ini"><code class="sourceCode ini"><span id="cb22-1"><a href="#cb22-1" aria-hidden="true" tabindex="-1"></a><span class="dt">onlyoffice.mydomain.com.  19778  IN  A  WWW.XXX.YYY.ZZZ</span></span></code></pre></div>
<p>This is clearly not a good thing because it means my DNS server is answering with the public IP of
my router. It should instead answer with the private IP of my server.</p>
<p>This has two main implications. Firstly, it means my DNS server doesn’t know this
<code>onlyoffice.mydomain.com</code> fqdn and forwards the question to a public DNS. This is bad for
performance reasons but also for privacy reasons as it’s leaking DNS calls that should be internal
only. Also, this won’t work if my internet is disconnected.</p>
<p>Secondly, the subsequent HTTP call (which we didn’t see in the profile) will try to contact my
OnlyOffice service using my public IP address instead of the private IP directly. This means at
least more hops and so an increased response time. Actually, the router is smart and realizes the IP
I want to reach is its own public IP so the request won’t actually go out to the internet thanks to
<a href="https://en.m.wikipedia.org/wiki/Network_address_translation#NAT_hairpinning">NAT hairpinning</a>. Here it’s helping us a bit but it <a href="https://blog.tiserbox.com/posts/2023-08-12-what&#39;s-up-with-nextcloud-webdav-slowness.html#the-issue-was-seemingly-unrelated">bit me previously</a>.</p>
<p>Anyway, the correct fix here is to make my <code>dnsmasq</code> service aware of OnlyOffice and answer to the
DNS queries with the internal IP of my server which is <code>192.168.1.10</code>:</p>
<div class="sourceCode" id="cb23"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb23-1"><a href="#cb23-1" aria-hidden="true" tabindex="-1"></a>services.dnsmasq.settings.address = <span class="op">[</span></span>
<span id="cb23-2"><a href="#cb23-2" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;/onlyoffice.mydomain.com/192.168.1.10&quot;</span></span>
<span id="cb23-3"><a href="#cb23-3" aria-hidden="true" tabindex="-1"></a><span class="op">]</span>;</span></code></pre></div>
<p>This is the resulting change in the <code>dnsmasq</code> config file:</p>
<div class="sourceCode" id="cb24"><pre class="sourceCode ini"><code class="sourceCode ini"><span id="cb24-1"><a href="#cb24-1" aria-hidden="true" tabindex="-1"></a><span class="dt">address</span><span class="ot">=</span><span class="st">/onlyoffice.mydomain.com/192.168.1.10</span></span></code></pre></div>
<p>After deploying this, <code>dig</code> returns the internal IP:</p>
<div class="sourceCode" id="cb25"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb25-1"><a href="#cb25-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix run nixpkgs#dig onlyoffice.mydomain.com</span>
<span id="cb25-2"><a href="#cb25-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb25-3"><a href="#cb25-3" aria-hidden="true" tabindex="-1"></a><span class="kw">;</span> <span class="op">&lt;&lt;&gt;&gt;</span> DiG <span class="ex">9.18.20</span> <span class="op">&lt;&lt;&gt;&gt;</span> onlyoffice.mydomain.com</span>
<span id="cb25-4"><a href="#cb25-4" aria-hidden="true" tabindex="-1"></a><span class="kw">;;</span> <span class="ex">global</span> options: +cmd</span>
<span id="cb25-5"><a href="#cb25-5" aria-hidden="true" tabindex="-1"></a><span class="kw">;;</span> <span class="ex">Got</span> answer:</span>
<span id="cb25-6"><a href="#cb25-6" aria-hidden="true" tabindex="-1"></a><span class="kw">;;</span> <span class="ex">-</span><span class="op">&gt;&gt;</span>HEADER<span class="op">&lt;&lt;- opcode:</span> <span class="ex">QUERY,</span> status: NOERROR, id: 45810</span>
<span id="cb25-7"><a href="#cb25-7" aria-hidden="true" tabindex="-1"></a><span class="st">;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1</span></span>
<span id="cb25-8"><a href="#cb25-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb25-9"><a href="#cb25-9" aria-hidden="true" tabindex="-1"></a><span class="st">;; OPT PSEUDOSECTION:</span></span>
<span id="cb25-10"><a href="#cb25-10" aria-hidden="true" tabindex="-1"></a><span class="st">; EDNS: version: 0, flags:; udp: 4096</span></span>
<span id="cb25-11"><a href="#cb25-11" aria-hidden="true" tabindex="-1"></a><span class="st">;; QUESTION SECTION:</span></span>
<span id="cb25-12"><a href="#cb25-12" aria-hidden="true" tabindex="-1"></a><span class="st">;onlyoffice.mydomain.com.     IN  A</span></span>
<span id="cb25-13"><a href="#cb25-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb25-14"><a href="#cb25-14" aria-hidden="true" tabindex="-1"></a><span class="st">;; ANSWER SECTION:</span></span>
<span id="cb25-15"><a href="#cb25-15" aria-hidden="true" tabindex="-1"></a><span class="st">onlyoffice.mydomain.com.   0  IN  A  192.168.1.10</span></span>
<span id="cb25-16"><a href="#cb25-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb25-17"><a href="#cb25-17" aria-hidden="true" tabindex="-1"></a><span class="st">;; Query time: 0 msec</span></span>
<span id="cb25-18"><a href="#cb25-18" aria-hidden="true" tabindex="-1"></a><span class="st">;; SERVER: 192.168.1.60#53(192.168.1.60) (UDP)</span></span>
<span id="cb25-19"><a href="#cb25-19" aria-hidden="true" tabindex="-1"></a><span class="st">;; WHEN: Sat Dec 16 17:59:15 UTC 2023</span></span>
<span id="cb25-20"><a href="#cb25-20" aria-hidden="true" tabindex="-1"></a><span class="st">;; MSG SIZE  rcvd: 60</span></span></code></pre></div>
<p>Great, with that fixed, let’s reload the UI and look at remaining long running requests.</p>
<h1 id="remaining-optimizations">Remaining Optimizations</h1>
<p>I’m just reloading the <code>/apps/files/?dir=/</code> endpoint and there are quite a few indeed:</p>
<figure>
<img src="/images/2023-12-11-investigate-nextcloud-slowness-part-2/browser_network_profile_third.png" class="zoom" alt="More slow requests." />
<figcaption aria-hidden="true">More slow requests.</figcaption>
</figure>
<p>I see files, files, files. I bet this <code>oc_filecache</code> table has something to do with all these.
Actually, no! It’s Redis, another dependency of Nextcloud used for caching.</p>
<p>All the profiles have one thing in common, they spend a lot of time calling to Redis. Sometimes 30%
of the time as can be seen in the following table which I compiled from 4 profiles:</p>
<table>
<thead>
<tr class="header">
<th>Time (ms)</th>
<th># Calls</th>
<th>Per Call (ms)</th>
<th>% of Request</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>286</td>
<td>89</td>
<td>3.2</td>
<td>30.4</td>
</tr>
<tr class="even">
<td>273</td>
<td>86</td>
<td>3.2</td>
<td>13.7</td>
</tr>
<tr class="odd">
<td>529</td>
<td>89</td>
<td>5.9</td>
<td>25</td>
</tr>
<tr class="even">
<td>486</td>
<td>86</td>
<td>5.7</td>
<td>24.3</td>
</tr>
</tbody>
</table>
<p>3 milliseconds per call is not much, but it adds up.</p>
<p>The <a href="https://redis.io/docs/management/optimization/latency/">documentation</a> gives us a good hint as
to why Redis could be slow, it could be Redis swapping to disk. To check that, we can issue the
following command:</p>
<div class="sourceCode" id="cb26"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb26-1"><a href="#cb26-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo <span class="at">-u</span> nextcloud redis-cli <span class="at">-s</span> /run/redis-nextcloud/redis.sock info <span class="kw">|</span> <span class="fu">grep</span> process_id</span>
<span id="cb26-2"><a href="#cb26-2" aria-hidden="true" tabindex="-1"></a><span class="ex">process_id:1424342</span></span>
<span id="cb26-3"><a href="#cb26-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb26-4"><a href="#cb26-4" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> cd /proc/1424342</span>
<span id="cb26-5"><a href="#cb26-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb26-6"><a href="#cb26-6" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo cat smaps <span class="kw">|</span> <span class="fu">egrep</span> <span class="st">&#39;^(Swap|Size)&#39;</span></span>
<span id="cb26-7"><a href="#cb26-7" aria-hidden="true" tabindex="-1"></a><span class="ex">[...]</span></span>
<span id="cb26-8"><a href="#cb26-8" aria-hidden="true" tabindex="-1"></a><span class="ex">Size:</span>   364 kB</span>
<span id="cb26-9"><a href="#cb26-9" aria-hidden="true" tabindex="-1"></a><span class="ex">Swap:</span>   364 kB</span>
<span id="cb26-10"><a href="#cb26-10" aria-hidden="true" tabindex="-1"></a><span class="ex">Size:</span>   8192 kB</span>
<span id="cb26-11"><a href="#cb26-11" aria-hidden="true" tabindex="-1"></a><span class="ex">Swap:</span>   1296 kB</span>
<span id="cb26-12"><a href="#cb26-12" aria-hidden="true" tabindex="-1"></a><span class="ex">Size:</span>   24576 kB</span>
<span id="cb26-13"><a href="#cb26-13" aria-hidden="true" tabindex="-1"></a><span class="ex">Swap:</span>   19004 kB</span></code></pre></div>
<p>Indeed, a few memory maps are pretty heavily swapped. Not good, I suppose?</p>
<p>That made me think, could this be the case also for PostgreSQL? I issued the same commands as above,
using <code>systemctl cat postgresql</code> to get the PID. And Indeed, that was the case.</p>
<p>I don’t have a permanent fix against swapping but I could already test if swapping has an impact
simply by restarting Redis and PostgreSQL. I did that, re-ran the <code>smaps</code> command above, verified
nothing was swapped and reloaded the web UI. But that didn’t help, the request was still spending
the same amount of time on Redis calls and PostgreSQL calls.</p>
<h1 id="return-to-the-basics">Return to the Basics</h1>
<p>After this very disappointing optimization tour, I thought maybe I should check the overall pressure
on the system.</p>
<div class="sourceCode" id="cb27"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb27-1"><a href="#cb27-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> free <span class="at">-m</span></span>
<span id="cb27-2"><a href="#cb27-2" aria-hidden="true" tabindex="-1"></a>      <span class="ex">total</span>  used  free   shared  buff/cache  available</span>
<span id="cb27-3"><a href="#cb27-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Mem:</span>   7663  5328   209      334        2125       1703</span>
<span id="cb27-4"><a href="#cb27-4" aria-hidden="true" tabindex="-1"></a><span class="ex">Swap:</span>  7628  7271   357</span></code></pre></div>
<p>There’s 1.7Gb of available memory, so that’s good, not all RAM is used. But 7.2Gb of SWAP is used!
That’s really not good.</p>
<p>Oh well, time to understand why so much SWAP is used. I found this script in the comments of a
<a href="https://stackoverflow.com/a/7180078/1013628">StackOverflow answer</a> which prints the base command
followed by the PID and the used SWAP space.</p>
<div class="sourceCode" id="cb28"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb28-1"><a href="#cb28-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> for file in /proc/<span class="pp">*</span>/status <span class="kw">;</span> <span class="cf">do</span> <span class="dt">\</span></span>
<span id="cb28-2"><a href="#cb28-2" aria-hidden="true" tabindex="-1"></a>    <span class="fu">awk</span> <span class="st">&#39;/Tgid|VmSwap|Name/{printf $2 &quot; &quot; $3}END{ print &quot;&quot;}&#39;</span> <span class="va">$file</span><span class="kw">;</span> <span class="dt">\</span></span>
<span id="cb28-3"><a href="#cb28-3" aria-hidden="true" tabindex="-1"></a>  <span class="cf">done</span> <span class="dt">\</span></span>
<span id="cb28-4"><a href="#cb28-4" aria-hidden="true" tabindex="-1"></a>  <span class="kw">|</span> <span class="fu">grep</span> kB <span class="dt">\</span></span>
<span id="cb28-5"><a href="#cb28-5" aria-hidden="true" tabindex="-1"></a>  <span class="kw">|</span> <span class="fu">sort</span> <span class="at">-k</span> 3 <span class="at">-n</span> <span class="dt">\</span></span>
<span id="cb28-6"><a href="#cb28-6" aria-hidden="true" tabindex="-1"></a>  <span class="kw">|</span> <span class="fu">tail</span> <span class="at">-n</span> 20</span>
<span id="cb28-7"><a href="#cb28-7" aria-hidden="true" tabindex="-1"></a><span class="ex">.php-fpm-wrappe</span> 2608 50092 kB</span>
<span id="cb28-8"><a href="#cb28-8" aria-hidden="true" tabindex="-1"></a><span class="ex">.php-fpm-wrappe</span> 2626 55092 kB</span>
<span id="cb28-9"><a href="#cb28-9" aria-hidden="true" tabindex="-1"></a><span class="ex">hledger-web</span> 945 65476 kB</span>
<span id="cb28-10"><a href="#cb28-10" aria-hidden="true" tabindex="-1"></a><span class="ex">dotnet</span> 1140 69844 kB</span>
<span id="cb28-11"><a href="#cb28-11" aria-hidden="true" tabindex="-1"></a><span class="ex">piper</span> 2203 81592 kB</span>
<span id="cb28-12"><a href="#cb28-12" aria-hidden="true" tabindex="-1"></a><span class="ex">piper</span> 2212 81600 kB</span>
<span id="cb28-13"><a href="#cb28-13" aria-hidden="true" tabindex="-1"></a><span class="ex">.php-fpm-wrappe</span> 2515 102124 kB</span>
<span id="cb28-14"><a href="#cb28-14" aria-hidden="true" tabindex="-1"></a><span class="ex">.php-fpm-wrappe</span> 2344 102152 kB</span>
<span id="cb28-15"><a href="#cb28-15" aria-hidden="true" tabindex="-1"></a><span class="ex">.php-fpm-wrappe</span> 2463 103080 kB</span>
<span id="cb28-16"><a href="#cb28-16" aria-hidden="true" tabindex="-1"></a><span class="ex">.php-fpm-wrappe</span> 2577 103212 kB</span>
<span id="cb28-17"><a href="#cb28-17" aria-hidden="true" tabindex="-1"></a><span class="ex">.php-fpm-wrappe</span> 3559 103900 kB</span>
<span id="cb28-18"><a href="#cb28-18" aria-hidden="true" tabindex="-1"></a><span class="ex">.php-fpm-wrappe</span> 2359 104004 kB</span>
<span id="cb28-19"><a href="#cb28-19" aria-hidden="true" tabindex="-1"></a><span class="ex">mono</span> 1155 117608 kB</span>
<span id="cb28-20"><a href="#cb28-20" aria-hidden="true" tabindex="-1"></a><span class="ex">dotnet</span> 1200 126696 kB</span>
<span id="cb28-21"><a href="#cb28-21" aria-hidden="true" tabindex="-1"></a><span class="ex">python3.11</span> 1681 149296 kB</span>
<span id="cb28-22"><a href="#cb28-22" aria-hidden="true" tabindex="-1"></a><span class="ex">.wyoming-openwa</span> 2086 155984 kB</span>
<span id="cb28-23"><a href="#cb28-23" aria-hidden="true" tabindex="-1"></a><span class="ex">.hass-wrapped</span> 2141 239184 kB</span>
<span id="cb28-24"><a href="#cb28-24" aria-hidden="true" tabindex="-1"></a><span class="ex">jellyfin</span> 33101 266688 kB</span>
<span id="cb28-25"><a href="#cb28-25" aria-hidden="true" tabindex="-1"></a><span class="ex">.wyoming-faster</span> 2085 808604 kB</span>
<span id="cb28-26"><a href="#cb28-26" aria-hidden="true" tabindex="-1"></a><span class="ex">.wyoming-faster</span> 2083 808608 kB</span></code></pre></div>
<p>Wow there’s a lot. There’s no silver bullet to fix this but I will in the meantime stop the Home
Assistant voice pipeline (wyoming/piper) and see how free swap looks like.</p>
<p>After about one hour of waiting, the used SWAP did decrease a bit:</p>
<div class="sourceCode" id="cb29"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb29-1"><a href="#cb29-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> free <span class="at">-m</span></span>
<span id="cb29-2"><a href="#cb29-2" aria-hidden="true" tabindex="-1"></a>      <span class="ex">total</span>  used  free   shared  buff/cache  available</span>
<span id="cb29-3"><a href="#cb29-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Mem:</span>   7663  4837   132      332        2693       2196</span>
<span id="cb29-4"><a href="#cb29-4" aria-hidden="true" tabindex="-1"></a><span class="ex">Swap:</span>  7628  6030   159</span></code></pre></div>
<p>After a couple days, free SWAP is at 785.</p>
<p>I think there’s no secret anymore. I need more RAM :’(</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Deploy to NixOS With Colmena and Without Root User</title>
    <link href="https://blog.tiserbox.com//posts/2023-12-03-2-deploy-to-nix-os-without-root-user.html" />
    <id>https://blog.tiserbox.com//posts/2023-12-03-2-deploy-to-nix-os-without-root-user.html</id>
    <published>2023-12-03T00:00:00Z</published>
    <updated>2023-12-03T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on December  3, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <p>By default, you use the root user to deploy to a target machine. If you want to avoid that, you can
create another user but getting all the configuration right is not obvious. So here’s a rundown in 3 steps.</p>
<h1 id="create-user">1. Create User</h1>
<p>First, you need to create the user and make it able to run sudo without requiring a password. Let’s
pick the username <code>nixos</code>. The hammer way of doing it is allowing all commands to be ran with sudo
without password.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>users.users.nixos = <span class="op">{</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">isNormalUser</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>security.sudo.extraRules = <span class="op">[</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>  <span class="op">{</span> <span class="va">users</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;nixos&quot;</span> <span class="op">];</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>    <span class="va">commands</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>      <span class="op">{</span> <span class="va">command</span> <span class="op">=</span> <span class="st">&quot;ALL&quot;</span><span class="op">;</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>        <span class="va">options</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;NOPASSWD&quot;</span> <span class="op">];</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>      <span class="op">}</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="op">]</span>;</span></code></pre></div>
<p>If you don’t do that, you’ll stumble in the following error and the build fails right afterwards:</p>
<blockquote>
<p><code>sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper</code></p>
</blockquote>
<h1 id="user-must-be-trusted-by-nix">2. User Must be Trusted by Nix</h1>
<p>Second, you must make the user a trusted user. On the target machine’s configuration, add:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>nix.settings.trusted<span class="op">-</span>users = <span class="op">[</span> <span class="st">&quot;nixos&quot;</span> <span class="op">]</span>;</span></code></pre></div>
<p>Otherwise, although you will be able to copy derivations over to the target machine, you won’t be
allowed to talk to the nix daemon to add them to the nix store:</p>
<blockquote>
<p><code>error: cannot add path '/nix/store/00yiiplzcqzmqaw10cghbxlb4l4xibc0-i3lock-color.pam' because it lacks a signature by a trusted key</code></p>
</blockquote>
<h1 id="make-deploy-system-use-new-user">3. Make Deploy System Use New User</h1>
<p>This step will depend on which system you use. For <a href="https://colmena.cli.rs/">Colmena</a>, add the following option in the
target’s configuration:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>deployment.targetUser = <span class="st">&quot;nixos&quot;</span>;</span></code></pre></div>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Nix Log a Flake Output</title>
    <link href="https://blog.tiserbox.com//posts/2023-12-03-1-nix-log-a-flake-output.html" />
    <id>https://blog.tiserbox.com//posts/2023-12-03-1-nix-log-a-flake-output.html</id>
    <published>2023-12-03T00:00:00Z</published>
    <updated>2023-12-03T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on December  3, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <p>After running a <code>nix build</code> command and getting an error, the output usually prompts you to run nix log like so:</p>
<blockquote>
<p><code>For full logs, run 'nix log /nix/store/iyp0l0h9ik0zkmmg8ryxmb8y15a32apz-self-host-blocks-manual.drv'</code></p>
</blockquote>
<p>This is great but a bit heavy on copy/pasting. Also, if you update your code and build again, the hash will change as expected but that requires you to copy/paste again, you can’t use your terminal’s history to quickly check the logs.</p>
<p>One solution is to add <code>-L</code> (<code>--print-build-log</code>) <a href="https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix3-build#opt-print-build-logs">cli argument</a> to the <code>nix build</code> command. I
use it when the output stays small but for long running output I find it just clutters the terminal
output.</p>
<p>I just found out that you can use <code>nix log</code> on the same flake output you use in <code>nix build</code> and get to see the logs too! An example:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>nix build .<span class="co">#manualHtml</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>nix log .<span class="co">#manualHtml</span></span></code></pre></div>
<p>No copy/pasting involved!</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Flakify This Blog</title>
    <link href="https://blog.tiserbox.com//posts/2023-12-02-flakify-this-blog.html" />
    <id>https://blog.tiserbox.com//posts/2023-12-02-flakify-this-blog.html</id>
    <published>2023-12-02T00:00:00Z</published>
    <updated>2023-12-02T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on December  2, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>, <a title="All pages tagged &#39;blog&#39;." href="/tags/blog.html">blog</a>
	  
	</section>
    <section>
        <h1 id="motivation">Motivation</h1>
<p>I wanted to use flakes to build this blog since quite a while but never got to it.</p>
<p>The last nudge needed was I was fed up with the current deploy process which involved a <a href="https://github.com/ibizaman/ibizaman.github.io">second
repository</a> (see a <a href="2020-10-16-1-deploy-to-github-pages.html#actually-deploy">previous blog post</a> for more
details). It was really hindering my motivation to write blog posts. I wanted instead to deploy by
just pushing to the main branch thanks to a GitHub action, like any sane person.</p>
<h1 id="first-try">First Try</h1>
<p>In one of my other projects, that’s exactly what I did thanks to a <a href="https://github.com/ibizaman/selfhostblocks/blob/main/.github/workflows/pages.yml">GitHub action</a>, so I copied
that action and just updated the part on how to build the docs.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">name</span><span class="kw">:</span><span class="at"> Deploy blog</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="fu">on</span><span class="kw">:</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="co">  # Runs on pushes targeting the default branch</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">push</span><span class="kw">:</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">branches</span><span class="kw">:</span><span class="at"> </span><span class="kw">[</span><span class="st">&quot;main&quot;</span><span class="kw">]</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a><span class="co">  # Allows you to run this workflow manually from the Actions tab</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">workflow_dispatch</span><span class="kw">:</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a><span class="co"># Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a><span class="fu">permissions</span><span class="kw">:</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">contents</span><span class="kw">:</span><span class="at"> read</span></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">pages</span><span class="kw">:</span><span class="at"> write</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">id-token</span><span class="kw">:</span><span class="at"> write</span></span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a><span class="co"># Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.</span></span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a><span class="co"># However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.</span></span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a><span class="fu">concurrency</span><span class="kw">:</span></span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">group</span><span class="kw">:</span><span class="at"> </span><span class="st">&quot;pages&quot;</span></span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">cancel-in-progress</span><span class="kw">:</span><span class="at"> </span><span class="ch">false</span></span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a><span class="fu">jobs</span><span class="kw">:</span></span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a><span class="co">  # Single deploy job since we&#39;re just deploying</span></span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">deploy</span><span class="kw">:</span></span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">environment</span><span class="kw">:</span></span>
<span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="fu">name</span><span class="kw">:</span><span class="at"> github-pages</span></span>
<span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="fu">url</span><span class="kw">:</span><span class="at"> ${{ steps.deployment.outputs.page_url }}</span></span>
<span id="cb1-29"><a href="#cb1-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-30"><a href="#cb1-30" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">runs-on</span><span class="kw">:</span><span class="at"> ubuntu-latest</span></span>
<span id="cb1-31"><a href="#cb1-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-32"><a href="#cb1-32" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">steps</span><span class="kw">:</span></span>
<span id="cb1-33"><a href="#cb1-33" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Checkout repository</span></span>
<span id="cb1-34"><a href="#cb1-34" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> actions/checkout@v3</span></span>
<span id="cb1-35"><a href="#cb1-35" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-36"><a href="#cb1-36" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Install nix</span></span>
<span id="cb1-37"><a href="#cb1-37" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> cachix/install-nix-action@v20</span></span>
<span id="cb1-38"><a href="#cb1-38" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-39"><a href="#cb1-39" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Build docs</span></span>
<span id="cb1-40"><a href="#cb1-40" aria-hidden="true" tabindex="-1"></a><span class="fu">        run</span><span class="kw">: </span><span class="ch">|</span></span>
<span id="cb1-41"><a href="#cb1-41" aria-hidden="true" tabindex="-1"></a>          nix-build -v</span>
<span id="cb1-42"><a href="#cb1-42" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-43"><a href="#cb1-43" aria-hidden="true" tabindex="-1"></a>          ./result/bin/site build</span>
<span id="cb1-44"><a href="#cb1-44" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-45"><a href="#cb1-45" aria-hidden="true" tabindex="-1"></a>          mkdir -p ibizaman.github.io</span>
<span id="cb1-46"><a href="#cb1-46" aria-hidden="true" tabindex="-1"></a>          ./result/bin/site deploy</span>
<span id="cb1-47"><a href="#cb1-47" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-48"><a href="#cb1-48" aria-hidden="true" tabindex="-1"></a>          # see https://github.com/actions/deploy-pages/issues/58</span>
<span id="cb1-49"><a href="#cb1-49" aria-hidden="true" tabindex="-1"></a>          cp \</span>
<span id="cb1-50"><a href="#cb1-50" aria-hidden="true" tabindex="-1"></a>            --recursive \</span>
<span id="cb1-51"><a href="#cb1-51" aria-hidden="true" tabindex="-1"></a>            --dereference \</span>
<span id="cb1-52"><a href="#cb1-52" aria-hidden="true" tabindex="-1"></a>            --no-preserve=mode,ownership \</span>
<span id="cb1-53"><a href="#cb1-53" aria-hidden="true" tabindex="-1"></a>            ibizaman.github.io \</span>
<span id="cb1-54"><a href="#cb1-54" aria-hidden="true" tabindex="-1"></a>            public</span>
<span id="cb1-55"><a href="#cb1-55" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-56"><a href="#cb1-56" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Setup Pages</span></span>
<span id="cb1-57"><a href="#cb1-57" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> actions/configure-pages@v3</span></span>
<span id="cb1-58"><a href="#cb1-58" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-59"><a href="#cb1-59" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Upload artifact</span></span>
<span id="cb1-60"><a href="#cb1-60" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> actions/upload-pages-artifact@v1</span></span>
<span id="cb1-61"><a href="#cb1-61" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">with</span><span class="kw">:</span></span>
<span id="cb1-62"><a href="#cb1-62" aria-hidden="true" tabindex="-1"></a><span class="at">          </span><span class="fu">path</span><span class="kw">:</span><span class="at"> ./public</span></span>
<span id="cb1-63"><a href="#cb1-63" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-64"><a href="#cb1-64" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Deploy to GitHub Pages</span></span>
<span id="cb1-65"><a href="#cb1-65" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">id</span><span class="kw">:</span><span class="at"> deployment</span></span>
<span id="cb1-66"><a href="#cb1-66" aria-hidden="true" tabindex="-1"></a><span class="at">        </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> actions/deploy-pages@v2</span></span></code></pre></div>
<p>But <code>nix-build</code> failed with:</p>
<blockquote>
<pre><code>file &#39;nixpkgs&#39; was not found in the Nix search path
(add it using $NIX_PATH or -I)</code></pre>
</blockquote>
<p>This is because the <code>release.nix</code> file uses the bracket syntax <code>&lt;nixpkgs&gt;</code> <a href="https://github.com/ibizaman/blog/blob/4f3e3337ba82a7606fe2f5fefbc0b19ad4c4748e/release.nix#L3">to find nixpkgs</a> in
the <code>$NIX_PATH</code>.</p>
<p>Now, <code>cachix/install-nix-action</code> does <a href="https://github.com/cachix/install-nix-action#usage">have a way</a> to add <code>nixpkgs</code> to the nix path, so I
could’ve done that. Instead, I took the time to switch to flakes to avoid needing to change the
<code>$NIX_PATH</code> at all.</p>
<h1 id="use-flakes-to-build-hakyll">Use Flakes to Build Hakyll</h1>
<p>Now, I tried a few variations of a <code>flake.nix</code> file. First, copying the <code>release.nix</code> and just
adding an <code>inputs</code> section. Then, I tried <a href="https://github.com/input-output-hk/haskell.nix/blob/master/docs/tutorials/getting-started-flakes.md">the template</a> from <code>haskell.nix</code> using <code>haskell.nix</code>.
In both cases, I was recompiling a lot of Haskell packages.</p>
<p>Finally, I found <a href="https://github.com/Radvendii/hakyll-flakes">hakyll-flakes</a>. My <code>flake.nix</code> file is now:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">description</span> <span class="op">=</span> <span class="st">&quot;ibizaman&#39;s blog&quot;</span><span class="op">;</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">inputs</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>    <span class="va">nixpkgs</span>.<span class="va">url</span> <span class="op">=</span> <span class="st">&quot;github:nixos/nixpkgs&quot;</span><span class="op">;</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>    <span class="va">hakyll-flakes</span>.<span class="va">url</span> <span class="op">=</span> <span class="st">&quot;github:Radvendii/hakyll-flakes&quot;</span><span class="op">;</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>    <span class="va">flake-utils</span>.<span class="va">url</span> <span class="op">=</span> <span class="st">&quot;github:numtide/flake-utils&quot;</span><span class="op">;</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>  <span class="va">outputs</span> <span class="op">=</span> <span class="op">{</span> <span class="va">nixpkgs</span><span class="op">,</span> <span class="va">hakyll-flakes</span><span class="op">,</span> <span class="va">flake-utils</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>:</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>    <span class="kw">let</span></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>      <span class="va">supportedSystems</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;x86_64-linux&quot;</span></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;x86_64-darwin&quot;</span></span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;aarch64-linux&quot;</span></span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>        <span class="st">&quot;aarch64-darwin&quot;</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a>      <span class="op">];</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a>    <span class="kw">in</span></span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a>      flake<span class="op">-</span>utils.lib.eachSystem supportedSystems <span class="op">(</span><span class="va">system</span><span class="op">:</span></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a>        hakyll<span class="op">-</span>flakes.lib.mkAllOutputs <span class="op">{</span></span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a>          <span class="kw">inherit</span> system<span class="op">;</span></span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a>          <span class="va">name</span> <span class="op">=</span> <span class="st">&quot;site&quot;</span><span class="op">;</span></span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a>          <span class="va">src</span> <span class="op">=</span> <span class="ss">./.</span><span class="op">;</span></span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a>          <span class="va">websiteBuildInputs</span> <span class="op">=</span> <span class="kw">with</span> nixpkgs.legacyPackages.$<span class="op">{</span><span class="va">system</span><span class="op">};</span> <span class="op">[</span></span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a>            <span class="co"># rubber</span></span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a>            <span class="co"># texlive.combined.scheme-full</span></span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a>            <span class="co"># poppler_utils</span></span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a>          <span class="op">];</span></span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a>        <span class="op">});</span></span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>Building the project is done with <code>nix build .#website</code> which generates the html files under
<code>result/</code> folder:</p>
<pre><code>$ ls -1 result
about.html
archive.html
atom.xml
contact.html
css
images
index.html
posts
tags
tags.html</code></pre>
<p>Also, <code>nix run . watch</code> is very useful.</p>
<h1 id="deploy-with-github-action">Deploy with GitHub Action</h1>
<p>I just needed to update the GitHub action:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Build docs</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="fu">  run</span><span class="kw">: </span><span class="ch">|</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>    nix \</span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>      --print-build-logs \</span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>      --show-trace #\</span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>      build .#website</span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a>    # see https://github.com/actions/deploy-pages/issues/58</span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a>    cp \</span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a>      --recursive \</span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a>      --dereference \</span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a>      --no-preserve=mode,ownership \</span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a>      result \</span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a>      public</span></code></pre></div>
<p>Finally, I needed to configure the Settings &gt; Environments &gt; Configure github-pages to allow
deploying from my <code>main</code> branch.</p>
<p>The two relevant commits are <a href="https://github.com/ibizaman/blog/commit/6969b9986aeacccb6fa1bd6fac372ffec3f53c37">6969b9</a> and <a href="https://github.com/ibizaman/blog/commit/4f3e3337ba82a7606fe2f5fefbc0b19ad4c4748e">4f3e33</a>.</p>
<p>The only thing to remember now is to correctly commit the blog post, which I already managed to forget once :D</p>
<h1 id="addendum">Addendum</h1>
<p>I needed to disable the pages integration on my second - now useless - repo so that I could add the correct custom domain to deploy to to my current repo. I just went and archive that second repo altogether.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Add Patches to Nixpkgs</title>
    <link href="https://blog.tiserbox.com//posts/2023-11-26-add-patches-to-nixpkgs.html" />
    <id>https://blog.tiserbox.com//posts/2023-11-26-add-patches-to-nixpkgs.html</id>
    <published>2023-11-26T00:00:00Z</published>
    <updated>2023-11-26T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on November 26, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <p><em>edit 2024-06-15: fix snippet</em></p>
<p>Following the trail from <a href="https://discourse.nixos.org/t/support-patching-nixpkgs/2737">discourse</a> to a <a href="https://github.com/NixOS/nixpkgs/pull/59990#issuecomment-1128274552">closed GitHub pull request</a> to a comment in a
<a href="https://github.com/NixOS/nixpkgs/pull/142273#issuecomment-948225922">second GitHub pull request</a> :) I’m happy to share that we can now easily apply patches to
<code>nixpkgs</code> itself.</p>
<p>This is pretty neat because now, if you want to apply an open pull request immediately to your
project, you can easily do it. Well, the code is not particularly elegant but it’s easier than
cloning <a href="https://github.com/NixOS/nixpkgs">nixpkgs</a> and merging the PR yourself…</p>
<p>This is what applying a patch looks like now:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">system</span> <span class="op">=</span> <span class="st">&quot;x86_64-linux&quot;</span><span class="op">;</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">originPkgs</span> <span class="op">=</span> nixpkgs.legacyPackages.$<span class="op">{</span><span class="va">system</span><span class="op">};</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>  <span class="va">patches</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>    <span class="op">(</span>originPkgs.fetchpatch <span class="op">{</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>      <span class="va">url</span> <span class="op">=</span> <span class="st">&quot;https://patch-diff.githubusercontent.com/raw/NixOS/nixpkgs/pull/315018.patch&quot;</span><span class="op">;</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>      <span class="va">hash</span> <span class="op">=</span> <span class="st">&quot;sha256-8jcGyO/d+htfv/ZajxXh89S3OiDZAr7/fsWC1JpGczM=&quot;</span><span class="op">;</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>    <span class="op">})</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>  <span class="op">];</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>  <span class="va">patchedNixpkgs</span> <span class="op">=</span> originPkgs.applyPatches <span class="op">{</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>    <span class="va">name</span> <span class="op">=</span> <span class="st">&quot;nixpkgs-patched&quot;</span><span class="op">;</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a>    <span class="va">src</span> <span class="op">=</span> nixpkgs<span class="op">;</span></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a>    <span class="kw">inherit</span> patches<span class="op">;</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a><span class="kw">in</span></span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a>  <span class="op">{</span></span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a>    <span class="va">nixpkgs</span> <span class="op">=</span> <span class="bu">import</span> patchednixpkgs <span class="op">{</span> <span class="kw">inherit</span> system<span class="op">;</span> <span class="op">};</span></span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span>;</span></code></pre></div>
<p>On GitHub, to get a patch from a PR, you must go to the PR (for example
<a href="https://github.com/NixOS/nixpkgs/pull/268168">https://github.com/NixOS/nixpkgs/pull/268168</a>), edit
the URL to add a <code>.patch</code> suffix
(<a href="https://github.com/NixOS/nixpkgs/pull/268168.path">https://github.com/NixOS/nixpkgs/pull/268168.patch</a>)
and that will redirect you to the final URL (the one in the snippet above for this example).</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Switch to Colmena for Local Deploys</title>
    <link href="https://blog.tiserbox.com//posts/2023-11-08-switch-to-colmena-for-local-deploys.html" />
    <id>https://blog.tiserbox.com//posts/2023-11-08-switch-to-colmena-for-local-deploys.html</id>
    <published>2023-11-08T00:00:00Z</published>
    <updated>2023-11-08T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on November  8, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <p>The <a href="#incentive">Incentive</a> section explains why I even wanted to do this. Feel free to skip to <a href="#switching-from-nixos-rebuild-to-colmena">the second paragraph</a>.</p>
<!--toc:start-->
<ul>
<li><a href="#incentive">Incentive</a></li>
<li><a href="#switching-from-nixos-rebuild-to-colmena">Switching From Nixos-Rebuild to Colmena</a>
<!--toc:end--></li>
</ul>
<h1 id="incentive">Incentive</h1>
<p>I’ve been deploying to my two server boxes with <a href="https://colmena.cli.rs/unstable/">Colmena</a> since a
few months now and I’m very happy.</p>
<p>My laptop is using NixOS and I’ve been “deploying” to it using <code>nixos-rebuild switch</code> since the
start.</p>
<p>Recently, I wanted to mount a webdav folder from one of my servers to my laptop, to do that I
created a NixOS module (more on that in a later post) that sets up the following systemd mount:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode ini"><code class="sourceCode ini"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="dt">$ systemctl cat home-me-mymount.mount </span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="co"># /etc/systemd/system/home-me-mymount.mount</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="kw">[Unit]</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="dt">After</span><span class="ot">=</span><span class="st">network-online.target</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a><span class="dt">Description</span><span class="ot">=</span><span class="st">Webdav mount point</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a><span class="dt">Wants</span><span class="ot">=</span><span class="st">network-online.target</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a><span class="kw">[Mount]</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a><span class="dt">Options</span><span class="ot">=</span><span class="st">uid=1000</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a><span class="dt">TimeoutSec</span><span class="ot">=</span><span class="dv">15</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a><span class="dt">Type</span><span class="ot">=</span><span class="st">davfs</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="dt">What</span><span class="ot">=</span><span class="st">https://mydomain.com/webdav/mount</span></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a><span class="dt">Where</span><span class="ot">=</span><span class="st">/home/me/mymount</span></span></code></pre></div>
<p>I then tried to start the mount:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo systemctl start home-me-mymount.mount </span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="va">systemd</span><span class="op">[</span><span class="dv">1</span><span class="op">]</span><span class="ex">:</span> Mounting Webdav mount point...</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="ex">mount.davfs[1231360]:</span> davfs2 1.7.0</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="ex">mount.davfs[1231360]:</span> opening /etc/davfs2/secrets failed</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="va">systemd</span><span class="op">[</span><span class="dv">1</span><span class="op">]</span><span class="ex">:</span> home-me-mymount.mount: Mount process exited, code=exited, status=255/EXCEPTION</span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="va">systemd</span><span class="op">[</span><span class="dv">1</span><span class="op">]</span><span class="ex">:</span> home-me-mymount.mount: Failed with result <span class="st">&#39;exit-code&#39;</span>.</span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a><span class="va">systemd</span><span class="op">[</span><span class="dv">1</span><span class="op">]</span><span class="ex">:</span> Failed to mount Webdav mount point.</span></code></pre></div>
<p>Right, we need to create a file at <code>/etc/davfs2/secrets</code> with the password needed to access the
Webdav folder.</p>
<p>I obviously didn’t want to create this file by hand. Also, I want this secret to be stored encrypted and be deployed in the correct place. We’re using Nix after all and those are good practices.</p>
<p>I have been using <a href="https://github.com/Mic92/sops-nix">sops-nix</a> to deploy secrets with great success and declaring this secrets looks like so using <code>sops-nix</code>:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>sops.secrets.<span class="st">&quot;webdav&quot;</span> = <span class="op">{</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">sopsFile</span> <span class="op">=</span> <span class="ss">./secrets.yaml</span><span class="op">;</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">mode</span> <span class="op">=</span> <span class="st">&quot;0600&quot;</span><span class="op">;</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">path</span> <span class="op">=</span> <span class="st">&quot;/etc/davfs2/secrets&quot;</span><span class="op">;</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>The snippet above is only half of the job though, the remaining part is actually creating the
<code>secrets.yaml</code> and encrypting it. I’ll leave that out of this blog post as it’s not relevant here.
Also, everything is explained in the <code>sops-nix</code> repo’s readme file.</p>
<p>The issue here is we can’t use <code>nixos-rebuild</code> to deploy keys with <code>sops-nix</code> or any other key
management system. You need to switch to <code>nixops</code>, <code>colmena</code> or any other such deploy system listed
<a href="https://wiki.nixos.org/wiki/Applications#Deployment">here</a>.</p>
<h1 id="switching-from-nixos-rebuild-to-colmena">Switching From Nixos-Rebuild to Colmena</h1>
<p>Colmena allows you to use to <a href="https://colmena.cli.rs/unstable/features/apply-local.html">deploy
locally</a> and switching to it is pretty
easy in my case. This was the flake output related to my laptop before:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode diff"><code class="sourceCode diff"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="st">- nixosConfigurations.mylaptop = nixpkgs.lib.nixosSystem {</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="st">-   inherit system;</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="st">-   specialArgs = {</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="st">-     inherit nixpkgs;</span></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="st">-   };</span></span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a><span class="st">-   modules = [</span></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a><span class="st">-     inputs.home-manager.nixosModules.default</span></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a><span class="st">-     ./machines/laspin-configuration.nix</span></span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a><span class="st">-     ./machines/laspin-home.nix</span></span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a><span class="st">-   ];</span></span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a><span class="st">- };</span></span></code></pre></div>
<p>I removed those lines and created the following ones:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode diff"><code class="sourceCode diff"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>colmena = {</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>  meta = {</span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>    nixpkgs = import inputs.nixpkgs {</span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>      system = &quot;x86_64-linux&quot;;</span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>    };</span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>    specialArgs = inputs;</span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a>  };</span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a><span class="va">+  mylaptop = {</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a><span class="va">+    deployment = {</span></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a><span class="va">+      # Allow local deployment with `colmena apply-local`</span></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a><span class="va">+      allowLocalDeployment = true;</span></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a><span class="va">+</span></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a><span class="va">+      # Disable SSH deployment. This node will be skipped in a</span></span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a><span class="va">+      # normal `colmena apply`.</span></span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a><span class="va">+      targetHost = null;</span></span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a><span class="va">+    };</span></span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a><span class="va">+</span></span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a><span class="va">+    imports = [</span></span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a><span class="va">+      inputs.home-manager.nixosModules.default</span></span>
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a><span class="va">+      inputs.sops-nix.nixosModules.default</span></span>
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a><span class="va">+      ./machines/laspin-configuration.nix</span></span>
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a><span class="va">+      ./machines/laspin-home.nix</span></span>
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a><span class="va">+    ];</span></span>
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a><span class="va">+  };</span></span>
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a>};</span></code></pre></div>
<p>I just copy pasted the <code>modules</code> field to the <code>imports</code> field, added the <code>deployment</code> field to tell
colmena to deploy locally, added the <code>sops-nix</code> module because of my use case and… That was it!</p>
<p>Note that for Colmena matches the hostname with the attribute name <code>mylaptop</code>. I actually have <code>networking.hostName = "mylaptop";</code> set in my config.</p>
<p>To deploy on my laptop, I switched from:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> sudo nixos-rebuild switch</span></code></pre></div>
<p>to:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> colmena apply-local <span class="at">--sudo</span></span></code></pre></div>
<p>Which created the secret file I needed in the first place:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> readlink /etc/davfs2/secrets </span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a><span class="ex">/run/secrets/webdav</span></span></code></pre></div>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Migrate Nextcloud User To LDAP Account</title>
    <link href="https://blog.tiserbox.com//posts/2023-10-26-migrate-nextcloud-user-to-ldap-account.html" />
    <id>https://blog.tiserbox.com//posts/2023-10-26-migrate-nextcloud-user-to-ldap-account.html</id>
    <published>2023-10-26T00:00:00Z</published>
    <updated>2023-10-26T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on October 26, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: 
	  
	</section>
    <section>
        <!--toc:start-->
<ul>
<li><a href="#the-issue">The Issue</a></li>
<li><a href="#prerequisite">Prerequisite</a></li>
<li><a href="#the-cli-way-to-migrate-data">The CLI Way to Migrate Data</a></li>
<li><a href="#the-web-ui-way-to-migrate-data">The Web UI Way to Migrate Data</a></li>
<li><a href="#conclusion">Conclusion</a>
<!--toc:end--></li>
</ul>
<h1 id="the-issue">The Issue</h1>
<p>When setting up Nextcloud for the first time, I created some users. Everything was fine.</p>
<p>But then, I wanted to integrate with <a href="https://github.com/lldap/lldap">LLDAP</a> to manage the users through a LDAP server. The issue
arose from the fact that the user created from Nextcloud is not the same as the one created from the
LDAP server.</p>
<p>This means the new user has none of the data of the old user. We need to migrate data to the new user and then delete the old one.</p>
<p>For example, with a hypothetical user called “bob”:</p>
<ul>
<li>Created from Nextcloud:
<ul>
<li>Display name: <code>bob</code></li>
<li>Username: <code>bob</code></li>
<li>Path on the filesystem: <code>$nextcloud_root/data/bob</code></li>
</ul></li>
<li>Created from LLDAP:
<ul>
<li>Display name: <code>bob</code></li>
<li>Username: <code>e7749dfe-9740-440d-b857-0c0c508c6876</code></li>
<li>Path on the filesystem: <code>$nextcloud_root/data/e7749dfe-9740-440d-b857-0c0c508c6876</code></li>
</ul></li>
</ul>
<p>As you can see, the username ends up being a UUID. For Nextcloud, everything about these two users
is different.</p>
<p>You can see the display name and username of a user by going to the <code>/settings/users</code> endpoint.</p>
<h1 id="prerequisite">Prerequisite</h1>
<p>Before continuing on and (spoiler) migrating data from one user to the other, you first need to have
created both users on Nextcloud. I assume the old Nextcloud user was already created, otherwise you
have no reason to read this post. But the new LDAP user is probably not yet created.</p>
<p>To do that, you have to login into Nextcloud with that new LDAP user. Nextcloud is smart enough to
handle having two users with the same display name but two different username like in the example
above.</p>
<p>To connect as the old bob user, just use <code>bob</code> and the old password. To connect as the new bob user,
use <code>bob</code> and the new password.</p>
<h1 id="the-cli-way-to-migrate-data">The CLI Way to Migrate Data</h1>
<p>CLI transfer ownership is done with the following command:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="ex">occ</span> files:transfer-ownership <span class="dt">\</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="pp">[</span><span class="ss">options</span><span class="pp">]</span> <span class="pp">[--]</span> <span class="dt">\</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>  <span class="op">&lt;</span>source-user<span class="op">&gt;</span> <span class="op">&lt;</span>destination-user<span class="op">&gt;</span></span></code></pre></div>
<p>Running <code>occ files:transfer-ownership --help</code> tells us:</p>
<blockquote>
<p>All files and folders are moved to another user - outgoing shares and incoming user file shares
(optionally) are moved as well.</p>
</blockquote>
<p>There’s a <code>--move</code> option but I could never use it successfully. I had assume without setting this option that the folders would be somehow copied but no, they are effectively transferred. Every time I tried to use it, I get the following error:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">Destination</span> path does not exists or is not empty</span></code></pre></div>
<p>Anyway, with our hypothetical “bob” user, the command becomes:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="ex">occ</span> files:transfer-ownership <span class="dt">\</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  bob e7749dfe-9740-440d-b857-0c0c508c6876</span></code></pre></div>
<p>Note the ownership transfer is executed right away.</p>
<h1 id="the-web-ui-way-to-migrate-data">The Web UI Way to Migrate Data</h1>
<p>Here, we go in the UI to transfer ownership of folders. Later, the nextcloud cron job will take care
of actually moving the folders.</p>
<ol type="1">
<li><p>In the old account you want to move files from, go to <code>Settings &gt; Sharing</code></p></li>
<li><p>In the <code>Files</code> section, click on <code>Choose file or folder to transfer</code>.</p>
<p>Since I had more than 10 folders to transfer, I tried playing it smart and selecting “no” folder, assuming this would transfer all the folders. But no, although the UI lets you do that, the cron job fails later on.</p>
<p>The trick here was just to move all folders I wanted to transfer into a new folder and transfer
that folder.</p></li>
<li><p>Choose the new owner.</p></li>
<li><p>Click on “Transfer”.</p></li>
<li><p>Log out then log in with the new user and accept the ownership transfer.</p></li>
<li><p>Wait for the cron job to kick in.</p></li>
</ol>
<p>This schedules the transfer through the nextcloud cron job. So to see if it works, you’ll need to
monitor that systemd service. In my case, it failed because the job couldn’t access the <code>perl</code>
binary. I fixed that by <a href="https://github.com/ibizaman/selfhostblocks/commit/cb7fb66ee2ce1390846ea2e338a8195c5168c4a2">making it available to the cron job</a>.</p>
<p>To avoid waiting for the cron job to kick in on its own, just start it manually with <code>systemctl start nextcloud-cron.service</code>.</p>
<h1 id="conclusion">Conclusion</h1>
<p>Now you can transfer all data from legacy accounts to new LDAP managed accounts using the CLI or the
Web UI. Of course, this can be used to transfer from any user to any user.</p>
<p>Remember to test the transfer by monitoring the nextcloud cron job if you’re using the Web UI way.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>What's up with Nextcloud webdav slowness?</title>
    <link href="https://blog.tiserbox.com//posts/2023-08-12-what%27s-up-with-nextcloud-webdav-slowness.html" />
    <id>https://blog.tiserbox.com//posts/2023-08-12-what%27s-up-with-nextcloud-webdav-slowness.html</id>
    <published>2023-08-12T00:00:00Z</published>
    <updated>2023-08-12T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on August 12, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nextcloud&#39;." href="/tags/nextcloud.html">nextcloud</a>, <a title="All pages tagged &#39;performance&#39;." href="/tags/performance.html">performance</a>
	  
	</section>
    <section>
        <p>Accessing Nextcloud’s web UI, using http, seems reasonably fast but accessing the webdav interface
is painfully slow. Like, it doesn’t even seem to work at all slow.</p>
<p>What’s up with that?</p>
<p>Let’s embark in a journey on how to profile Nextcloud.</p>
<h2 id="did-anyone-stumbled-onto-this-issue-already">Did anyone stumbled onto this issue already?</h2>
<p>Searching for slow Nextcloud webdav brought up <a href="1">this post</a>. It details what the OP tried to fix
their issue but with a not satisfying conclusion. Anyway, they provide very useful curl commands to
test timing:</p>
<p>Http timing:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="bu">time</span> curl <span class="dt">\</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="at">-H</span> <span class="st">&#39;Cookie:$SESSION&#39;</span> <span class="dt">\</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>  <span class="st">&#39;https://$MYINSTANCE/apps/dashboard/#/&#39;</span></span></code></pre></div>
<p><code>$MYINSTANCE</code> would for example be <code>nextcloud.domain.com</code>, which I’m going to use from now on.</p>
<p>Webdav timing:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="bu">time</span> curl <span class="dt">\</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="at">-X</span> <span class="st">&#39;PROPFIND&#39;</span> <span class="dt">\</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  <span class="at">-H</span> <span class="st">&#39;Depth: 1&#39;</span> <span class="dt">\</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>  <span class="at">-u</span> <span class="st">&#39;$USER:$PASSWORD&#39;</span> <span class="dt">\</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>  <span class="st">&#39;https://$MYINSTANCE/remote.php/dav/files/$USER/&#39;</span></span></code></pre></div>
<p>I advice instead to leave out the <code>$PASSWORD</code> as curl will ask for it. Otherwise, the password will
appear at least in your shell history.</p>
<h2 id="try-to-replicate">Try to replicate</h2>
<p>I tweaked the above commands by adding the <code>-I</code> flag so I could see the headers but not the
response:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="bu">time</span> curl <span class="at">-I</span> ...</span></code></pre></div>
<p>Http timing is not bad:</p>
<pre><code>$ 
real    0m0.464s
user    0m0.086s
sys     0m0.012s</code></pre>
<p>Webdav timing is abysmal:</p>
<pre><code>real    0m26.931s
user    0m0.095s
sys     0m0.011s</code></pre>
<h2 id="is-the-issue-in-the-nextcloud-code-or-elsewhere">Is the issue in the Nextcloud code or elsewhere?</h2>
<p>I need to know first if the issue lies in the Nextcloud php code, in the phpfpm server serving the
php code or in the nginx proxy that’s serving Nextcloud publicly.</p>
<p>To know that, I enabled the access log in nginx with the following snippet. The convoluted string
actually produces a valid json format which makes it super easy to investigate.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>services.nginx.logError = <span class="st">&quot;stderr warn&quot;</span>;</span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>services.nginx.appendHttpConfig = <span class="st">&#39;&#39;</span></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="st">  log_format apm</span></span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;{&#39;</span></span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;remote_addr&quot;:&quot;$remote_addr&quot;,&#39;</span></span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;remote_user&quot;:&quot;$remote_user&quot;,&#39;</span></span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;time_local&quot;:&quot;$time_local&quot;,&#39;</span></span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;request&quot;:&quot;$request&quot;,&#39;</span></span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;request_length&quot;:&quot;$request_length&quot;,&#39;</span></span>
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;server_name&quot;:&quot;$server_name&quot;,&#39;</span></span>
<span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;status&quot;:&quot;$status&quot;,&#39;</span></span>
<span id="cb6-12"><a href="#cb6-12" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;bytes_sent&quot;:&quot;$bytes_sent&quot;,&#39;</span></span>
<span id="cb6-13"><a href="#cb6-13" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;body_bytes_sent&quot;:&quot;$body_bytes_sent&quot;,&#39;</span></span>
<span id="cb6-14"><a href="#cb6-14" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;referrer&quot;:&quot;$http_referrer&quot;,&#39;</span></span>
<span id="cb6-15"><a href="#cb6-15" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;user_agent&quot;:&quot;$http_user_agent&quot;,&#39;</span></span>
<span id="cb6-16"><a href="#cb6-16" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;gzip_ration&quot;:&quot;$gzip_ratio&quot;,&#39;</span></span>
<span id="cb6-17"><a href="#cb6-17" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;post&quot;:&quot;$request_body&quot;,&#39;</span></span>
<span id="cb6-18"><a href="#cb6-18" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;upstream_addr&quot;:&quot;$upstream_addr&quot;,&#39;</span></span>
<span id="cb6-19"><a href="#cb6-19" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;upstream_status&quot;:&quot;$upstream_status&quot;,&#39;</span></span>
<span id="cb6-20"><a href="#cb6-20" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;request_time&quot;:&quot;$request_time&quot;,&#39;</span></span>
<span id="cb6-21"><a href="#cb6-21" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;upstream_response_time&quot;:&quot;$upstream_response_time&quot;,&#39;</span></span>
<span id="cb6-22"><a href="#cb6-22" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;upstream_connect_time&quot;:&quot;$upstream_connect_time&quot;,&#39;</span></span>
<span id="cb6-23"><a href="#cb6-23" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;&quot;upstream_header_time&quot;:&quot;$upstream_header_time&quot;&#39;</span></span>
<span id="cb6-24"><a href="#cb6-24" aria-hidden="true" tabindex="-1"></a><span class="st">    &#39;}&#39;;</span></span>
<span id="cb6-25"><a href="#cb6-25" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-26"><a href="#cb6-26" aria-hidden="true" tabindex="-1"></a><span class="st">  access_log syslog:server=unix:/dev/log apm;</span></span>
<span id="cb6-27"><a href="#cb6-27" aria-hidden="true" tabindex="-1"></a><span class="st">  &#39;&#39;</span>;</span></code></pre></div>
<p>Take a look in the <a href="https://nginx.org/en/docs/http/ngx_http_upstream_module.html#variables">nginx
manual</a> to see what the
<code>upstream_*</code> variables mean.</p>
<p>This is what I see for the <code>curl</code> request above. I formatted the json output for easier reading.</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="er">Sep</span> <span class="er">17</span> <span class="er">21:08:45</span> <span class="er">myserver</span> <span class="er">nginx</span><span class="ot">[</span><span class="dv">287808</span><span class="ot">]</span><span class="er">:</span> <span class="er">myserver</span> <span class="er">nginx:</span> </span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a><span class="fu">{</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;remote_addr&quot;</span><span class="fu">:</span> <span class="st">&quot;$ADDR&quot;</span><span class="fu">,</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;remote_user&quot;</span><span class="fu">:</span> <span class="st">&quot;$USER&quot;</span><span class="fu">,</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;time_local&quot;</span><span class="fu">:</span> <span class="st">&quot;17/Sep/2023:21:08:45 +0000&quot;</span><span class="fu">,</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;request&quot;</span><span class="fu">:</span> <span class="st">&quot;PROPFIND https://nextcloud.domain.com/remote.php/dav/files/$USER/ HTTP/2.0&quot;</span><span class="fu">,</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;request_length&quot;</span><span class="fu">:</span> <span class="st">&quot;185&quot;</span><span class="fu">,</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;server_name&quot;</span><span class="fu">:</span> <span class="st">&quot;$INSTANCE&quot;</span><span class="fu">,</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;status&quot;</span><span class="fu">:</span> <span class="st">&quot;207&quot;</span><span class="fu">,</span></span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;bytes_sent&quot;</span><span class="fu">:</span> <span class="st">&quot;1401&quot;</span><span class="fu">,</span></span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;body_bytes_sent&quot;</span><span class="fu">:</span> <span class="st">&quot;633&quot;</span><span class="fu">,</span></span>
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;referrer&quot;</span><span class="fu">:</span> <span class="st">&quot;-&quot;</span><span class="fu">,</span></span>
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;user_agent&quot;</span><span class="fu">:</span> <span class="st">&quot;curl/8.2.1&quot;</span><span class="fu">,</span></span>
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;gzip_ration&quot;</span><span class="fu">:</span> <span class="st">&quot;-&quot;</span><span class="fu">,</span></span>
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;post&quot;</span><span class="fu">:</span> <span class="st">&quot;-&quot;</span><span class="fu">,</span></span>
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;upstream_addr&quot;</span><span class="fu">:</span> <span class="st">&quot;unix:/run/phpfpm/nextcloud.sock&quot;</span><span class="fu">,</span></span>
<span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;upstream_status&quot;</span><span class="fu">:</span> <span class="st">&quot;207&quot;</span><span class="fu">,</span></span>
<span id="cb7-18"><a href="#cb7-18" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;request_time&quot;</span><span class="fu">:</span> <span class="st">&quot;26.058&quot;</span><span class="fu">,</span></span>
<span id="cb7-19"><a href="#cb7-19" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;upstream_response_time&quot;</span><span class="fu">:</span> <span class="st">&quot;26.057&quot;</span><span class="fu">,</span></span>
<span id="cb7-20"><a href="#cb7-20" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;upstream_connect_time&quot;</span><span class="fu">:</span> <span class="st">&quot;0.000&quot;</span><span class="fu">,</span></span>
<span id="cb7-21"><a href="#cb7-21" aria-hidden="true" tabindex="-1"></a>  <span class="dt">&quot;upstream_header_time&quot;</span><span class="fu">:</span> <span class="st">&quot;26.057&quot;</span></span>
<span id="cb7-22"><a href="#cb7-22" aria-hidden="true" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div>
<p>This indicates the problem lies in the Nextcloud code as the time spent in the “upstream” part is
the same as the total request time.</p>
<h2 id="lets-profile-nextcloud">Let’s profile Nextcloud</h2>
<p>We need to enable the xdebug extension and add some lines to the php.ini config. In NixOS, that’s done quite easily by adding:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a>services.nextcloud = <span class="op">{</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>  <span class="co"># Disable the minifier and outputs some additional</span></span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a>  <span class="co"># debug information.</span></span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">extraOptions</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;</span>debug<span class="st">&quot;</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;filelocking.debug&quot;</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a>  <span class="co"># Enable profiling xdebug mode and save file only</span></span>
<span id="cb8-10"><a href="#cb8-10" aria-hidden="true" tabindex="-1"></a>  <span class="co"># if the trigger_value is given in the request.</span></span>
<span id="cb8-11"><a href="#cb8-11" aria-hidden="true" tabindex="-1"></a>  <span class="va">phpOptions</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb8-12"><a href="#cb8-12" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;</span>xdebug.mode<span class="st">&quot;</span> <span class="op">=</span> <span class="st">&quot;profile&quot;</span><span class="op">;</span></span>
<span id="cb8-13"><a href="#cb8-13" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;xdebug.trigger_value&quot;</span> <span class="op">=</span> <span class="st">&quot;debug_me&quot;</span><span class="op">;</span></span>
<span id="cb8-14"><a href="#cb8-14" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;xdebug.output_dir&quot;</span> <span class="op">=</span> <span class="st">&quot;/var/log/xdebug&quot;</span><span class="op">;</span></span>
<span id="cb8-15"><a href="#cb8-15" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;xdebug.start_with_request&quot;</span> <span class="op">=</span> <span class="st">&quot;trigger&quot;</span><span class="op">;</span></span>
<span id="cb8-16"><a href="#cb8-16" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb8-17"><a href="#cb8-17" aria-hidden="true" tabindex="-1"></a>  </span>
<span id="cb8-18"><a href="#cb8-18" aria-hidden="true" tabindex="-1"></a>  <span class="co"># Adds xdebug extension</span></span>
<span id="cb8-19"><a href="#cb8-19" aria-hidden="true" tabindex="-1"></a>  <span class="va">phpExtraExtensions</span> <span class="op">=</span> <span class="va">all</span><span class="op">:</span> <span class="op">[</span> all.xdebug <span class="op">];</span></span>
<span id="cb8-20"><a href="#cb8-20" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span>
<span id="cb8-21"><a href="#cb8-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-22"><a href="#cb8-22" aria-hidden="true" tabindex="-1"></a><span class="co"># Create output folder with correct permission.</span></span>
<span id="cb8-23"><a href="#cb8-23" aria-hidden="true" tabindex="-1"></a>systemd.services.phpfpm<span class="op">-</span>nextcloud.preStart = <span class="st">&#39;&#39;</span></span>
<span id="cb8-24"><a href="#cb8-24" aria-hidden="true" tabindex="-1"></a><span class="st">  mkdir -p /var/log/xdebug</span></span>
<span id="cb8-25"><a href="#cb8-25" aria-hidden="true" tabindex="-1"></a><span class="st">  chown -R nextcloud: /var/log/xdebug</span></span>
<span id="cb8-26"><a href="#cb8-26" aria-hidden="true" tabindex="-1"></a><span class="st">&#39;&#39;</span>;</span></code></pre></div>
<p>This will create a <code>cachegrind.out.XXXXXX</code> file under the <code>/var/log/xdebug</code> directory for each
request having the cookie <code>XDEBUG_PROFILE=debug_me</code> set.</p>
<p>You could leave out the <code>start_with_request</code> option but I wouldn’t advice doing that as normal usage
of the Nextcloud instance will also produce a lot of <code>cachegrind.out.XXXXXX</code> files which are just
noise.</p>
<p>Anyway, the full <code>curl</code> request to time webdav becomes:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="bu">time</span> curl <span class="at">-I</span> <span class="at">-s</span> <span class="dt">\</span></span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a>  <span class="at">-X</span> <span class="st">&#39;PROPFIND&#39;</span> <span class="dt">\</span></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a>  <span class="at">-H</span> <span class="st">&#39;Depth: 1&#39;</span> <span class="dt">\</span></span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a>  <span class="at">-u</span> <span class="st">&#39;$USER&#39;</span> <span class="dt">\</span></span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a>  <span class="at">--cookie</span> <span class="st">&#39;XDEBUG_PROFILE=debug_me&#39;</span> <span class="dt">\</span></span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a>  <span class="st">&#39;https://$MYINSTANCE/remote.php/dav/files/$USER/&#39;</span></span></code></pre></div>
<p>This is the full output of the response:</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="ex">Enter</span> host password for user <span class="st">&#39;$USER&#39;</span>:</span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="ex">HTTP/2</span> 207 </span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a><span class="ex">server:</span> nginx</span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a><span class="ex">date:</span> Sun, 17 Sep 2023 21:08:45 GMT</span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a><span class="ex">content-type:</span> application/xml<span class="kw">;</span> <span class="va">charset</span><span class="op">=</span>utf-8</span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a><span class="ex">x-xdebug-profile-filename:</span> /var/log/xdebug/cachegrind.out.552470</span>
<span id="cb10-7"><a href="#cb10-7" aria-hidden="true" tabindex="-1"></a><span class="ex">set-cookie:</span> oc_sessionPassphrase=XYZ<span class="kw">;</span> <span class="va">path</span><span class="op">=</span>/<span class="kw">;</span> <span class="ex">secure</span><span class="kw">;</span> <span class="ex">HttpOnly</span><span class="kw">;</span> <span class="va">SameSite</span><span class="op">=</span>Lax</span>
<span id="cb10-8"><a href="#cb10-8" aria-hidden="true" tabindex="-1"></a><span class="ex">content-security-policy:</span> default-src <span class="st">&#39;none&#39;</span><span class="kw">;</span></span>
<span id="cb10-9"><a href="#cb10-9" aria-hidden="true" tabindex="-1"></a><span class="ex">expires:</span> Thu, 19 Nov 1981 08:52:00 GMT</span>
<span id="cb10-10"><a href="#cb10-10" aria-hidden="true" tabindex="-1"></a><span class="ex">cache-control:</span> no-store, no-cache, must-revalidate</span>
<span id="cb10-11"><a href="#cb10-11" aria-hidden="true" tabindex="-1"></a><span class="ex">pragma:</span> no-cache</span>
<span id="cb10-12"><a href="#cb10-12" aria-hidden="true" tabindex="-1"></a><span class="ex">set-cookie:</span> occXYZ=XYZ<span class="kw">;</span> <span class="va">path</span><span class="op">=</span>/<span class="kw">;</span> <span class="ex">secure</span><span class="kw">;</span> <span class="ex">HttpOnly</span><span class="kw">;</span> <span class="va">SameSite</span><span class="op">=</span>Lax</span>
<span id="cb10-13"><a href="#cb10-13" aria-hidden="true" tabindex="-1"></a><span class="ex">set-cookie:</span> cookie_test=test<span class="kw">;</span> <span class="va">expires</span><span class="op">=</span>Sun, <span class="ex">17</span> Sep 2023 22:08:44 GMT<span class="kw">;</span> <span class="ex">Max-Age=3600</span></span>
<span id="cb10-14"><a href="#cb10-14" aria-hidden="true" tabindex="-1"></a><span class="ex">vary:</span> Brief,Prefer</span>
<span id="cb10-15"><a href="#cb10-15" aria-hidden="true" tabindex="-1"></a><span class="ex">dav:</span> 1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, nextcloud-checksum-update, nc-calendar-search, nc-enable-birthday-calendar</span>
<span id="cb10-16"><a href="#cb10-16" aria-hidden="true" tabindex="-1"></a><span class="ex">x-request-id:</span> 9Yjd8BK6bGf92s7qXpWW</span>
<span id="cb10-17"><a href="#cb10-17" aria-hidden="true" tabindex="-1"></a><span class="ex">x-debug-token:</span> 9Yjd8BK6bGf92s7qXpWW</span>
<span id="cb10-18"><a href="#cb10-18" aria-hidden="true" tabindex="-1"></a><span class="ex">strict-transport-security:</span> max-age=31536000<span class="kw">;</span> <span class="ex">includeSubDomains</span></span>
<span id="cb10-19"><a href="#cb10-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-20"><a href="#cb10-20" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb10-21"><a href="#cb10-21" aria-hidden="true" tabindex="-1"></a><span class="ex">real</span>    0m28.410s</span>
<span id="cb10-22"><a href="#cb10-22" aria-hidden="true" tabindex="-1"></a><span class="ex">user</span>    0m0.050s</span>
<span id="cb10-23"><a href="#cb10-23" aria-hidden="true" tabindex="-1"></a><span class="ex">sys</span>     0m0.007s</span></code></pre></div>
<p>Again that abysmal execution time. Here, we’re interested in the <code>x-xdebug-profile-filename</code>
response header which shows us the corresponding profiling file.</p>
<p>I will open that file in KCacheGrind. I downloaded that file on my laptop then started
KCacheGrind with <code>nix run nixpkgs#kcachegrind</code>.</p>
<p>Here is what I saw, and my jaw dropped (open image in a new tab to be able to zoom):</p>
<p><img src="/images/2023-08-12-what's-up-with-nextcloud-webdav-slowness/KCacheGrind-output.png" /></p>
<p>I’m not a KCacheGrind expert but let me give you a primer. Each line shows:</p>
<ul>
<li>a function call in the <code>Function</code> column,</li>
<li>how many times that function got called in the <code>Called</code> column,</li>
<li>how long was spent somewhere inside that function and any of its descendants in the <code>Incl</code> column,</li>
<li>and how long was spent somewhere inside that function and <em>none</em> of its descendants in the <code>Self</code> column.</li>
</ul>
<p>Here we can see the overwhelming majority of the time is spent inside 1 call to the <code>php::usleep</code>
function which is called once by the <code>OC\Security\Bruteforce\Throttler-&gt;getDelay</code> function.</p>
<p>All the slowness was thus due to Nextcloud’s <a href="https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/bruteforce_configuration.html">bruteforce avoidance
feature</a>.
Somehow it thought someone was trying to bruteforce access to the server. But why did it activate at
all as I’m the only user?</p>
<h2 id="the-issue-was-seemingly-unrelated">The issue was seemingly unrelated</h2>
<p>Looking back in the logs I could see that the <code>remote_addr</code> of the request was pointing to my router
instance and not to the user’s real IP address.</p>
<p>Thanks to <a href="https://help.nextcloud.com/t/tutorial-on-profiling-nextcloud/170348/2?u=ibizaman"><span class="citation" data-cites="bb77">@bb77</span></a>
over in the Nextcloud forum, I learned my issue was due to <a href="https://en.wikipedia.org/wiki/Network_address_translation#NAT_hairpinning">NAT
hairpinning</a>. This
allows, I quote wikipedia, “a machine on the LAN is able to access another machine on the LAN via
the external IP address of the LAN/router”.</p>
<p>This is all great but this means the Nextcloud server was seeing all internal requests as coming
from the same IP address, the one of my router, leading to more bruteforce mitigation than
necessary.</p>
<p>I circumvented by using split DNS by adding <code>nextcloud.domain.com</code> to my internal dns server with:</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a>services.dnsmasq = <span class="op">{</span></span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">extraConfig</span> <span class="op">=</span> <span class="st">&#39;&#39;</span></span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a><span class="st">    address=/$MYISNTANCE/192.168.1.10</span></span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a><span class="st">  &#39;&#39;</span><span class="op">;</span></span></code></pre></div>
<p>Where <code>192.168.1.10</code> is the internal IP of the server hosting Nextcloud.</p>
<p>Now, the webdav request finishes in <code>8.188s</code> and the <code>remote_addr</code> shows correctly the one from my
laptop. It’s still slow and more profiling showed the issue lies now in ldap calls. But I’ll leave
that for a later blog post.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Backup Home-Assistant with and without Nix</title>
    <link href="https://blog.tiserbox.com//posts/2023-07-02-backup-home-assistant-with-and-without-nix.html" />
    <id>https://blog.tiserbox.com//posts/2023-07-02-backup-home-assistant-with-and-without-nix.html</id>
    <published>2023-07-02T00:00:00Z</published>
    <updated>2023-07-02T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on July  2, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>, <a title="All pages tagged &#39;server&#39;." href="/tags/server.html">server</a>
	  
	</section>
    <section>
        <p>This post will show one way to backup Home Assistant automatically. I’ll use Nix to set things up
but I will also show the resulting configuration files so you can follow along even if you do not
use Nix.</p>
<p>The idea is to have home assistant itself create a backup using an automation that is scheduled to
run on a regular basis. Then, you can use whatever method to store that backup in a secure location.</p>
<h1 id="backup-automation">Backup Automation</h1>
<p>Create the following shell command (excerpt from <code>configuration.nix</code>):</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">shell_command</span><span class="kw">:</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">delete_backups</span><span class="kw">:</span><span class="at"> find /var/lib/hass/backups -type f -delete</span></span></code></pre></div>
<p>And create the following automation:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="fu">alias</span><span class="kw">:</span><span class="at"> Create Backup on Schedule</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="fu">mode</span><span class="kw">:</span><span class="at"> single</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="fu">trigger</span><span class="kw">:</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="kw">-</span><span class="at"> </span><span class="fu">minutes</span><span class="kw">:</span><span class="at"> </span><span class="st">&#39;5&#39;</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">platform</span><span class="kw">:</span><span class="at"> time_pattern</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="fu">action</span><span class="kw">:</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a><span class="kw">-</span><span class="at"> </span><span class="fu">data</span><span class="kw">:</span><span class="at"> </span><span class="kw">{}</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">service</span><span class="kw">:</span><span class="at"> shell_command.delete_backups</span></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="kw">-</span><span class="at"> </span><span class="fu">data</span><span class="kw">:</span><span class="at"> </span><span class="kw">{}</span></span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="fu">service</span><span class="kw">:</span><span class="at"> backup.create</span></span></code></pre></div>
<p>This script will run on the 5th minute of every hour to 1/ delete old backups 2/ create a new backup. The backup will live inside the Home Assistant’s <code>backup/</code> folder.</p>
<p>In Nix, you would do:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>services.home<span class="op">-</span>assistant = <span class="op">{</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">config</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;</span>automation manual<span class="st">&quot;</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>      <span class="op">{</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>        <span class="va">alias</span> <span class="op">=</span> <span class="st">&quot;Create Backup on Schedule&quot;</span><span class="op">;</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>        <span class="va">trigger</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>          <span class="op">{</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>            <span class="va">platform</span> <span class="op">=</span> <span class="st">&quot;time_pattern&quot;</span><span class="op">;</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>            <span class="va">minutes</span> <span class="op">=</span> <span class="st">&quot;5&quot;</span><span class="op">;</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>          <span class="op">}</span></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>        <span class="op">];</span></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>        <span class="va">action</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>          <span class="op">{</span></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>            <span class="va">service</span> <span class="op">=</span> <span class="st">&quot;shell_command.delete_backups&quot;</span><span class="op">;</span></span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>            <span class="va">data</span> <span class="op">=</span> <span class="op">{};</span></span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>          <span class="op">}</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a>          <span class="op">{</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a>            <span class="va">service</span> <span class="op">=</span> <span class="st">&quot;backup.create&quot;</span><span class="op">;</span></span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a>            <span class="va">data</span> <span class="op">=</span> <span class="op">{};</span></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a>          <span class="op">}</span></span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a>        <span class="op">];</span></span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a>        <span class="va">mode</span> <span class="op">=</span> <span class="st">&quot;single&quot;</span><span class="op">;</span></span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a>      <span class="op">}</span></span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a>    <span class="va">shell_command</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a>      <span class="va">delete_backups</span> <span class="op">=</span> <span class="st">&quot;find </span><span class="sc">${</span>config.services.home<span class="op">-</span>assistant.configDir<span class="sc">}</span><span class="st">/backups -type f -delete&quot;</span><span class="op">;</span></span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>Note that I <a href="https://wiki.nixos.org/wiki/Home_Assistant#Combine_declarative_and_UI_defined_automations">combine declarative and UI defined automations</a>.</p>
<h1 id="allow-backup-user-to-access-home-assistant-backup-folder">Allow backup user to access Home Assistant backup folder</h1>
<p>Create a <code>backup</code> user and make it member of the Home Assistant group:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="ex">useradd</span> backup <span class="at">--system</span> <span class="at">--groups</span> hass</span></code></pre></div>
<p>In nix:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>users.groups.hass = <span class="op">{</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">members</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;backup&quot;</span> <span class="op">];</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>Now, we need to ensure the <code>backup</code> user has access to the Home Assistant folder by enabling the “read” and “execute” group bits. First, by editing the Home Assistant service config and updating the UMask settings:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="ex">systemctl</span> edit home-assistant.service</span></code></pre></div>
<p>Then enter:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="va">UMask</span><span class="op">=</span>0027</span></code></pre></div>
<p>Finally, restart the service.</p>
<p>In nix:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a>users.users.hass.homeMode = <span class="st">&quot;0750&quot;</span>;</span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a>systemd.services.home<span class="op">-</span>assistant.serviceConfig = <span class="op">{</span></span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">UMask</span> <span class="op">=</span> lib.mkForce <span class="st">&quot;0027&quot;</span><span class="op">;</span></span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>Second, you need to update the files already created by Home Assistant:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> find /var/lib/hass <span class="at">-type</span> d <span class="at">-exec</span> chmod <span class="at">-r</span> g+rx <span class="st">&#39;{}&#39;</span> <span class="st">&#39;;&#39;</span></span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> find /var/lib/hass <span class="at">-type</span> f <span class="at">-exec</span> chmod <span class="at">-r</span> g+r <span class="st">&#39;{}&#39;</span> <span class="st">&#39;;&#39;</span></span></code></pre></div>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Display Counter Increase in Grafana</title>
    <link href="https://blog.tiserbox.com//posts/2023-06-25-display-counter-increase-in-grafana.html" />
    <id>https://blog.tiserbox.com//posts/2023-06-25-display-counter-increase-in-grafana.html</id>
    <published>2023-06-25T00:00:00Z</published>
    <updated>2023-06-25T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on June 25, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;grafana&#39;." href="/tags/grafana.html">grafana</a>
	  
	</section>
    <section>
        <p>I had this problem where in grafana I wanted to show the increase of a gauge or counter over time. I
didn’t want to show the absolute value, but the increase.</p>
<p>There is no builtin function to handle showing increases where a time serie possibly has null values
and drops, like for example after a deploy where it starts from 0. So here are the steps to achieve
this.</p>
<p>Say you have this time series:</p>
<p><img src="/images/grafana-20230625-1_time-series.png" /></p>
<p>and you want to produce this:</p>
<p><img src="/images/grafana-20230625-2_result.png" /></p>
<p>We go from 7 to 8 at the start, then go from 0 to 5 at the end, that makes a total increase of 6.</p>
<p>First, you need to duplicate the time series with an offset of <code>$__interval</code>, here I plot both without (A) and with offset (B):</p>
<pre><code>A:  sum (my_gauge)
B:  sum (my_gauge offset $__interval)</code></pre>
<p><img src="/images/grafana-20230625-3_offset.png" /></p>
<p>A naive A-B operation does not produce a good result: the total is wrong and we can see negative values</p>
<pre><code>A:  sum (my_gauge)
B:  sum (my_gauge offset $__interval)
C:  $A - $B</code></pre>
<p><img src="/images/grafana-20230625-4_A-B.png" /></p>
<p>let’s first get rid of negative values:</p>
<pre><code>C:  ($A - $B) &gt;= 0</code></pre>
<p><img src="/images/grafana-20230625-5_A-Bbt0.png" /></p>
<p>The total is better, but it’s not 6 just yet.</p>
<p>You can see where the vertical bar is, we do not register the jump from 0 to 1, because there is no data point. To add data points, we need to do this:</p>
<pre><code>A:  sum (my_gauge) or vector (0)
B:  sum (my_gauge offset $__interval) or vector (0)</code></pre>
<p><img src="/images/grafana-20230625-6_vector(0).png" /></p>
<p>If we again print the difference, we can see the total is accurate:</p>
<pre><code>C:  ($A - $B) &gt;= 0</code></pre>
<p><img src="/images/grafana-20230625-7_A-Bvector(0).png" /></p>
<p>But there’s a not pleasing gap there when the values drop. The final calculation is:</p>
<pre><code>C:  ($A - $B) &gt;= 0 or vector (0)</code></pre>
<p><img src="/images/grafana-20230625-8_final.png" /></p>
<p>The actual calculation I have is this <del>beautiful</del> horrendous formula:</p>
<pre><code>(
    (sum (my_gauge)                    or vector (0))
  - (sum (my_gauge offset $__interval) or vector (0))
) &gt;= 0 or vector (0)</code></pre>
<p>which gives:</p>
<p><img src="/images/grafana-20230625-2_result.png" /></p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Declarative WiFi with Encrypted Secret on NixOS</title>
    <link href="https://blog.tiserbox.com//posts/2023-05-15-declarative-wi-fi-with-encrypted-secret-on-nix-os.html" />
    <id>https://blog.tiserbox.com//posts/2023-05-15-declarative-wi-fi-with-encrypted-secret-on-nix-os.html</id>
    <published>2023-05-15T00:00:00Z</published>
    <updated>2023-05-15T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on May 15, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;server&#39;." href="/tags/server.html">server</a>, <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <p>This blog post recaps how to install NixOS with only a WiFi connection, how to manage the WiFi
connection declaratively and how to encrypt the passphrase during deploy.</p>
<p>This is only really useful for a machine that will not move much. If you’re trying to configure WiFi
on a laptop, I would recommend using <code>services.networkmanager</code> and configure the connection
manually.</p>
<!--toc:start-->
<ul>
<li><a href="#why-encrypt-the-passphrase">Why Encrypt the Passphrase</a></li>
<li><a href="#enable-wifi-manually">Enable WiFi Manually</a></li>
<li><a href="#initial-install-wifi-configuration">Initial Install WiFi Configuration</a></li>
<li><a href="#deploy-with-encrypted-secret">Deploy with Encrypted Secret</a>
<!--toc:end--></li>
</ul>
<h1 id="why-encrypt-the-passphrase">Why Encrypt the Passphrase</h1>
<p>Like for any non-encrypted secret in NixOS, two issues arise if you put the passphrase in clear in
the configuration:</p>
<ol type="1">
<li>the passphrase will be stored in the nix store of both the build machine and the target machine</li>
<li>and the passphrase will be stored in the git repo you use to manage your deploy.</li>
</ol>
<p>The solution to the first issue is to use something like the <code>deployment.keys</code> option that is
supported by most <a href="https://wiki.nixos.org/wiki/Applications#Deployment">deployment tools</a>.</p>
<p>The solution to the second issue is to encrypt the secret in the repo, and decrypt it on the target
machine, after deploy. <a href="https://wiki.nixos.org/wiki/Comparison_of_secret_managing_schemes">Multiple
tools</a> exist to handle this.</p>
<p>In this post, we will use <a href="https://github.com/Mic92/sops-nix">sops-nix</a> which provides a solution
for both issues.</p>
<h1 id="enable-wifi-manually">Enable WiFi Manually</h1>
<p>A little aside before we start. If something goes wrong, you can always setup WiFi manually with the
commands from the
<a href="https://wiki.archlinux.org/title/wpa_supplicant#Connecting_with_wpa_passphrase">wpa_supplicant</a> and
<a href="https://wiki.archlinux.org/title/Dhcpcd#Configuration">dhcpcd</a> Arch Linux wiki. The only difference
with the wiki is you do not need to install the commands as they come with NixOS.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> wpa_supplicant <span class="at">-B</span> <span class="dt">\</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>     <span class="at">-i</span> wlan0 <span class="dt">\</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>     <span class="at">-c</span> <span class="op">&lt;(</span><span class="ex">wpa_passphrase</span> SSID PASSPHRASE<span class="op">)</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="fu">touch</span> dhcpcd.conf <span class="kw">&amp;&amp;</span> <span class="dt">\</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>    <span class="fu">sudo</span> dhcpcd <span class="at">--config</span> dhcpcd.conf</span></code></pre></div>
<h1 id="initial-install-wifi-configuration">Initial Install WiFi Configuration</h1>
<p>Assuming you just booted on NixOS for the first time on the target machine and you made some edits
to <code>configuration.nix</code>, your next step is to run <code>nixos-rebuild switch</code>. But first, you need a
working internet connection.</p>
<p>What is following looks like a convoluted way to set WiFi up but it sets us up nicely to be able to
declaratively set the WiFi connection with an encrypted password later on.</p>
<p>In the machine’s <code>configuration.nix</code>, add:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>networking.wireless = <span class="op">{</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">enable</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">environmentFile</span> <span class="op">=</span> <span class="st">&quot;/run/secrets/MY_SSID_PSK&quot;</span><span class="op">;</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">networks</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;</span>MY_SSID<span class="st">&quot;</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>      <span class="va">psk</span> <span class="op">=</span> <span class="st">&quot;@MY_SSID_PSK@&quot;</span><span class="op">;</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>Replace <code>MY_SSID</code> with the name of the SSID you will be connecting to.</p>
<p>Then create the file <code>/run/secrets/MY_SSID_PSK</code> with the following content:</p>
<pre><code>MY_SSID_PSK=theactualpassphrase</code></pre>
<h1 id="deploy-with-encrypted-secret">Deploy with Encrypted Secret</h1>
<p>Like we said earlier, we will use <a href="https://github.com/Mic92/sops-nix">nix-sops</a> to encrypt the
secret at rest and during deploy.</p>
<p>A few prerequisites:
- You copied over the machine’s <code>configuration.nix</code> locally which includes the <code>networking.wireless</code>
section we added earlier.
- You created a public/private key pair that allows you to ssh into the target machine.</p>
<p>Now, to actually encrypt the secret, we will follow the <a href="https://github.com/Mic92/sops-nix">nix-sops readme
file</a>. The gist is:</p>
<ol type="1">
<li><p>Install the necessary packages to run the commands:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="ex">nix</span> shell nixpkgs#ssh-to-age nixpkgs#sops</span></code></pre></div>
<p>You need the latest <code>ssh-to-age</code> binary as the one provided in 21.11 does not have all the
necessary arguments.</p></li>
<li><p>Create an <code>age</code> secret from that public/private key pair used to connect to the target machine.</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="ex">ssh-to-age</span> <span class="at">-private-key</span> <span class="dt">\</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>           <span class="at">-i</span> ~/.ssh/TARGET_HOSTNAME <span class="dt">\</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>           <span class="at">-o</span> ~/.config/sops/age/TARGET_HOSTNAME.txt</span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="ex">age-keygen</span> <span class="at">-y</span> ~/.config/sops/age/TARGET_HOSTNAME.txt</span></code></pre></div>
<p>Use the output of that last command for <code>admin_nixos</code> later on.</p>
<p>Also, replace <code>TARGET_HOSTNAME</code> with the actual hostname of the target machine.</p>
<p>If the private key uses a passphrase, you’ll first need to export an environment variable with the passphrase:</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="bu">read</span> <span class="at">-s</span> <span class="va">SSH_TO_AGE_PASSPHRASE</span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="va">SSH_TO_AGE_PASSPHRASE</span></span></code></pre></div></li>
<li><p>Get the <code>age</code> secret from the target machine</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="fu">ssh-keyscan</span> <span class="at">-t</span> ed25519 TARGET_MACHINE_IP <span class="kw">|</span> <span class="dt">\</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>    <span class="ex">ssh-to-age</span></span></code></pre></div>
<p>Use the output of that command for <code>server_TARGET_HOSTNAME</code> later on.</p>
<p>Note here I am using the IP of the target machine as <code>ssh-keyscan</code> was failing to retrieve
anything with the hostname. I do not know why.</p></li>
<li><p>Then fill in <code>.sops.yaml</code> with:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode yaml"><code class="sourceCode yaml"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="fu">keys</span><span class="kw">:</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="kw">-</span><span class="at"> </span><span class="ot">&amp;admin_nixos</span><span class="at"> age1...</span></span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="kw">-</span><span class="at"> </span><span class="ot">&amp;server_TARGET_HOSTNAME</span><span class="at"> age1...</span></span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a><span class="fu">creation_rules</span><span class="kw">:</span></span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a><span class="at">  </span><span class="kw">-</span><span class="at"> </span><span class="fu">path_regex</span><span class="kw">:</span><span class="at"> secrets/[^/]+\.yaml$</span></span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="fu">key_groups</span><span class="kw">:</span></span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a><span class="at">    </span><span class="kw">-</span><span class="at"> </span><span class="fu">age</span><span class="kw">:</span></span>
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> </span><span class="ot">*admin_nixos</span></span>
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a><span class="at">      </span><span class="kw">-</span><span class="at"> </span><span class="ot">*server_TARGET_HOSTNAME</span></span></code></pre></div>
<p>That file should be living in your repository used for deploys.</p>
<p>A few replacements are needed in the file:</p>
<ul>
<li>Replace <code>age1...</code> string for <code>admin_nixos</code> with the value we obtained at step 2.</li>
<li>Replace <code>age1...</code> string for <code>server_TARGET_HOSTNAME</code> with the value we obtained at step 3.</li>
<li>Replace <code>TARGET_HOSTNAME</code> with the actual hostname of the target server.</li>
</ul></li>
<li><p>Create the encrypted secret file:</p></li>
</ol>
<div class="sourceCode" id="cb9"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="fu">mkdir</span> <span class="at">-p</span> secrets</span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a><span class="ex">sops</span> secrets/secrets.yaml</span></code></pre></div>
<p>The content of the file should be the content of the file in <code>/run/secrets</code> we created earlier:
<code>MY_SSID_PSK=theactualpassphrase</code></p>
<ol start="6" type="1">
<li>Wire up SOPS in the <code>configuration.nix</code>:</li>
</ol>
<div class="sourceCode" id="cb10"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a>sops = <span class="op">{</span></span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">defaultSopsFile</span> <span class="op">=</span> <span class="ss">./secrets/secrets.yaml</span><span class="op">;</span></span>
<span id="cb10-3"><a href="#cb10-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">age</span>.<span class="va">sshKeyPaths</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;/etc/ssh/ssh_host_ed25519_key&quot;</span> <span class="op">];</span></span>
<span id="cb10-4"><a href="#cb10-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">age</span>.<span class="va">keyFile</span> <span class="op">=</span> <span class="st">&quot;/home/ME/.config/sops/age/TARGET_HOSTNAME.txt&quot;</span><span class="op">;</span></span>
<span id="cb10-5"><a href="#cb10-5" aria-hidden="true" tabindex="-1"></a>  <span class="va">secrets</span>.<span class="st">&quot;MY_SSID_PSK&quot;</span> <span class="op">=</span> <span class="op">{};</span></span>
<span id="cb10-6"><a href="#cb10-6" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>Now, next time you will deploy, sops will use the secret file, send it over to the target machine
when deploying, decrypt the file and populate the content of <code>/run/secrets/MY_SSID_PSK</code>.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Install NixOS on a Raspberry PI</title>
    <link href="https://blog.tiserbox.com//posts/2023-05-12-install-nixos-on-a-raspberry-pi.html" />
    <id>https://blog.tiserbox.com//posts/2023-05-12-install-nixos-on-a-raspberry-pi.html</id>
    <published>2023-05-12T00:00:00Z</published>
    <updated>2023-05-12T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on May 12, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;server&#39;." href="/tags/server.html">server</a>, <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <p>I am writing this blog post because, although all the documentation is available online, it is not
always obvious how all the parts should fit together.</p>
<!--toc:start-->
<ul>
<li><a href="#install-nixos">Install NixOS</a>
<ul>
<li><a href="#aside-on-wifi">Aside on WiFi</a></li>
</ul></li>
<li><a href="#activate-the-system-manually">Activate the System Manually</a></li>
<li><a href="#configure-ssh-public-key-access">Configure SSH Public Key Access</a></li>
<li><a href="#remove-ssh-password-access">Remove SSH Password Access</a></li>
<li><a href="#deploy-to-the-raspberry-pi-with-colmena">Deploy to the Raspberry PI with Colmena</a></li>
<li><a href="#conclusion">Conclusion</a>
<!--toc:end--></li>
</ul>
<h1 id="install-nixos">Install NixOS</h1>
<p>Follow the <a href="https://wiki.nixos.org/wiki/NixOS_on_ARM/Raspberry_Pi">Official NixOS on Raspberry Pi wiki</a>.
You should be able to download a pre-built image through Hydra which was nice. The guide will walk
you through writing the image to a SD card and booting up the PI for the first time.</p>
<p>After <code>configuration.nix</code> file gets generated on first boot, modify it to:</p>
<ul>
<li><p>Change the hostname.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>networking.hostName = <span class="st">&quot;RPI_HOSTNAME&quot;</span>;</span></code></pre></div></li>
<li><p>Enable ssh daemon, which automatically opens the port 22 in the firewall.</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>services.openssh.enable = <span class="cn">true</span>;</span></code></pre></div></li>
<li><p>Add a <code>nixos</code> user that can sudo that will be used to deploy.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>users.users.nixos = <span class="op">{</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">isNormalUser</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>  <span class="co"># Enable ‘sudo’ for the user:</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">extraGroups</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;wheel&quot;</span> <span class="op">];</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>  <span class="va">packages</span> <span class="op">=</span> <span class="kw">with</span> pkgs<span class="op">;</span> <span class="op">[</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>    <span class="co"># ...</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>  <span class="op">];</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>And enable sudo without entering the password.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a>security.sudo.extraRules = <span class="op">[</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>  <span class="op">{</span> <span class="va">users</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;nixos&quot;</span> <span class="op">];</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>    <span class="va">options</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;NOPASSWD&quot;</span> <span class="op">];</span></span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>  <span class="op">}</span></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="op">]</span>;</span></code></pre></div>
<p>You can choose another user name here, no need to stick to <code>nixos</code>.</p></li>
</ul>
<p>You should not need to modify the <code>hardware-configuration.nix</code> file.</p>
<h2 id="aside-on-wifi">Aside on WiFi</h2>
<p>If you need WiFi, check out my <a href="./2023-05-15-declarative-wi-fi-with-encrypted-secret-on-nix-os.html">blog post on using WiFi with
NixOS</a>. Like for this post, all
the documentation is out there but setting up WiFi declaratively with an encrypted secret is not
trivial.</p>
<p>If you connected the Raspberry PI with an Ethernet cable to the router, it should have automatically
setup access to the internet.</p>
<h1 id="activate-the-system-manually">Activate the System Manually</h1>
<p>With a functioning internet connection, run:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="fu">sudo</span> nixos-rebuild switch</span></code></pre></div>
<p>Also, set a password for the user we just created.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="fu">passwd</span> nixos</span></code></pre></div>
<p>Note that if you chose another user name than <code>nixos</code>, the <code>nixos</code> user we were using until now does
not exist anymore. You will need to <code>exit</code> the session and login as the user we configured earlier.</p>
<p>In the rest of the post, I will assume you did stick to <code>nixos</code> so every time you see <code>nixos</code>,
replace it by the user name you chose.</p>
<h1 id="configure-ssh-public-key-access">Configure SSH Public Key Access</h1>
<p>Now, you can access the Raspberry PI using its IP address and the <code>nixos</code> user’s password. We will
configure a private-public key pair instead and disable ssh password access.</p>
<p>On your laptop - not the Raspberry PI - run:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="fu">ssh-keygen</span> <span class="at">-t</span> ed25519 <span class="dt">\</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>           <span class="at">-f</span> ~/.ssh/RPI_HOSTNAME <span class="dt">\</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>           <span class="at">-C</span> <span class="st">&quot;nixos@LAPTOP_HOSTNAME&quot;</span></span></code></pre></div>
<p>Personally, I do use a passphrase when generating the key and add the key to ssh-agent. I will add a
post about that later.</p>
<p>Now, copy over the public key with:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="ex">ssh-copy-id</span> <span class="at">-i</span> ~/.ssh/RPI_HOSTNAME RPI_IP_ADDRESS</span></code></pre></div>
<p>And update your laptop’s ssh config. Somewhere in your NixOS config you should add:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a>program.ssh.matchBlocks = <span class="op">{</span></span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;</span>RPI_HOSTNAME<span class="st">&quot;</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a>        <span class="va">user</span> <span class="op">=</span> <span class="st">&quot;nixos&quot;</span><span class="op">;</span></span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a>        <span class="va">iidentityFile</span> <span class="op">=</span> <span class="st">&quot;/home/ME/.ssh/RPI_HOSTNAME&quot;</span><span class="op">;</span></span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>Now, you can ssh in with <code>ssh RPI_HOSTNAME</code>.</p>
<h1 id="remove-ssh-password-access">Remove SSH Password Access</h1>
<p>It is not ideal to leave this, so add the following to the Raspberry PI’s <code>configuration.nix</code>.</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a>services.openssh.permitRootLogin = <span class="st">&quot;no&quot;</span>;</span>
<span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a>services.openssh.passwordAuthentication = <span class="cn">false</span>;</span></code></pre></div>
<h1 id="deploy-to-the-raspberry-pi-with-colmena">Deploy to the Raspberry PI with Colmena</h1>
<p><a href="https://github.com/zhaofengli/colmena">Colmena</a> is one of the existing ways to deploy a
configuration to a NixOS machine.</p>
<p>Add the machine to a <code>flake.nix</code>:</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">outputs</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a>    <span class="va">colmena</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a>      <span class="va">meta</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a>        <span class="va">nixpkgs</span> <span class="op">=</span> <span class="bu">import</span> nixpkgs <span class="op">{</span></span>
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a>          <span class="va">system</span> <span class="op">=</span> <span class="st">&quot;x86_64-linux&quot;</span><span class="op">;</span></span>
<span id="cb11-7"><a href="#cb11-7" aria-hidden="true" tabindex="-1"></a>        <span class="op">};</span></span>
<span id="cb11-8"><a href="#cb11-8" aria-hidden="true" tabindex="-1"></a>      <span class="op">};</span></span>
<span id="cb11-9"><a href="#cb11-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb11-10"><a href="#cb11-10" aria-hidden="true" tabindex="-1"></a>      <span class="va">RPI_HOSTNAME</span> <span class="op">=</span> <span class="op">{</span> <span class="va">name</span><span class="op">,</span> <span class="va">nodes</span><span class="op">,</span> <span class="op">...</span> <span class="op">}</span>: <span class="op">{</span></span>
<span id="cb11-11"><a href="#cb11-11" aria-hidden="true" tabindex="-1"></a>        <span class="va">deployment</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb11-12"><a href="#cb11-12" aria-hidden="true" tabindex="-1"></a>          <span class="co"># Must correspond to the hostname in the</span></span>
<span id="cb11-13"><a href="#cb11-13" aria-hidden="true" tabindex="-1"></a>          <span class="co"># ssh config.</span></span>
<span id="cb11-14"><a href="#cb11-14" aria-hidden="true" tabindex="-1"></a>          <span class="va">targetHost</span> <span class="op">=</span> <span class="st">&quot;RPI_HOSTNAME&quot;</span><span class="op">;</span></span>
<span id="cb11-15"><a href="#cb11-15" aria-hidden="true" tabindex="-1"></a>          <span class="co"># Needed to build on the Raspberry PI</span></span>
<span id="cb11-16"><a href="#cb11-16" aria-hidden="true" tabindex="-1"></a>          <span class="co"># because the laptop is a x86 architecture.</span></span>
<span id="cb11-17"><a href="#cb11-17" aria-hidden="true" tabindex="-1"></a>          <span class="va">buildOnTarget</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb11-18"><a href="#cb11-18" aria-hidden="true" tabindex="-1"></a>          <span class="co"># The user that can do password-less sudo.</span></span>
<span id="cb11-19"><a href="#cb11-19" aria-hidden="true" tabindex="-1"></a>          <span class="va">targetUser</span> <span class="op">=</span> <span class="st">&quot;nixos&quot;</span><span class="op">;</span></span>
<span id="cb11-20"><a href="#cb11-20" aria-hidden="true" tabindex="-1"></a>        <span class="op">};</span></span>
<span id="cb11-21"><a href="#cb11-21" aria-hidden="true" tabindex="-1"></a>        <span class="va">networking</span>.<span class="va">hostName</span> <span class="op">=</span> name<span class="op">;</span></span>
<span id="cb11-22"><a href="#cb11-22" aria-hidden="true" tabindex="-1"></a>      </span>
<span id="cb11-23"><a href="#cb11-23" aria-hidden="true" tabindex="-1"></a>        <span class="co"># The configuration.nix file copied from the</span></span>
<span id="cb11-24"><a href="#cb11-24" aria-hidden="true" tabindex="-1"></a>        <span class="co"># Raspberry PI.</span></span>
<span id="cb11-25"><a href="#cb11-25" aria-hidden="true" tabindex="-1"></a>        <span class="va">imports</span> <span class="op">=</span> <span class="op">[</span></span>
<span id="cb11-26"><a href="#cb11-26" aria-hidden="true" tabindex="-1"></a>          <span class="ss">./machines/RPI_HOSTNAME-configuration.nix</span></span>
<span id="cb11-27"><a href="#cb11-27" aria-hidden="true" tabindex="-1"></a>        <span class="op">];</span></span>
<span id="cb11-28"><a href="#cb11-28" aria-hidden="true" tabindex="-1"></a>      <span class="op">};</span></span>
<span id="cb11-29"><a href="#cb11-29" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb11-30"><a href="#cb11-30" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb11-31"><a href="#cb11-31" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<p>Copy over the Rasbpberry PI’s <code>configuration.nix</code> and <code>hardware-configuration.nix</code> files to a local
<code>machines/</code> folder and reference the former in the import above. You should have the following
files:</p>
<ul>
<li><code>flakes.nix</code> that imports</li>
<li><code>machines/RPI_HOSTNAME-configuration.nix</code> that imports</li>
<li><code>machines/RPI_HOSTNAME-hardware-configuration.nix</code></li>
</ul>
<p>Finally, run:</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="ex">colmena</span> apply</span></code></pre></div>
<p>It should work for a few minutes - around 15 the first time in my case - then say “activation
successful”.</p>
<h1 id="conclusion">Conclusion</h1>
<p>We just installed NixOS on a Raspberry PI and were able to deploy to it.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Quickly create a post from Emacs</title>
    <link href="https://blog.tiserbox.com//posts/2023-05-09-quickly-create-a-post-from-emacs.html" />
    <id>https://blog.tiserbox.com//posts/2023-05-09-quickly-create-a-post-from-emacs.html</id>
    <published>2023-05-09T00:00:00Z</published>
    <updated>2023-05-09T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on May  9, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;emacs&#39;." href="/tags/emacs.html">emacs</a>, <a title="All pages tagged &#39;hakyll&#39;." href="/tags/hakyll.html">hakyll</a>
	  
	</section>
    <section>
        <p>With the following snippet, I can call <code>M-x hakyll-blog-new-post</code>,
fill in the title and get started writing right away.</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode lisp"><code class="sourceCode commonlisp"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>(defgroup hakyll-blog <span class="kw">nil</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;Hakyll Blog.&quot;</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>  :group <span class="dt">&#39;applications</span>)</span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>(defcustom hakyll-blog-dir</span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;~/blog&quot;</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;Hakyll blog directory.&quot;</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>  <span class="bu">:type</span> <span class="dt">&#39;string</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>  :group <span class="dt">&#39;hakyll-blog</span>)</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>(defcustom hakyll-blog-file-time-format</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;%Y-%m-%d&quot;</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;Hakyll blog post filename time format.&quot;</span></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a>  <span class="bu">:type</span> <span class="dt">&#39;string</span></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a>  :group <span class="dt">&#39;hakyll-blog</span>)</span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a>(<span class="kw">defun</span><span class="fu"> hakyll-blog-new-post </span>(title)</span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;Create new blog post under `hakyll-blog-dir&#39; with given TITLE.&quot;</span></span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a>  (interactive <span class="st">&quot;sBlog post title: &quot;</span>)</span>
<span id="cb1-20"><a href="#cb1-20" aria-hidden="true" tabindex="-1"></a>  (find-file (hakyll-blog--file-format title))</span>
<span id="cb1-21"><a href="#cb1-21" aria-hidden="true" tabindex="-1"></a>  (insert (<span class="kw">format</span> <span class="st">&quot;---</span><span class="sc">\n</span><span class="st">title: %s</span><span class="sc">\n</span><span class="st">tags: </span><span class="sc">\n</span><span class="st">---</span><span class="sc">\n\n</span><span class="st">&quot;</span> title)))</span>
<span id="cb1-22"><a href="#cb1-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-23"><a href="#cb1-23" aria-hidden="true" tabindex="-1"></a>(<span class="kw">defun</span><span class="fu"> hakyll-blog--file-format </span>(title)</span>
<span id="cb1-24"><a href="#cb1-24" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;File name for TITLE post.&quot;</span></span>
<span id="cb1-25"><a href="#cb1-25" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">format</span> <span class="st">&quot;%s/posts/%s-%s.markdown&quot;</span></span>
<span id="cb1-26"><a href="#cb1-26" aria-hidden="true" tabindex="-1"></a>          (expand-file-name hakyll-blog-dir)</span>
<span id="cb1-27"><a href="#cb1-27" aria-hidden="true" tabindex="-1"></a>          (format-time-string hakyll-blog-file-time-format)</span>
<span id="cb1-28"><a href="#cb1-28" aria-hidden="true" tabindex="-1"></a>          (s-dashed-words title)))</span></code></pre></div>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Launch Capture Window in KDE</title>
    <link href="https://blog.tiserbox.com//posts/2023-05-08-launch-capture-window-in-kde.html" />
    <id>https://blog.tiserbox.com//posts/2023-05-08-launch-capture-window-in-kde.html</id>
    <published>2023-05-08T00:00:00Z</published>
    <updated>2023-05-08T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on May  8, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;emacs&#39;." href="/tags/emacs.html">emacs</a>
	  
	</section>
    <section>
        <p>I created a custom shortcut <code>Meta-T</code> in KDE with the following
trigger:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="ex">emacsclient</span> <span class="at">-e</span> <span class="st">&quot;(org-capture)&quot;</span> <span class="at">--create-frame</span></span></code></pre></div>
<p>I went with calling <code>org-capture</code> directly as that shows a buffer
where I can choose which template to use. If I was using the
<code>org-protocol</code> feature, I’d need to setup one KDE shortcut per
template, the maintenance of which would annoy me to no end, on top of
filling up my global shortcuts.</p>
<p>But this did not raise the new frame in all cases. For example, if I
had my browser up and pressed <code>Meta-T</code>, the new Emacs frame would be
created but would not be focused.</p>
<p>To make that work, I needed the shortcut trigger to be:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">emacsclient</span> <span class="at">-e</span> <span class="dt">\</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;(progn (x-focus-frame nil) (org-capture))&quot;</span> <span class="dt">\</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  <span class="at">--create-frame</span></span></code></pre></div>
<p>and I needed to add the following snippet to my Emacs config:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode lisp"><code class="sourceCode commonlisp"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>(<span class="kw">defun</span><span class="fu"> my/focus-new-client-frame </span>()</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  (select-frame-set-input-focus (selected-frame)))</span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>(add-hook <span class="dt">&#39;server-after-make-frame-hook</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>  <span class="op">#&#39;</span>my/focus-new-client-frame)</span></code></pre></div>
<p>I got that snippet from <a href="https://www.reddit.com/r/emacs/comments/it4m2w/comment/g5kr7z7/">this reddit
post</a>.</p>
<p>Now, when pressing <code>Meta-T</code> anywhere will create a new frame and focus
it.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Sops on NixOps 2</title>
    <link href="https://blog.tiserbox.com//posts/2023-04-21-sops-on-nixops-2.html" />
    <id>https://blog.tiserbox.com//posts/2023-04-21-sops-on-nixops-2.html</id>
    <published>2023-04-21T00:00:00Z</published>
    <updated>2023-04-21T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on April 21, 2023
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>, <a title="All pages tagged &#39;server&#39;." href="/tags/server.html">server</a>
	  
	</section>
    <section>
        <p>Setting Sops is done by following the steps in the <a href="https://github.com/Mic92/sops-nix#usage-example">README</a> file. But I struggled on step “Get a public key for your target machine”. This is what it tells us to do:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="ex">nix-shell</span> <span class="at">-p</span> ssh-to-age <span class="dt">\</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  <span class="at">--run</span> <span class="st">&#39;cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age&#39;</span></span></code></pre></div>
<p>But you need to ssh into the machine to get the public key. How to do that? Well, this “little” one liner does the trick:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">nixops</span> export <span class="at">--network</span> dev <span class="kw">|</span> <span class="dt">\</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="ex">jq</span> <span class="st">&#39;..|.&quot;virtualbox.publicHostKey&quot;? | select(. != null)&#39;</span> <span class="at">-r</span></span></code></pre></div>
<ul>
<li>The <code>..</code> is a recursive descent on all JSON object fields.</li>
<li>We’re searching for the field names <code>virtualbox.publicHostKey</code>. We
need the double quotes because the dot is actually part of the field
name.</li>
<li>We then select the non null items, effectively keeping the only match.</li>
</ul>
<p>This outputs something like <code>ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJhZqrj2+idV2uZXHUp2Q4sJ8SzRWGYz0nHSKuiW5oo3 NixOps auto-generated key</code>.</p>
<p>You can then pipe that to <code>ssh-to-age</code> and go to the next step.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>OS Install</title>
    <link href="https://blog.tiserbox.com//posts/2021-10-09-os-install.html" />
    <id>https://blog.tiserbox.com//posts/2021-10-09-os-install.html</id>
    <published>2021-10-09T00:00:00Z</published>
    <updated>2021-10-09T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on October  9, 2021
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;server&#39;." href="/tags/server.html">server</a>
	  
	</section>
    <section>
        <p><strong>Note:</strong> <em>since I wrote this blog post, I switched to using NixOS. I’m leaving this post up because
it could still hold valuable info.</em></p>
<p>This is obviously the first step, we will install the OS. I chose
Archlinux. Everything here should be adaptable to other linux
distributions but the locations of files could be different.</p>
<p>We’ll create a live USB, use it to install Arch on another server and
we’ll finish by SSHing to the server using SSH keys for
authentication.</p>
<h1 id="preliminaries">Preliminaries</h1>
<p>We need to choose a few things before starting the install. Here is
what I’ll stick to in this blog post:</p>
<ul>
<li>hostname of the server: <code>$server</code></li>
<li>hostname of the laptop you’re currently using: <code>$laptop</code></li>
<li>user I’ll use to connect to it: <code>$user</code></li>
</ul>
<p>I confess, there’s nothing original here but I feel like it’s easier
to follow along this way. I use the convention of bash variables with
the dollar prefix in the following.</p>
<h1 id="live-usb">Live USB</h1>
<p>First step is to create a bootable USB that we’ll use to install Arch
on the server. We’ll create two partitions, one for the OS, one for
storing some files like a SSH public key that will allow us to SSH
into the server without password.</p>
<p>Detailed instructions can be found in the <a href="https://wiki.archlinux.org/title/USB_flash_installation_medium">Arch
wiki</a>.</p>
<h2 id="download-the-arch-iso">Download the Arch ISO</h2>
<p>From <a href="https://archlinux.org/download/">https://archlinux.org/download/</a>.</p>
<h2 id="find-the-usb-drive-path">Find the USB drive path</h2>
<pre><code>$ lsblk -p -d -o NAME,MODEL,SIZE,TRAN \
    | grep &#39;NAME\|usb&#39;</code></pre>
<p>Example output:</p>
<pre><code>NAME         MODEL               SIZE TRAN
/dev/sda     ST9500325ASG      465.8G sata
/dev/nvme0n1 SPCC M.2 PCIe SSD 953.9G nvme</code></pre>
<p>In the following, I’ll use <code>/dev/sdX</code> as the path. Replace it by the
one you got from the command above.</p>
<h2 id="partition-and-format-the-usb-drive">Partition and format the USB drive</h2>
<p>Create two partitions, the first one must be <code>fat32</code>, the second
<code>ext4</code>.</p>
<pre><code>$ fdisk /dev/sdX &lt;&lt;EOF
g    # use gpt table
n    # create new partition
1    # partition number
     # accept default
+3G  # 3Gb, adapt to your USB key size
t    # Change type of partition
1    #
n    # create new partiton
2    # partition number


p    # print table, to double check
w    # write table
EOF

$ mkfs.vfat -F32 /dev/sdX1
$ mkfs.ext4 -F /dev/sdX2</code></pre>
<h2 id="write-os-and-files-to-usb-drive">Write OS and files to USB drive</h2>
<p>Mount the two partitions and the Arch ISO.</p>
<pre><code>$ mkdir -p mnt/root mnt/data mnt/iso
$ mount -o rw /dev/sdX1 mnt/root
$ mount -o rw /dev/sdX2 mnt/data
$ mount -o loop arch.iso mnt/iso</code></pre>
<p>Copy the files to the root partition.</p>
<pre><code>$ rsync -a \
    --info=progress2 \
    --human-readable \
    --no-inc-recursive \
    mnt/iso \
    mnt/root</code></pre>
<p>Create a SSH private key and public key pair and copy it to the USB
key. I use <code>Password Store</code> as my password manager to generate and
store the passphrase.</p>
<pre><code>$ pass generate --clip sshkey-passphrase/$laptop/$server

$ ssh-keygen -b 4096 -i ~/.ssh/$server -N $(pass show sshkey-passphrase/$laptop/$server)
$ cp ~/.ssh/$server.pub mnt/data</code></pre>
<p>I use the convention <code>sshkey-passphrase/$laptop/$server</code> for the
location of the passphrase in my password manager.</p>
<p>Update the syslinux install to instruct the OS where the root
partition is located.</p>
<pre><code>$ uuid=$(blkid -o value -s UUID &quot;/dev/sdX1&quot;)
$ sed -i -e \
    &quot;s|archisolabel=.*$|archisodevice=/dev/disk/by-uuid/$uuid|&quot; \
    &quot;mnt/root/arch/boot/syslinux/archiso_sys.cfg&quot;
$ syslinux-install_update -iam</code></pre>
<p>Don’t forget to sync to actually write the files. This step can take a
while.</p>
<pre><code>$ sync</code></pre>
<p>Now, let’s unmount and we can then remove the USB drive.</p>
<pre><code>$ umount mnt/*</code></pre>
<h1 id="install-os-on-the-server">Install OS on the server</h1>
<p>Let’s put the USB key in the server’s USB port and boot the server.
I’ll assume you have brand new drives or are happy to wipe them clean.</p>
<h1 id="todo">TODO</h1>
<p>GnuPG setup for ssh agent</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Server setup - Introduction</title>
    <link href="https://blog.tiserbox.com//posts/2021-10-05-server-setup.html" />
    <id>https://blog.tiserbox.com//posts/2021-10-05-server-setup.html</id>
    <published>2021-10-05T00:00:00Z</published>
    <updated>2021-10-05T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on October  5, 2021
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;server&#39;." href="/tags/server.html">server</a>
	  
	</section>
    <section>
        <p><strong>Note:</strong> <em>since I wrote this blog post, I switched to using NixOS. I’m leaving this post up because
it could still hold valuable info.</em></p>
<p>This is the introductory post in the let’s build a server series. This
series goes through all the steps needed to setup the same server I
have at home. Along the way, I’ll explain everything I know or thought
of while setting up this server.</p>
<p>We’ll build a box with a media server, automated movies and series
downloader protected by VPN, a file server and synchronizer,
integration with dropbox, a code server, dynamic IP support,
monitoring, RSS support, SSO, a dashboard, backup and recovery.</p>
<p>We’ll also talk about how your laptop and phone will interact with the
server, so essentially file synchronization, picture viewing and
backup.</p>
<figure>
<img src="/images/screenshot_homer.png" alt="Our dashboard to be" />
<figcaption aria-hidden="true">Our dashboard to be</figcaption>
</figure>
<p>We will install and configure on an Archlinux box:</p>
<ul>
<li><a href="https://www.bazarr.media/">Bazarr</a></li>
<li><a href="https://torsion.org/borgmatic/">Borgmatic</a></li>
<li><a href="https://caddyserver.com/">Caddy</a></li>
<li><a href="https://deluge-torrent.org/">Deluge</a> through VPN</li>
<li>DynDNS through <a href="https://www.godaddy.com/">Godaddy</a></li>
<li><a href="https://ersatztv.org/">Ersatztv</a></li>
<li><a href="https://about.gitlab.com/">Gitlab</a></li>
<li><a href="https://gnupg.org/">GnuPG</a></li>
<li><a href="http://www.haproxy.org/">Haproxy</a></li>
<li><a href="https://github.com/bastienwirtz/homer">Homer</a></li>
<li><a href="https://github.com/Jackett/Jackett">Jackett</a></li>
<li><a href="https://jellyfin.org/">Jellyfin</a></li>
<li><a href="https://www.keycloak.org/">Keycloak</a></li>
<li><a href="https://sourceware.org/lvm2/">LVM</a></li>
<li><a href="http://neil.brown.name/blog/mdadm">MDADM</a></li>
<li><a href="https://nextcloud.com/">Nextcloud</a></li>
<li><a href="https://oauth2-proxy.github.io/oauth2-proxy/">Oauth2-Proxy</a></li>
<li><a href="https://openvpn.net/">OpenVPN</a></li>
<li><a href="https://www.passwordstore.org/">Password Store</a></li>
<li><a href="https://www.postgresql.org/">PostgreSQL</a></li>
<li><a href="https://radarr.video/">Radarr</a></li>
<li><a href="https://rclone.org/">Rclone</a></li>
<li><a href="https://redis.io/">Redis</a></li>
<li><a href="https://sonarr.tv/">Sonarr</a></li>
<li><a href="https://tt-rss.org/">TinyTinyRSS</a></li>
<li><a href="https://php-fpm.org/">PHP-FPM</a></li>
</ul>
<p>You’ll notice I left out an automation tool like Docker or Ansible
which we won’t use here. The goal of this serie is to empower you and
to make you understand what’s going on under the hood. That’s how I
like to build the things I use.</p>
<p>I assume you are familiar with the command line, that’s about it.</p>
<h1 id="hardware-and-distro">Hardware and Distro</h1>
<ul>
<li>CPU: Intel i5 760 2.80GHz, 4 cores</li>
<li>RAM: 16GB</li>
<li>Graphics Card: GeForce GTS 250</li>
<li>Disks:
<ul>
<li>Samsung SSD 950 PRO (256GB)</li>
<li>TOSHIBA HDWD110 (1 TB)</li>
<li>WDC WD10EZEX-60W (1 TB)</li>
<li>ST2000DM006-2DM1 (1.82 TB) x2</li>
</ul></li>
<li>Distro: Archlinux 5.13.13-arch1-1</li>
</ul>
<p>In short, it’s an old desktop repurposed in a server with some new
hard drives. The drives are in a RAID 1 setup so I have a total of
2.82 TB of available storage. The SSD is used to store <code>/var</code> to speed
up writing logs and access to PostgreSQL.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Add Tags to a Hakyll Site</title>
    <link href="https://blog.tiserbox.com//posts/2020-10-22-add-tags-to-site.html" />
    <id>https://blog.tiserbox.com//posts/2020-10-22-add-tags-to-site.html</id>
    <published>2020-10-22T00:00:00Z</published>
    <updated>2020-10-22T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on October 22, 2020
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;hakyll&#39;." href="/tags/hakyll.html">hakyll</a>
	  
	</section>
    <section>
        <p>Let’s add some tags to each blog post. The tags are added in the
header section of each post. For example:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode markdown"><code class="sourceCode markdown"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="co">---</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="an">title:</span><span class="co"> Deploy the Blog to GitHub Pages</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="an">tags:</span><span class="co"> hakyll, haskell, nix</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="co">---</span></span></code></pre></div>
<p>This will produce a blog post with 3 tags. Clicking on a tag will
bring you to a page with all blog posts tagged with that tag.</p>
<p>You can also access to all tags from a special Tags page.</p>
<p>Let’s set this all up. Everything outlined here can be found in <a href="https://github.com/ibizaman/blog/commit/6ef96feaaeddd3cae4a1e54fe9fde385598e7871">this
commit</a>.</p>
<p>I was heavily inspired by <a href="https://javran.github.io/posts/2014-03-01-add-tags-to-your-hakyll-blog.html">this blog
post</a>
by Javran but I diverged in the way I wanted the tags to be shown. I
also added a Tags page.</p>
<h1 id="show-tags-in-post">Show Tags in Post</h1>
<p>First, let’s show the tags on each post page. This will be the result:</p>
<p><img src="/images/tags-post.png" /></p>
<p>We first need to add a section to <code>templates/post.html</code>:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode markdown"><code class="sourceCode markdown"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">&lt;section</span> <span class="er">class</span><span class="ot">=</span><span class="st">&quot;tags&quot;</span><span class="kw">&gt;</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  $if(tags)$</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  Tags: $tags$</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>  $endif$</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="kw">&lt;/section&gt;</span></span></code></pre></div>
<p>Note that the <code>$if()$</code> clause only checks if a field exists. So the
<code>Tags:</code> line will be printed also if the <code>$tags$</code> field is empty.</p>
<p>We then need to update <code>site.hs</code> to populate the <code>$tags$</code> field:</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode diff"><code class="sourceCode diff"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>main = hakyllWith conf $ do</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>  -- [...]</span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="va">+  tags &lt;- buildTags &quot;posts/*&quot; (fromCapture &quot;tags/*.html&quot;)</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="va">+  let postCtxWithTags = postCtx tags</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>  -- [...]</span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>   match &quot;posts/*&quot; $ do</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>     route $ setExtension &quot;html&quot;</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>     compile</span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>       $   pandocCompiler</span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a><span class="st">-      &gt;&gt;= loadAndApplyTemplate &quot;templates/post.html&quot;    postCtx</span></span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a><span class="st">-      &gt;&gt;= loadAndApplyTemplate &quot;templates/default.html&quot; postCtx</span></span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a><span class="va">+      &gt;&gt;= loadAndApplyTemplate &quot;templates/post.html&quot;    postCtxWithTags</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a><span class="va">+      &gt;&gt;= loadAndApplyTemplate &quot;templates/default.html&quot; postCtxWithTags</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a>       &gt;&gt;= relativizeUrls</span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a><span class="st">-postCtx :: Context String</span></span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a><span class="st">-postCtx = dateField &quot;date&quot; &quot;%B %e, %Y&quot; `mappend` defaultContext</span></span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a><span class="va">+postCtx :: Tags -&gt; Context String</span></span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a><span class="va">+postCtx tags =</span></span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a><span class="va">+  tagsField &quot;tags&quot; tags</span></span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a><span class="va">+    `mappend` dateField &quot;date&quot; &quot;%B %e, %Y&quot;</span></span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a><span class="va">+    `mappend` defaultContext</span></span></code></pre></div>
<p>The <code>match "posts/*"</code> block existed already but we changed the
<code>postCtx</code> to <code>postCtxWithTags</code>. Actually, we changed all occurrences
of <code>postCtx</code> with <code>postCtxWithTags</code>. We also added a <code>"tags"</code> field in
the <code>postCtx</code> function.</p>
<p>Let’s analyze two lines which took me some time to understand.</p>
<h2 id="build-tags">Build Tags</h2>
<div class="sourceCode" id="cb4"><pre class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="ot">buildTags ::</span> <span class="dt">MonadMetadata</span> m</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>          <span class="ot">=&gt;</span> <span class="dt">Pattern</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>          <span class="ot">-&gt;</span> (<span class="dt">String</span> <span class="ot">-&gt;</span> <span class="dt">Identifier</span>)</span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a>          <span class="ot">-&gt;</span> m <span class="dt">Tags</span></span></code></pre></div>
<p>We use it like so:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>buildTags <span class="st">&quot;posts/*&quot;</span> (fromCapture <span class="st">&quot;tags/*.html&quot;</span>)</span></code></pre></div>
<p>In this case, <code>buildTags</code> will parse each file in the <code>posts/</code>
directory and extract all tags in the header. We will later generate
an HTML page per tag and that’s when the <code>fromCapture</code> function will
be used.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="ot">fromCapture ::</span> <span class="dt">Pattern</span> <span class="ot">-&gt;</span> <span class="dt">String</span> <span class="ot">-&gt;</span> <span class="dt">Identifier</span></span></code></pre></div>
<p><code>Pattern</code> is a simple glob pattern:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a>(toFilePath <span class="op">$</span> fromCapture <span class="st">&quot;tags/*.html&quot;</span> <span class="st">&quot;foo&quot;</span>) <span class="op">==</span> <span class="st">&quot;tags/foo.html&quot;</span></span></code></pre></div>
<h2 id="tags-field">Tags Field</h2>
<div class="sourceCode" id="cb8"><pre class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="ot">tagsField ::</span> <span class="dt">String</span> <span class="ot">-&gt;</span> <span class="dt">Tags</span> <span class="ot">-&gt;</span> <span class="dt">Context</span> a</span></code></pre></div>
<p>We use it like so:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a>tagsField <span class="st">&quot;tags&quot;</span> tags</span></code></pre></div>
<p>This adds a field named <code>"tags"</code> that can be accessed in the template
as <code>$tags$</code>. <code>Context a</code> is what the template has access to.</p>
<p>Here, the default rendering for <code>$tags$</code> (comma separated list of
urls) suits us but we will see later how to make something custom.</p>
<h1 id="tag-page">Tag Page</h1>
<p>Let’s create an HTML page per tag that will show all posts that has
the tag. It will look like this:</p>
<p><img src="/images/tags-page.png" /></p>
<p>We create a template for the tag page in <code>templates/tag.html</code>. Here,
we can reuse the <code>post-list.html</code> template:</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode markdown"><code class="sourceCode markdown"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a>$partial(&quot;templates/post-list.html&quot;)$</span></code></pre></div>
<p>In <code>templates/post-list.html</code>, we added the <code>tags</code> div:</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode diff"><code class="sourceCode diff"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a> &lt;ul&gt;</span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a>     $for(posts)$</span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a>         &lt;li&gt;</span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a>             &lt;a href=&quot;$url$&quot;&gt;$title$&lt;/a&gt; - $date$</span>
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a><span class="va">+            &lt;div class=&quot;tags&quot;&gt;$tags$&lt;/div&gt;</span></span>
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a>         &lt;/li&gt;</span>
<span id="cb11-7"><a href="#cb11-7" aria-hidden="true" tabindex="-1"></a>     $endfor$</span>
<span id="cb11-8"><a href="#cb11-8" aria-hidden="true" tabindex="-1"></a> &lt;/ul&gt;</span></code></pre></div>
<p>With some accompanying css in <code>css/default.css</code>:</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode css"><code class="sourceCode css"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a>article <span class="fu">.tags</span> {</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a>  <span class="kw">font-size</span>: <span class="dv">1.4</span><span class="dt">rem</span><span class="op">;</span></span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a>  <span class="kw">font-style</span>: <span class="dv">italic</span><span class="op">;</span></span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a>  <span class="kw">color</span>: <span class="cn">#555</span><span class="op">;</span></span>
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a>}</span>
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-7"><a href="#cb12-7" aria-hidden="true" tabindex="-1"></a>div<span class="fu">.tags</span> {</span>
<span id="cb12-8"><a href="#cb12-8" aria-hidden="true" tabindex="-1"></a>  <span class="kw">text-align</span>: <span class="dv">right</span><span class="op">;</span></span>
<span id="cb12-9"><a href="#cb12-9" aria-hidden="true" tabindex="-1"></a>}</span></code></pre></div>
<p>In <code>site.hs</code>, we add a new block to the <code>main</code> function which takes
care of generating one HTML page per tag.</p>
<div class="sourceCode" id="cb13"><pre class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a>main <span class="ot">=</span> hakyllWith conf <span class="op">$</span> <span class="kw">do</span></span>
<span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>  tagsRules tags <span class="op">$</span> \tag <span class="kw">pattern</span> <span class="ot">-&gt;</span> <span class="kw">do</span></span>
<span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>    <span class="kw">let</span> title <span class="ot">=</span> <span class="st">&quot;Posts tagged \&quot;&quot;</span> <span class="op">++</span> tag <span class="op">++</span> <span class="st">&quot;\&quot;&quot;</span></span>
<span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>    route idRoute</span>
<span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>    compile <span class="op">$</span> <span class="kw">do</span></span>
<span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>      posts <span class="ot">&lt;-</span> recentFirst <span class="op">=&lt;&lt;</span> loadAll <span class="kw">pattern</span></span>
<span id="cb13-8"><a href="#cb13-8" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>      <span class="kw">let</span> ctx <span class="ot">=</span></span>
<span id="cb13-9"><a href="#cb13-9" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>            constField <span class="st">&quot;title&quot;</span> title</span>
<span id="cb13-10"><a href="#cb13-10" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>              <span class="ot">`mappend`</span> listField <span class="st">&quot;posts&quot;</span> postCtxWithTags (<span class="fu">return</span> posts)</span>
<span id="cb13-11"><a href="#cb13-11" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>              <span class="ot">`mappend`</span> defaultContext</span>
<span id="cb13-12"><a href="#cb13-12" aria-hidden="true" tabindex="-1"></a><span class="op">+</span></span>
<span id="cb13-13"><a href="#cb13-13" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>      makeItem <span class="st">&quot;&quot;</span></span>
<span id="cb13-14"><a href="#cb13-14" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>        <span class="op">&gt;&gt;=</span> loadAndApplyTemplate <span class="st">&quot;templates/tag.html&quot;</span>     ctx</span>
<span id="cb13-15"><a href="#cb13-15" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>        <span class="op">&gt;&gt;=</span> loadAndApplyTemplate <span class="st">&quot;templates/default.html&quot;</span> ctx</span>
<span id="cb13-16"><a href="#cb13-16" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>        <span class="op">&gt;&gt;=</span> relativizeUrls</span></code></pre></div>
<p>Where <code>tags</code> is the variable we added earlier with the <code>buildTags</code>
function.</p>
<h1 id="page-with-all-tags">Page with all Tags</h1>
<p>Last piece of the puzzle, let’s add a page with all tags. Clicking on
a tag will forward to the corresponding tag page, the ones we created
above. It will look like this:</p>
<p><img src="/images/tags-all-page.png" /></p>
<p>In <code>site.hs</code>, we add a new block:</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a>main <span class="ot">=</span> hakyllWith conf <span class="op">$</span> <span class="kw">do</span></span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>  create [<span class="st">&quot;tags.html&quot;</span>] <span class="op">$</span> <span class="kw">do</span></span>
<span id="cb14-4"><a href="#cb14-4" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>    route idRoute</span>
<span id="cb14-5"><a href="#cb14-5" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>    compile <span class="op">$</span> <span class="kw">do</span></span>
<span id="cb14-6"><a href="#cb14-6" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>      tags&#39; <span class="ot">&lt;-</span> tagsMetadata tags</span>
<span id="cb14-7"><a href="#cb14-7" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>      <span class="kw">let</span> tagsCtx <span class="ot">=</span></span>
<span id="cb14-8"><a href="#cb14-8" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>            listField</span>
<span id="cb14-9"><a href="#cb14-9" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>                <span class="st">&quot;tags&quot;</span></span>
<span id="cb14-10"><a href="#cb14-10" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>                (  field <span class="st">&quot;name&quot;</span>  (<span class="fu">return</span> <span class="op">.</span> tagName <span class="op">.</span> itemBody)</span>
<span id="cb14-11"><a href="#cb14-11" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>                <span class="op">&lt;&gt;</span> field <span class="st">&quot;url&quot;</span>   (<span class="fu">return</span> <span class="op">.</span> tagUrl <span class="op">.</span> itemBody)</span>
<span id="cb14-12"><a href="#cb14-12" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>                <span class="op">&lt;&gt;</span> field <span class="st">&quot;count&quot;</span> (<span class="fu">return</span> <span class="op">.</span> <span class="fu">show</span> <span class="op">.</span> tagCount <span class="op">.</span> itemBody)</span>
<span id="cb14-13"><a href="#cb14-13" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>                )</span>
<span id="cb14-14"><a href="#cb14-14" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>                (<span class="fu">sequence</span> <span class="op">$</span> <span class="fu">map</span> makeItem <span class="op">$</span> tags&#39;)</span>
<span id="cb14-15"><a href="#cb14-15" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>              <span class="ot">`mappend`</span> defaultContext</span>
<span id="cb14-16"><a href="#cb14-16" aria-hidden="true" tabindex="-1"></a><span class="op">+</span></span>
<span id="cb14-17"><a href="#cb14-17" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>      getResourceBody</span>
<span id="cb14-18"><a href="#cb14-18" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>        <span class="op">&gt;&gt;=</span> applyAsTemplate tagsCtx</span>
<span id="cb14-19"><a href="#cb14-19" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>        <span class="op">&gt;&gt;=</span> loadAndApplyTemplate <span class="st">&quot;templates/default.html&quot;</span> tagsCtx</span>
<span id="cb14-20"><a href="#cb14-20" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>        <span class="op">&gt;&gt;=</span> relativizeUrls</span>
<span id="cb14-21"><a href="#cb14-21" aria-hidden="true" tabindex="-1"></a><span class="op">+</span></span>
<span id="cb14-22"><a href="#cb14-22" aria-hidden="true" tabindex="-1"></a><span class="op">+</span><span class="kw">data</span> <span class="dt">TagMetadata</span> <span class="ot">=</span> <span class="dt">TagMetadata</span></span>
<span id="cb14-23"><a href="#cb14-23" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>         {<span class="ot"> tagName ::</span> <span class="dt">String</span></span>
<span id="cb14-24"><a href="#cb14-24" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>         ,<span class="ot"> tagUrl ::</span> <span class="dt">String</span></span>
<span id="cb14-25"><a href="#cb14-25" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>         ,<span class="ot"> tagCount ::</span> <span class="dt">Int</span></span>
<span id="cb14-26"><a href="#cb14-26" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>         }</span>
<span id="cb14-27"><a href="#cb14-27" aria-hidden="true" tabindex="-1"></a><span class="op">+</span></span>
<span id="cb14-28"><a href="#cb14-28" aria-hidden="true" tabindex="-1"></a><span class="op">+</span><span class="ot">tagsMetadata ::</span> <span class="dt">Tags</span> <span class="ot">-&gt;</span> <span class="dt">Compiler</span> [<span class="dt">TagMetadata</span>]</span>
<span id="cb14-29"><a href="#cb14-29" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>tagsMetadata tags <span class="ot">=</span> <span class="kw">do</span></span>
<span id="cb14-30"><a href="#cb14-30" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>  <span class="kw">let</span> tagsList <span class="ot">=</span> <span class="fu">map</span> <span class="fu">fst</span> <span class="op">$</span> tagsMap tags</span>
<span id="cb14-31"><a href="#cb14-31" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>  forM (tagsMap tags) <span class="op">$</span> \(tag, ids) <span class="ot">-&gt;</span> <span class="kw">do</span></span>
<span id="cb14-32"><a href="#cb14-32" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>    route&#39; <span class="ot">&lt;-</span> getRoute <span class="op">$</span> tagsMakeId tags tag</span>
<span id="cb14-33"><a href="#cb14-33" aria-hidden="true" tabindex="-1"></a><span class="op">+</span>    <span class="fu">return</span> <span class="op">$</span> <span class="dt">TagMetadata</span> tag (fromMaybe <span class="st">&quot;/&quot;</span> route&#39;) (<span class="fu">length</span> ids)</span></code></pre></div>
<p>There is a lot going on here, let’s analyze it step by step.</p>
<h2 id="tags.html-page">Tags.html Page</h2>
<p>The <code>create</code> function will create the <code>tags.html</code> page. The
<code>getResourceBody</code> will load a template html page of the same name.
Let’s create it:</p>
<div class="sourceCode" id="cb15"><pre class="sourceCode markdown"><code class="sourceCode markdown"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="co">---</span></span>
<span id="cb15-2"><a href="#cb15-2" aria-hidden="true" tabindex="-1"></a><span class="an">title:</span><span class="co"> Tags</span></span>
<span id="cb15-3"><a href="#cb15-3" aria-hidden="true" tabindex="-1"></a><span class="co">---</span></span>
<span id="cb15-4"><a href="#cb15-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb15-5"><a href="#cb15-5" aria-hidden="true" tabindex="-1"></a>Here you can find all the tags set on the various posts:</span>
<span id="cb15-6"><a href="#cb15-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb15-7"><a href="#cb15-7" aria-hidden="true" tabindex="-1"></a><span class="kw">&lt;ul&gt;</span></span>
<span id="cb15-8"><a href="#cb15-8" aria-hidden="true" tabindex="-1"></a>    $for(tags)$</span>
<span id="cb15-9"><a href="#cb15-9" aria-hidden="true" tabindex="-1"></a>        <span class="kw">&lt;li&gt;</span></span>
<span id="cb15-10"><a href="#cb15-10" aria-hidden="true" tabindex="-1"></a>            <span class="kw">&lt;a</span> <span class="er">href</span><span class="ot">=</span><span class="st">&quot;$url$&quot;</span><span class="kw">&gt;</span>$name$<span class="kw">&lt;/a&gt;</span> ($count$)</span>
<span id="cb15-11"><a href="#cb15-11" aria-hidden="true" tabindex="-1"></a>        <span class="kw">&lt;/li&gt;</span></span>
<span id="cb15-12"><a href="#cb15-12" aria-hidden="true" tabindex="-1"></a>    $endfor$</span>
<span id="cb15-13"><a href="#cb15-13" aria-hidden="true" tabindex="-1"></a><span class="kw">&lt;/ul&gt;</span></span></code></pre></div>
<p>It needs a <code>$tags$</code> list whose items contain three fields: the <code>$url$</code>
of the tag page, the <code>$name$</code> of the tag and how many posts are tagged
with it in <code>$count$</code>.</p>
<h2 id="tags-metadata">Tags Metadata</h2>
<p>The <code>tagsMetadata</code> function creates a list of <code>TagMetadata</code> out of the
<code>Tags</code>. I copied it from the <a href="http://localhost:49513/file/nix/store/z2sxaiprlxhdmy2b5zwnzwdaamgni2cm-hakyll-4.13.4.0-doc/share/doc/hakyll-4.13.4.0/html/src/Hakyll.Web.Tags.html#renderTags"><code>renderTags</code> source
code</a>
and only added the use of <code>TagMetadata</code>.</p>
<p><code>TagMetadata</code> is a helper type I introduced to make passing data to
the template’s context explicit.</p>
<h2 id="tags-context">Tags Context</h2>
<p><code>tagsCtx</code> is a <code>Context</code> having a <code>$tags$</code> field which is a list. Each
item has the <code>$url$</code>, <code>$name$</code> and <code>$count$</code> fields which are needed
by the template. Those fields come from the <code>TagMetadata</code> type.</p>
<h2 id="tags-nav">Tags Nav</h2>
<p>Finally, we can add a link to the <code>/tags.html</code> page to the nav bar in
the <code>templates/default.html</code> file:</p>
<div class="sourceCode" id="cb16"><pre class="sourceCode diff"><code class="sourceCode diff"><span id="cb16-1"><a href="#cb16-1" aria-hidden="true" tabindex="-1"></a>    &lt;a href=&quot;/contact.html&quot;&gt;Contact&lt;/a&gt;</span>
<span id="cb16-2"><a href="#cb16-2" aria-hidden="true" tabindex="-1"></a><span class="va">+   &lt;a href=&quot;/tags.html&quot;&gt;Tags&lt;/a&gt;</span></span>
<span id="cb16-3"><a href="#cb16-3" aria-hidden="true" tabindex="-1"></a>    &lt;a href=&quot;/archive.html&quot;&gt;Archive&lt;/a&gt;</span></code></pre></div>
<h1 id="conclusion">Conclusion</h1>
<p>Although that was not straightforward and I ran into a lot of issues
while figuring things out, once more Haskell shines because most of
the issues were resolved at compile time. The only things I needed to
figure out at runtime were related to styling and cosmetics.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Unexpected Writing Perk</title>
    <link href="https://blog.tiserbox.com//posts/2020-10-20-1-unexpected-writing-perk.html" />
    <id>https://blog.tiserbox.com//posts/2020-10-20-1-unexpected-writing-perk.html</id>
    <published>2020-10-20T00:00:00Z</published>
    <updated>2020-10-20T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on October 20, 2020
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;misc&#39;." href="/tags/misc.html">misc</a>
	  
	</section>
    <section>
        <p>It’s kind of obvious in retrospective, but I try to pay attention when
I’m writing something on the internet.</p>
<p>For example, sharing my Emacs configuration made me realize I had a
few hacks in place and I felt dirty sharing those. I took some time to
rework the hacks and that resulted in improving my configuration and
making it more maintainable.</p>
<p>The same thing happens when I try to explain an idea to someone or
when I make a presentation at work. I try to explain my ideas in the
most intelligible way possible and that’s often when I realize I don’t
grasp the idea fully and am unable to explain at least some part of
it.</p>
<p>In other words, writing is like having a <a href="https://en.wikipedia.org/wiki/Rubber_duck_debugging">rubber
duck</a>.</p>
<p>Thank you for being my rubber duck.</p>
<p><a href="http://cliparts.co/clipart/920667" title="Image from cliparts.co"><img src="http://cliparts.co/cliparts/Bca/ArE/BcaArEaLi.png" width="350" alt="Rubber Duck Border Wallpapers and Background" /></a></p>
<p>I hope you will gain something from reading my blog posts. If you do,
or do not, I’d love to hear about it!</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Configure Emacs to Write this Blog With Nix</title>
    <link href="https://blog.tiserbox.com//posts/2020-10-17-1-configure-emacs.html" />
    <id>https://blog.tiserbox.com//posts/2020-10-17-1-configure-emacs.html</id>
    <published>2020-10-17T00:00:00Z</published>
    <updated>2020-10-17T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on October 17, 2020
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;hakyll&#39;." href="/tags/hakyll.html">hakyll</a>, <a title="All pages tagged &#39;haskell&#39;." href="/tags/haskell.html">haskell</a>, <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>, <a title="All pages tagged &#39;emacs&#39;." href="/tags/emacs.html">emacs</a>, <a title="All pages tagged &#39;lsp&#39;." href="/tags/lsp.html">lsp</a>, <a title="All pages tagged &#39;systemd&#39;." href="/tags/systemd.html">systemd</a>
	  
	</section>
    <section>
        <p>We need to make Emacs find Haskell executables through Nix. It will
use <a href="https://microsoft.github.io/language-server-protocol/">LSP</a> with
the <a href="https://github.com/haskell/ghcide/">Ghcide</a> LSP server as basis
to parse the source code,
<a href="https://www.flycheck.org/en/latest/">Flycheck</a> for live errors in the
buffer and <a href="https://hackage.haskell.org/package/brittany">Brittany</a> to
format the code. Ghcide and Brittany are installed with
<a href="https://nixos.org/">Nix</a>.</p>
<p>To make all this work, we will use the following Emacs packages:</p>
<ul>
<li><a href="https://github.com/travisbhartwell/nix-emacs/#nix-sandbox"><code>nix-sandbox</code></a> provides us with helper functions to get the current project’s nix sandbox.</li>
<li><a href="https://github.com/NixOS/nix-mode"><code>nix-mode</code></a> makes it easy to edix <code>.nix</code> files as well as provides the <code>nix-build</code> function.</li>
<li><a href="https://emacs-lsp.github.io/lsp-mode"><code>lsp-mode</code></a> handles talking to a LSP server, here Ghcide.</li>
<li><a href="https://github.com/emacs-lsp/lsp-ui"><code>lsp-ui</code></a> shows LSP actions and various infos.</li>
<li><a href="https://github.com/tigersoldier/company-lsp"><code>company-lsp</code></a> provides autocompletion based on the LSP server.</li>
<li><a href="https://github.com/emacs-lsp/lsp-haskell"><code>lsp-haskell</code></a> is used by <code>lsp-mode</code> to talk to Ghcide.</li>
<li><a href="https://haskell.github.io/haskell-mode"><code>haskell-mode</code></a> provides syntax highlighting, hoogle integration and much more for editing Haskell files.</li>
<li><a href="https://www.flycheck.org/"><code>flycheck</code></a> provides syntax check highlighting.</li>
</ul>
<p>By the way, I’m using
<a href="https://github.com/jwiegley/use-package">use-package</a> with
<a href="https://github.com/raxod502/straight.el">straight</a> to configure
Emacs.</p>
<p>Finally, to make this work with Emacs running as a daemon, there will
be some systemd-fu required.</p>
<h1 id="emacs-setup">Emacs Setup</h1>
<h2 id="nix-sandbox-and-helper">Nix Sandbox and Helper</h2>
<p>We add <code>nix-sandbox</code> and <code>nix-mode</code>:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>(<span class="kw">use-package</span> nix-sandbox</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>  :straight <span class="kw">t</span>)</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>(<span class="kw">use-package</span> nix-mode</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>  :straight <span class="kw">t</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>  :mode <span class="st">&quot;</span><span class="sc">\\</span><span class="st">.nix</span><span class="sc">\\</span><span class="st">&#39;&quot;</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>  :init</span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">require</span> <span class="dt">&#39;nix-build</span>))</span></code></pre></div>
<p><code>nix-sandbox</code> defines <code>nix-current-sandbox</code> which returns the path to
<code>shell.nix</code> if it exists, or falls back to <code>default.nix</code> if it exists
or <code>nil</code> if none exist. It also defines <code>nix-shell-command</code> which,
from the docs:</p>
<blockquote>
<div class="sourceCode" id="cb2"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>(<span class="kw">defun</span><span class="fu"> nix-shell-command </span>(sandbox &amp;<span class="kw">rest</span> args)</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="st">&quot;Assemble a command from ARGS that can be executed in the specified SANDBOX.&quot;</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  ...</span></code></pre></div>
</blockquote>
<p>Super useful for running <code>brittany</code> or <code>ghcide</code> inside our Nix
environment. We won’t need to run those manually though.</p>
<p>Another useful function is <code>nix-compile</code> which interactively asks for
a sandbox and a command to run.</p>
<p>We add <code>nix-mode</code> mostly for its <code>nix-build</code> function. This allows us
to build the site executable from Emacs.</p>
<h2 id="lsp-mode-lsp-ui-company-lsp">lsp-mode, lsp-ui, company-lsp</h2>
<p>These are fairly standard configuration whenever you use LSP in Emacs.
I reproduce them here for completeness.</p>
<div class="sourceCode" id="cb3"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>(<span class="kw">use-package</span> lsp-mode</span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>  :straight <span class="kw">t</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>  :commands lsp</span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>  :init</span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">defun</span><span class="fu"> my/lsp-format-buffer-silent </span>()</span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>    <span class="st">&quot;Silence errors from `lsp-format-buffer&#39;.&quot;</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>    (<span class="kw">ignore-errors</span> (lsp-format-buffer)))</span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>  :hook ((sh-mode . lsp-deferred)</span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>         (javascript-mode . lsp-deferred)</span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>         (html-mode . lsp-deferred)</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>         (before-save . my/lsp-format-buffer-silent))</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>  :config</span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">setq</span> lsp-signature-auto-activate <span class="kw">t</span>)</span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>  (lsp-lens-mode <span class="kw">t</span>))</span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>(<span class="kw">use-package</span> lsp-ui</span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a>  :straight <span class="kw">t</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a>  :hook (lsp-mode-hook . lsp-ui-mode)</span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a>  :commands lsp-ui-mode</span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a>  :config</span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">setq</span> lsp-ui-flycheck-enable <span class="kw">t</span></span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a>        lsp-ui-flycheck-live-reporting <span class="kw">nil</span>))</span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a>(<span class="kw">use-package</span> company-lsp</span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a>  :straight <span class="kw">t</span></span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a>  :commands company-lsp</span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a>  :config</span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">push</span> <span class="dt">&#39;company-lsp</span> company-backends))</span></code></pre></div>
<h2 id="haskell-mode">Haskell-mode</h2>
<p>Haskell-mode is used to edit haskell source code.</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a>(<span class="kw">use-package</span> haskell-mode</span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>  :straight <span class="kw">t</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a>  :after nix-sandbox</span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>  :init</span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">defun</span><span class="fu"> my/haskell-set-stylish </span>()</span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a>	(if-let* ((sandbox (nix-current-sandbox))</span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a>			  (fullcmd (nix-shell-command sandbox <span class="st">&quot;brittany&quot;</span>))</span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a>			  (path (<span class="kw">car</span> fullcmd))</span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a>			  (args (<span class="kw">cdr</span> fullcmd)))</span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a>	  (setq-local haskell-mode-stylish-haskell-path path</span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a>				  haskell-mode-stylish-haskell-args args)))</span>
<span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">defun</span><span class="fu"> my/haskell-set-hoogle </span>()</span>
<span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a>	(if-let* ((sandbox (nix-current-sandbox)))</span>
<span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a>		(setq-local haskell-hoogle-command (nix-shell-string sandbox <span class="st">&quot;hoogle&quot;</span>))))</span>
<span id="cb4-18"><a href="#cb4-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-19"><a href="#cb4-19" aria-hidden="true" tabindex="-1"></a>  :hook ((haskell-mode . capitalized-words-mode)</span>
<span id="cb4-20"><a href="#cb4-20" aria-hidden="true" tabindex="-1"></a>		 (haskell-mode . haskell-decl-scan-mode)</span>
<span id="cb4-21"><a href="#cb4-21" aria-hidden="true" tabindex="-1"></a>		 (haskell-mode . haskell-indent-mode)</span>
<span id="cb4-22"><a href="#cb4-22" aria-hidden="true" tabindex="-1"></a>		 (haskell-mode . haskell-indentation-mode)</span>
<span id="cb4-23"><a href="#cb4-23" aria-hidden="true" tabindex="-1"></a>		 (haskell-mode . my/haskell-set-stylish)</span>
<span id="cb4-24"><a href="#cb4-24" aria-hidden="true" tabindex="-1"></a>		 (haskell-mode . my/haskell-set-hoogle)</span>
<span id="cb4-25"><a href="#cb4-25" aria-hidden="true" tabindex="-1"></a>		 (haskell-mode . lsp-deferred)</span>
<span id="cb4-26"><a href="#cb4-26" aria-hidden="true" tabindex="-1"></a>		 (haskell-mode . haskell-auto-insert-module-template))</span>
<span id="cb4-27"><a href="#cb4-27" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-28"><a href="#cb4-28" aria-hidden="true" tabindex="-1"></a>  :config</span>
<span id="cb4-29"><a href="#cb4-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-30"><a href="#cb4-30" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">defun</span><span class="fu"> my/haskell-hoogle--server-command </span>(port)</span>
<span id="cb4-31"><a href="#cb4-31" aria-hidden="true" tabindex="-1"></a>	(if-let* ((hooglecmd `(<span class="st">&quot;hoogle&quot;</span> <span class="st">&quot;serve&quot;</span> <span class="st">&quot;--local&quot;</span> <span class="st">&quot;-p&quot;</span> ,(number-to-string port)))</span>
<span id="cb4-32"><a href="#cb4-32" aria-hidden="true" tabindex="-1"></a>			  (sandbox (nix-current-sandbox)))</span>
<span id="cb4-33"><a href="#cb4-33" aria-hidden="true" tabindex="-1"></a>		(<span class="kw">apply</span> <span class="dt">&#39;nix-shell-command</span> sandbox hooglecmd)</span>
<span id="cb4-34"><a href="#cb4-34" aria-hidden="true" tabindex="-1"></a>	  hooglecmd))</span>
<span id="cb4-35"><a href="#cb4-35" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-36"><a href="#cb4-36" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">setq</span> haskell-hoogle-server-command <span class="dt">&#39;my/haskell-hoogle--server-command</span></span>
<span id="cb4-37"><a href="#cb4-37" aria-hidden="true" tabindex="-1"></a>		haskell-stylish-on-save <span class="kw">t</span>))</span></code></pre></div>
<p>The advanced configuration is for handling <code>hoogle</code> and <code>brittany</code>
inside Nix. In both cases, we use buffer-local variables through the
<code>haskell-mode</code> hook and always check if we are in a Nix environment.
If we are, that is if <code>(nix-current-sandbox)</code> is not <code>nil</code>, then we
use <code>nix-sandbox</code>’s helpers to wrap around the <code>hoogle</code> and <code>brittany</code>
executables. If not, we directly call <code>hoogle</code> and <code>brittany</code>.</p>
<p>To search in the local database, we can then use
<code>haskell-hoogle-lookup-from-local</code>. On first call, it will start the
local server.</p>
<p><code>brittany</code> will be called every time we save a buffer to format it.</p>
<h2 id="lsp-haskell">lsp-haskell</h2>
<p>Now we setup the package that talks to Ghcide, our LSP server.</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>(<span class="kw">use-package</span> lsp-haskell</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>  :straight <span class="kw">t</span></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a>  :after nix-sandbox</span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a>  :init</span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">setq</span> lsp-prefer-flymake <span class="kw">nil</span>)</span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">require</span> <span class="dt">&#39;lsp-haskell</span>)</span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a>  :config</span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a>  <span class="co">;; from https://github.com/travisbhartwell/nix-emacs#haskell-mode</span></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">defun</span><span class="fu"> my/nix--lsp-haskell-wrapper </span>(args)</span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a>	(if-let ((sandbox (nix-current-sandbox)))</span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a>		(<span class="kw">apply</span> <span class="dt">&#39;nix-shell-command</span> sandbox args)</span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a>	  args))</span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">setq</span> lsp-haskell-process-path-hie <span class="st">&quot;ghcide&quot;</span></span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a>		lsp-haskell-process-args-hie &#39;()</span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a>		lsp-haskell-process-wrapper-function <span class="dt">&#39;my/nix--lsp-haskell-wrapper</span>))</span></code></pre></div>
<p>Like for haskell-mode, the advanced configuration is focused on
wrapping the various commands with <code>nix-shell-command</code> and
<code>nix-shell-string</code>, with the wrapper being a pass-through if
<code>(nix-current-sandbox)</code> returns <code>nil</code>.</p>
<h2 id="flycheck">Flycheck</h2>
<p>Finally, we configure <code>flycheck</code> itself. Like for <code>lsp-haskell</code>, we
make it so <code>flycheck</code> can talk to Ghcide through Nix.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode commonlisp"><code class="sourceCode commonlisp"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>(<span class="kw">use-package</span> flycheck</span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>  :straight <span class="kw">t</span></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a>  :after nix-sandbox</span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a>  :init</span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a>  (add-hook <span class="dt">&#39;after-init-hook</span> <span class="dt">&#39;global-flycheck-mode</span>)</span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a>  <span class="co">;; from https://github.com/travisbhartwell/nix-emacs#flycheck</span></span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">defun</span><span class="fu"> my/nix--flycheck-command-wrapper </span>(command)</span>
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a>	(if-let ((sandbox (nix-current-sandbox)))</span>
<span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a>		(<span class="kw">apply</span> <span class="dt">&#39;nix-shell-command</span> (nix-current-sandbox) command)</span>
<span id="cb6-12"><a href="#cb6-12" aria-hidden="true" tabindex="-1"></a>	  command))</span>
<span id="cb6-13"><a href="#cb6-13" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">defun</span><span class="fu"> my/nix--flycheck-executable-find </span>(cmd)</span>
<span id="cb6-14"><a href="#cb6-14" aria-hidden="true" tabindex="-1"></a>	(if-let ((sandbox (nix-current-sandbox)))</span>
<span id="cb6-15"><a href="#cb6-15" aria-hidden="true" tabindex="-1"></a>		(nix-executable-find (nix-current-sandbox) cmd)</span>
<span id="cb6-16"><a href="#cb6-16" aria-hidden="true" tabindex="-1"></a>	  (flycheck-default-executable-find cmd)))</span>
<span id="cb6-17"><a href="#cb6-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-18"><a href="#cb6-18" aria-hidden="true" tabindex="-1"></a>  :config</span>
<span id="cb6-19"><a href="#cb6-19" aria-hidden="true" tabindex="-1"></a>  (<span class="kw">setq</span> flycheck-check-syntax-automatically &#39;(save idle-change)</span>
<span id="cb6-20"><a href="#cb6-20" aria-hidden="true" tabindex="-1"></a>		flycheck-relevant-error-other-file-show <span class="kw">nil</span></span>
<span id="cb6-21"><a href="#cb6-21" aria-hidden="true" tabindex="-1"></a>		flycheck-command-wrapper-function <span class="dt">&#39;my/nix--flycheck-command-wrapper</span></span>
<span id="cb6-22"><a href="#cb6-22" aria-hidden="true" tabindex="-1"></a>		flycheck-executable-find <span class="dt">&#39;my/nix--flycheck-executable-find</span>)</span>
<span id="cb6-23"><a href="#cb6-23" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-24"><a href="#cb6-24" aria-hidden="true" tabindex="-1"></a>  (add-to-list <span class="dt">&#39;display-buffer-alist</span></span>
<span id="cb6-25"><a href="#cb6-25" aria-hidden="true" tabindex="-1"></a>			   `(,(rx bos <span class="st">&quot;*Flycheck errors*&quot;</span> eos)</span>
<span id="cb6-26"><a href="#cb6-26" aria-hidden="true" tabindex="-1"></a>				 (display-buffer-reuse-window</span>
<span id="cb6-27"><a href="#cb6-27" aria-hidden="true" tabindex="-1"></a>				  display-buffer-in-side-window)</span>
<span id="cb6-28"><a href="#cb6-28" aria-hidden="true" tabindex="-1"></a>				 (side            . bottom)</span>
<span id="cb6-29"><a href="#cb6-29" aria-hidden="true" tabindex="-1"></a>				 (reusable-frames . visible)</span>
<span id="cb6-30"><a href="#cb6-30" aria-hidden="true" tabindex="-1"></a>				 (window-height   . <span class="fl">0.33</span>))))</span></code></pre></div>
<h1 id="systemd-fu-for-emacs-daemon">Systemd-fu for Emacs Daemon</h1>
<p>I will assume you use systemd to start emacs in daemon mode. If so,
create or update the <code>~/.config/systemd/user/emacs.service</code> file with
the same <code>NIX_*</code> and <code>PATH</code> <code>Environment</code> fields:</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode ini"><code class="sourceCode ini"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="kw">[Unit]</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a><span class="dt">Description</span><span class="ot">=</span><span class="st">Emacs text editor</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a><span class="dt">Documentation</span><span class="ot">=</span><span class="st">info:emacs man:emacs(1) https://gnu.org/software/emacs/</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a><span class="kw">[Service]</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a><span class="dt">Type</span><span class="ot">=</span><span class="st">forking</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a><span class="dt">ExecStart</span><span class="ot">=</span><span class="st">/usr/bin/emacs --daemon</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a><span class="dt">ExecStop</span><span class="ot">=</span><span class="st">/usr/bin/emacsclient --eval &quot;(kill-emacs)&quot;</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a><span class="dt">Environment</span><span class="ot">=</span><span class="st">NIX_PROFILES=/nix/var/nix/profiles/default %h/.nix-profile</span></span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a><span class="dt">Environment</span><span class="ot">=</span><span class="st">NIX_PATH=%h/.nix-defexpr/channels</span></span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a><span class="dt">Environment</span><span class="ot">=</span><span class="st">NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt</span></span>
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a><span class="dt">Environment</span><span class="ot">=</span><span class="st">PATH=%h/.nix-profile/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:</span></span>
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a><span class="dt">Restart</span><span class="ot">=</span><span class="st">on-failure</span></span>
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a><span class="kw">[Install]</span></span>
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a><span class="dt">WantedBy</span><span class="ot">=</span><span class="st">default.target</span></span></code></pre></div>
<h1 id="conclusion">Conclusion</h1>
<p>That’s it, you should be well equipped for editing your site.
Actually, everything we saw here is transferable to any Haskell code
running through Nix.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Add Syntax Highlighting</title>
    <link href="https://blog.tiserbox.com//posts/2020-10-16-2-add-syntax-highlighting.html" />
    <id>https://blog.tiserbox.com//posts/2020-10-16-2-add-syntax-highlighting.html</id>
    <published>2020-10-16T00:00:00Z</published>
    <updated>2020-10-16T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on October 16, 2020
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;hakyll&#39;." href="/tags/hakyll.html">hakyll</a>
	  
	</section>
    <section>
        <p>This is “just” a matter of creating a CSS file and adding it to the
repository. The hard part is to have the patience to create the CSS
file, at least for me, because the css classes are not descriptive
(ex: what the hell are <code>class="ot"</code> and <code>class="op"</code>?). I tried
finding ready to use ones but I could only find some on
<a href="https://github.com/tejasbubane/hakyll-css">tejasbubane/hakyll-css</a>. I
chose the
<a href="https://raw.githubusercontent.com/tejasbubane/hakyll-css/master/css/zenburn.css">zenburn</a>
one.</p>
<p>Next steps are to copy paste its content to <code>css/syntax.css</code> then add
a link to the css file in <code>templates/default.html</code>:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="kw">&lt;link</span> <span class="er">rel</span><span class="ot">=</span><span class="st">&quot;stylesheet&quot;</span> <span class="er">href</span><span class="ot">=</span><span class="st">&quot;/css/syntax.css&quot;</span> <span class="kw">/&gt;</span></span></code></pre></div>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Deploy the Blog to GitHub Pages</title>
    <link href="https://blog.tiserbox.com//posts/2020-10-16-1-deploy-to-github-pages.html" />
    <id>https://blog.tiserbox.com//posts/2020-10-16-1-deploy-to-github-pages.html</id>
    <published>2020-10-16T00:00:00Z</published>
    <updated>2020-10-16T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on October 16, 2020
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;hakyll&#39;." href="/tags/hakyll.html">hakyll</a>, <a title="All pages tagged &#39;haskell&#39;." href="/tags/haskell.html">haskell</a>, <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <p>I want to use GitHub Pages because it is a very simple to use static
site server and it is readily available when using GitHub. We could
try to use GitHub CI to deploy to GitHub Pages but I do not want to
deal with this complexity just yet.</p>
<p>The method I will use for now is manual and requires two repositories:
one that holds the source code and the other that holds the generated
static files. The easiest way to handle this is to use a git
submodule. (I say easiest but I still spent a few hours setting this
up correctly…)</p>
<p>In my case, the repository holding the static files is
<a href="https://github.com/ibizaman/ibizaman.github.io">https://github.com/ibizaman/ibizaman.github.io</a>
and the one holding the source code is
<a href="https://github.com/ibizaman/blog">https://github.com/ibizaman/blog</a>.
In the latter, we will create a submodule pointing to the former. In
other words, the submodule will be in the repository holding the
source code.</p>
<h1 id="setup-a-deploy-command">Setup a Deploy Command</h1>
<p>In essence, we need to build the site and copy the resulting files to
the submodule’s path. We then commit and push the submodule to its
repository.</p>
<p>I tried first to simply use the <code>_site/</code> directory, output of the
<code>./result/bin/site build</code> command, as the submodule. The issue is the
<code>./result/bin/site rebuild</code> command completely wipes the <code>_site/</code>,
including the submodule’s <code>.git</code> directory. This puts the submodule in
a bad state.</p>
<p>Next solution is to generate the files in <code>_site/</code> then copy them to
the submodule’s directory. Usually, that’s where I start putting
commands in a <code>Makefile</code> so I don’t forget how to handle this. But
actually, Hakyll allows you to run a custom command when running
<code>./result/bin/site deploy</code>. This is configured in <code>site.hs</code>. We will
use that as a self-contained Makefile. Documentation can be found on
<a href="https://jaspervdj.be/hakyll/reference/Hakyll-Core-Configuration.html#t:Configuration">Hackage</a>.</p>
<p>In <code>site.hs</code>, we change:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode diff"><code class="sourceCode diff"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="va">+conf :: Configuration</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="va">+conf = defaultConfiguration {</span></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="va">+    deployCommand = &quot;rm -rf ibizaman.github.io/* &amp;&amp; cp -r _site/* ibizaman.github.io&quot;</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="va">+  }</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a> main :: IO ()</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a><span class="st">-main = hakyll $ do</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a><span class="va">+main = hakyllWith conf $ do</span></span></code></pre></div>
<p>This is tailored to the names of my repos, of course. It corresponds
to <a href="https://github.com/ibizaman/blog/commit/64d0b697c7863c2b6a9aae5552abb937e66cc6c2">this
commit</a>.</p>
<h1 id="actually-deploy">Actually Deploy</h1>
<p>These are the, arguably manual, steps to deploy this blog:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-build</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ./result/bin/site rebuild</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ./result/bin/site deploy</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> <span class="er">(</span><span class="bu">cd</span> ibizaman.github.io <span class="kw">&amp;&amp;</span> <span class="fu">git</span> commit <span class="at">-m</span> <span class="st">&#39;MESSAGE&#39;</span> <span class="kw">&amp;&amp;</span> <span class="fu">git</span> tag TAG <span class="kw">&amp;&amp;</span> <span class="fu">git</span> push<span class="kw">)</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> git commit <span class="at">-m</span> <span class="st">&#39;MESSAGE&#39;</span> <span class="kw">&amp;&amp;</span> <span class="fu">git</span> push</span></code></pre></div>
<p>You often don’t need to do the first two steps as <code>./result/bin/watch</code>
takes care of it.</p>
<p>I’m sure I will get annoyed of running these commands at some point in
the future but it’s good enough for now.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>Setup a Hakyll Blog with Nix</title>
    <link href="https://blog.tiserbox.com//posts/2020-10-15-setup-local-blog.html" />
    <id>https://blog.tiserbox.com//posts/2020-10-15-setup-local-blog.html</id>
    <published>2020-10-15T00:00:00Z</published>
    <updated>2020-10-15T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on October 15, 2020
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;hakyll&#39;." href="/tags/hakyll.html">hakyll</a>, <a title="All pages tagged &#39;haskell&#39;." href="/tags/haskell.html">haskell</a>, <a title="All pages tagged &#39;nix&#39;." href="/tags/nix.html">nix</a>
	  
	</section>
    <section>
        <p>This blog post explains how to setup this site. You can find the
source code on <a href="https://github.com/ibizaman/blog">GitHub</a>. We will not
publish yet, it will be all local. Publishing is for the following
post.</p>
<p>The stack we’ll be using is <a href="https://jaspervdj.be/hakyll/">Hakyll</a> on
top of <a href="https://www.haskell.org/">Haskell</a> and
<a href="https://nixos.org/">Nix</a>. I use
<a href="https://www.gnu.org/software/emacs/">Emacs</a> as my editor so there
will be some later blog posts explaining how to set that up.</p>
<p>We will start by writing Nix files and for learning Nix, I recommend
the <a href="https://nixos.org/guides/nix-pills/index.html">Nix pills</a>. I
thought I could simply find example snippets online and learn from
that but the language and the conventions were too alien for me to
understand anything.</p>
<p>I followed <a href="https://robertwpearce.com/hakyll-pt-6-pure-builds-with-nix.html">this blog
post</a>
from Robert Pearce to set things up. I removed the <code>niv</code> part as I am
not using it.</p>
<p>There was an encoding bug as I’m using UTF-8 but by default Hakyll
only understands ASCII encoding. To solve that one, I followed <a href="https://www.slamecka.cz/posts/2020-06-08-encoding-issues-with-nix-hakyll/">this
blog
post</a>
from Ondřej Slámečka. There is a fix in the <a href="https://jaspervdj.be/hakyll/tutorials/faq.html#hgetcontents-invalid-argument-or-commitbuffer-invalid-argument">Hakyll’s
FAQ</a>,
but the post from Ondřej is tailored for nix so I followed it.</p>
<p>I recommend reading both blog posts as they explain things well. That
said, here is my version.</p>
<h1 id="initial-nix-files">Initial Nix Files</h1>
<p>Corresponds to <a href="https://github.com/ibizaman/blog/commit/b4200e564d8464f6783a38a14e8be059ef28b425">this commit</a>.</p>
<p>There are 3 nix files to create, then we will be able to use
<code>hakyll-init</code> to generate the base site.</p>
<h2 id="default.nix"><code>./default.nix</code></h2>
<div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="op">(</span><span class="bu">import</span> <span class="ss">./release.nix</span> <span class="op">{</span> <span class="op">})</span>.project</span></code></pre></div>
<h2 id="shell.nix"><code>./shell.nix</code></h2>
<div class="sourceCode" id="cb2"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="op">(</span><span class="bu">import</span> <span class="ss">./release.nix</span> <span class="op">{</span> <span class="op">})</span>.shell</span></code></pre></div>
<h2 id="release.nix"><code>./release.nix</code></h2>
<div class="sourceCode" id="cb3"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="co"># https://robertwpearce.com/hakyll-pt-6-pure-builds-with-nix.html</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="op">{</span> <span class="va">compiler</span> <span class="op">?</span> <span class="st">&quot;ghc883&quot;</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="op">,</span> <span class="va">pkgs</span> <span class="op">?</span> <span class="bu">import</span> &lt;nixpkgs&gt; <span class="op">{}</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>:</span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>  <span class="va">inherit</span> (<span class="va">pkgs</span>.<span class="va">lib</span>.<span class="va">trivial</span>) <span class="va">flip</span> <span class="va">pipe</span>;</span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>  <span class="va">inherit</span> (<span class="va">pkgs</span>.<span class="va">haskell</span>.<span class="va">lib</span>) <span class="va">appendConfigureFlags</span>;</span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>  <span class="va">haskellPackages</span> <span class="op">=</span> pkgs.haskell.packages.$<span class="op">{</span><span class="va">compiler</span><span class="op">}</span>.override <span class="op">{</span></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>    <span class="va">overrides</span> <span class="op">=</span> <span class="va">hpNew</span><span class="op">:</span> <span class="va">hpOld</span><span class="op">:</span> <span class="op">{</span></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>      <span class="va">hakyll</span> <span class="op">=</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>        pipe</span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>           hpOld.hakyll</span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a>           <span class="op">[</span> <span class="op">(</span>flip appendConfigureFlags <span class="op">[</span> <span class="st">&quot;-f&quot;</span> <span class="st">&quot;watchServer&quot;</span> <span class="st">&quot;-f&quot;</span> <span class="st">&quot;previewServer&quot;</span> <span class="op">])</span></span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a>           <span class="op">];</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a>      <span class="co"># https://www.slamecka.cz/posts/2020-06-08-encoding-issues-with-nix-hakyll/</span></span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a>      <span class="va">hakyll-blog</span> <span class="op">=</span> <span class="op">(</span>hpNew.callCabal2nix <span class="st">&quot;blog&quot;</span> <span class="ss">./.</span> <span class="op">{</span> <span class="op">})</span>.overrideAttrs <span class="op">(</span><span class="va">old</span><span class="op">:</span> <span class="op">{</span></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a>        <span class="va">nativeBuildInputs</span> <span class="op">=</span> old.nativeBuildInputs <span class="kw">or</span> <span class="op">[]</span> <span class="op">++</span> <span class="op">[</span></span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a>          pkgs.makeWrapper</span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a>        <span class="op">];</span></span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a>        <span class="va">postInstall</span> <span class="op">=</span> old.postInstall <span class="kw">or</span> <span class="st">&quot;&quot;</span> <span class="op">+</span> <span class="st">&#39;&#39;</span></span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a><span class="st">        wrapProgram $out/bin/site \</span></span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a><span class="st">          --set LANG &quot;en_US.UTF-8&quot; \</span></span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a><span class="st">          --set LOCALE_ARCHIVE &quot;</span><span class="sc">${</span>pkgs.glibcLocales<span class="sc">}</span><span class="st">/lib/locale/locale-archive&quot;</span></span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a><span class="st">        &#39;&#39;</span><span class="op">;</span></span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a>      <span class="op">});</span></span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a>    <span class="op">};</span></span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a>  <span class="va">project</span> <span class="op">=</span> haskellPackages.hakyll<span class="op">-</span>blog<span class="op">;</span></span>
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a><span class="kw">in</span></span>
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a>  <span class="va">project</span> <span class="op">=</span> project<span class="op">;</span></span>
<span id="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-37"><a href="#cb3-37" aria-hidden="true" tabindex="-1"></a>  <span class="va">shell</span> <span class="op">=</span> haskellPackages.shellFor <span class="op">{</span></span>
<span id="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a>    <span class="va">packages</span> <span class="op">=</span> <span class="va">p</span><span class="op">:</span> <span class="kw">with</span> p<span class="op">;</span> <span class="op">[</span></span>
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a>      project</span>
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb3-41"><a href="#cb3-41" aria-hidden="true" tabindex="-1"></a>    <span class="va">buildInputs</span> <span class="op">=</span> <span class="kw">with</span> haskellPackages<span class="op">;</span> <span class="op">[</span></span>
<span id="cb3-42"><a href="#cb3-42" aria-hidden="true" tabindex="-1"></a>      ghcid</span>
<span id="cb3-43"><a href="#cb3-43" aria-hidden="true" tabindex="-1"></a>      ghcide</span>
<span id="cb3-44"><a href="#cb3-44" aria-hidden="true" tabindex="-1"></a>      brittany</span>
<span id="cb3-45"><a href="#cb3-45" aria-hidden="true" tabindex="-1"></a>      hlint</span>
<span id="cb3-46"><a href="#cb3-46" aria-hidden="true" tabindex="-1"></a>    <span class="op">];</span></span>
<span id="cb3-47"><a href="#cb3-47" aria-hidden="true" tabindex="-1"></a>    <span class="va">withHoogle</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb3-48"><a href="#cb3-48" aria-hidden="true" tabindex="-1"></a>  <span class="op">};</span></span>
<span id="cb3-49"><a href="#cb3-49" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
<p>The first two files are using the <code>release.nix</code> one so let’s dive into
that one.</p>
<p>First, we set some defaults and import some functions:</p>
<div class="sourceCode" id="cb4"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span> <span class="va">compiler</span> <span class="op">?</span> <span class="st">&quot;ghc883&quot;</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="op">,</span> <span class="va">pkgs</span> <span class="op">?</span> <span class="bu">import</span> &lt;nixpkgs&gt; <span class="op">{}</span></span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>:</span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span></span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a>  <span class="va">inherit</span> (<span class="va">pkgs</span>.<span class="va">lib</span>.<span class="va">trivial</span>) <span class="va">flip</span> <span class="va">pipe</span>;</span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a>  <span class="va">inherit</span> (<span class="va">pkgs</span>.<span class="va">haskell</span>.<span class="va">lib</span>) <span class="va">appendConfigureFlags</span>;</span></code></pre></div>
<p>Then we set some overrides. To know what an override is, check the
<a href="https://nixos.org/guides/nix-pills/override-design-pattern.html">Nix pills</a>.</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a>haskellPackages = pkgs.haskell.packages.$<span class="op">{</span><span class="va">compiler</span><span class="op">}</span>.override <span class="op">{</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">overrides</span> <span class="op">=</span> <span class="va">hpNew</span><span class="op">:</span> <span class="va">hpOld</span><span class="op">:</span> <span class="op">{</span></span></code></pre></div>
<p>First for Hakyll, we configure it with <code>watchServer</code> and
<code>previewServer</code> options. Both options are super useful when developing
as the produced <code>./result/bin/site</code> executable will watch for file
changes and rebuild the blog post when needed.</p>
<div class="sourceCode" id="cb6"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>hakyll =</span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a>  pipe</span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a>     hpOld.hakyll</span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a>     <span class="op">[</span> <span class="op">(</span>flip appendConfigureFlags <span class="op">[</span> <span class="st">&quot;-f&quot;</span> <span class="st">&quot;watchServer&quot;</span> <span class="st">&quot;-f&quot;</span> <span class="st">&quot;previewServer&quot;</span> <span class="op">])</span></span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a>     <span class="op">]</span>;</span></code></pre></div>
<p>Second, we create a variable <code>hakyll-blog</code> to compile the blog. The
name <code>"blog"</code> must correspond to the name of the cabal file, which we
will generate later. We also set some flags to make Hakyll able to
read UTF-8 files.</p>
<div class="sourceCode" id="cb7"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a>hakyll<span class="op">-</span>blog = <span class="op">(</span>hpNew.callCabal2nix <span class="st">&quot;blog&quot;</span> <span class="ss">./.</span> <span class="op">{</span> <span class="op">})</span>.overrideAttrs <span class="op">(</span><span class="va">old</span><span class="op">:</span> <span class="op">{</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">nativeBuildInputs</span> <span class="op">=</span> old.nativeBuildInputs <span class="kw">or</span> <span class="op">[]</span> <span class="op">++</span> <span class="op">[</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>    pkgs.makeWrapper</span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a>  <span class="op">];</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a>  <span class="va">postInstall</span> <span class="op">=</span> old.postInstall <span class="kw">or</span> <span class="st">&quot;&quot;</span> <span class="op">+</span> <span class="st">&#39;&#39;</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a><span class="st">  wrapProgram $out/bin/site \</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a><span class="st">    --set LANG &quot;en_US.UTF-8&quot; \</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a><span class="st">    --set LOCALE_ARCHIVE &quot;</span><span class="sc">${</span>pkgs.glibcLocales<span class="sc">}</span><span class="st">/lib/locale/locale-archive&quot;</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a><span class="st">  &#39;&#39;</span><span class="op">;</span></span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a><span class="op">})</span>;</span></code></pre></div>
<p>We then set the <code>project</code> variable used in the <code>default.nix</code> file:</p>
<div class="sourceCode" id="cb8"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a>  project = haskellPackages.hakyll<span class="op">-</span>blog;</span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>in</span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="op">{</span></span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">project</span> <span class="op">=</span> project<span class="op">;</span></span></code></pre></div>
<p>Finally, we set the <code>shell</code> variable and provide some packages useful
for editors, namely <code>ghcide</code>, <code>brittany</code>, <code>hlint</code> and a local <code>hoogle</code>
with <code>withHoogle</code>:</p>
<div class="sourceCode" id="cb9"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a>shell = haskellPackages.shellFor <span class="op">{</span></span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a>  <span class="va">packages</span> <span class="op">=</span> <span class="va">p</span><span class="op">:</span> <span class="kw">with</span> p<span class="op">;</span> <span class="op">[</span></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a>    project</span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a>  <span class="op">];</span></span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a>  <span class="va">buildInputs</span> <span class="op">=</span> <span class="kw">with</span> haskellPackages<span class="op">;</span> <span class="op">[</span></span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a>    ghcid</span>
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a>    ghcide</span>
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a>    brittany</span>
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a>    hlint</span>
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a>  <span class="op">];</span></span>
<span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a>  <span class="va">withHoogle</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span>
<span id="cb9-12"><a href="#cb9-12" aria-hidden="true" tabindex="-1"></a><span class="op">}</span>;</span></code></pre></div>
<h1 id="generate-the-initial-template-blog">Generate the Initial Template Blog</h1>
<p>Corresponds to <a href="https://github.com/ibizaman/blog/commit/990cea6051a978ca407ff2ed3921d7deb3652e5c">this commit</a>.</p>
<p>Next step will take some time because we will run Nix for the first
time. It will fetch and compile every dependencies. It took roughly 1
hour on my laptop.</p>
<div class="sourceCode" id="cb10"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-shell <span class="at">--pure</span> <span class="at">-p</span> haskellPackages.hakyll <span class="at">--run</span> <span class="st">&quot;hakyll-init .&quot;</span></span></code></pre></div>
<p>This will generate a cabal file named after the git repo’s directory. Here is an example, with the following directory structure:</p>
<pre><code>./blog
./blog/.git</code></pre>
<p>Running the <code>hakyll-init .</code> command in the <code>blog</code> directory will create the following files:</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="ex">[nix-shell:~/blog]$</span> hakyll-init .</span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./posts/2015-11-28-carpe-diem.markdown</span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./posts/2015-10-07-rosa-rosa-rosam.markdown</span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./posts/2015-12-07-tu-quoque.markdown</span>
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./posts/2015-08-12-spqr.markdown</span>
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./site.hs</span>
<span id="cb12-7"><a href="#cb12-7" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./images/haskell-logo.png</span>
<span id="cb12-8"><a href="#cb12-8" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./templates/post-list.html</span>
<span id="cb12-9"><a href="#cb12-9" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./templates/default.html</span>
<span id="cb12-10"><a href="#cb12-10" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./templates/archive.html</span>
<span id="cb12-11"><a href="#cb12-11" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./templates/post.html</span>
<span id="cb12-12"><a href="#cb12-12" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./css/default.css</span>
<span id="cb12-13"><a href="#cb12-13" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./index.html</span>
<span id="cb12-14"><a href="#cb12-14" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./about.rst</span>
<span id="cb12-15"><a href="#cb12-15" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./contact.markdown</span>
<span id="cb12-16"><a href="#cb12-16" aria-hidden="true" tabindex="-1"></a><span class="ex">Creating</span> ./blog.cabal</span></code></pre></div>
<p>You get a bunch of files and 4 example blog posts under the <code>posts/</code>
directory. Also, the Cabal file <code>blog.cabal</code> is named from the
directory and must be named that way for the <code>release.nix</code> file to
work correctly, as we established previously.</p>
<p>Then, we can build the <code>./result/bin/site</code> blog executable:</p>
<div class="sourceCode" id="cb13"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> nix-build <span class="at">--show-trace</span></span></code></pre></div>
<p>Finally, we can run the executable to compile the site, including all
4 example blog posts:</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="ex">./result/bin/site</span> build</span></code></pre></div>
<p>This will create the <code>_cache</code> and <code>_site</code> directories. The latter is
where the generated files will be located.</p>
<h1 id="serve-and-watch-for-changes">Serve and Watch For Changes</h1>
<div class="sourceCode" id="cb15"><pre class="sourceCode bash"><code class="sourceCode bash"><span id="cb15-1"><a href="#cb15-1" aria-hidden="true" tabindex="-1"></a><span class="ex">./result/bin/site</span> watch</span></code></pre></div>
<p>Your blog will be up and running at <a href="http://localhost:8000">http://localhost:8000</a>.</p>
<p>You can change posts, add or remove posts, change the css and other
files and the <code>site</code> executable will see those changes and rebuild the
site. You just need to reload the site in your browser.</p>
<h1 id="gitignore">Gitignore</h1>
<p>Corresponds to <a href="https://github.com/ibizaman/blog/commit/731642ddf603bf348a50450055558ca3b57e469d">this commit</a>.</p>
<p>I can’t live with temporary and generated files like that. Let’s add a
<code>.gitignore</code>:</p>
<pre><code>*#
*~
_cache/
_site/
result</code></pre>
<p>The first two lines are for Emacs’ temporary files, you could omit
them.</p>
<h1 id="your-first-post">Your First Post</h1>
<p>I just deleted all 4 example blog posts and <a href="https://github.com/ibizaman/blog/commit/b8f97be67e44bb811524d7235414f8d8899281d5">created another
file</a>.</p>
<h1 id="conclusion">Conclusion</h1>
<p>That’s it, we created our blog with Hakyll and Nix and we can see it
locally. Next up, let’s publish it.</p>
    </section>
</article>
]]></summary>
</entry>
<entry>
    <title>First Post</title>
    <link href="https://blog.tiserbox.com//posts/2020-10-15-first-post.html" />
    <id>https://blog.tiserbox.com//posts/2020-10-15-first-post.html</id>
    <published>2020-10-15T00:00:00Z</published>
    <updated>2020-10-15T00:00:00Z</updated>
    <summary type="html"><![CDATA[<article>
    <section class="header">
        Posted on October 15, 2020
        
    </section>
	<section class="tags">
	  
	  Tags: <a title="All pages tagged &#39;misc&#39;." href="/tags/misc.html">misc</a>
	  
	</section>
    <section>
        <p>I wanted to start writing a blog since a long time. I like sharing my
knowledge so it seemed like a good idea. Time will tell! I needed a
way to write blog posts and publish them so I searched for a tool that
could suite me.</p>
<p>There are a few subjects I want to share about but being a programmer,
talking about code and sharing code snippets seemed natural. This is
often the missing feature for blogging tools or services. Also, I’m an
engineer at heart so I like to build things myself, or at least know
I’m able to modify the tools I use to my heart’s content. I searched
for some DIY alternatives to online services and found Hakyll. It
seemed like the perfect fix because I wanted to write about a language
I fell in love with, Haskell.</p>
<p>So here we are, this is my first blog post, what can I write about?
Let’s talk about how to setup a blog similar to this one. This is my
freshest project anyway.</p>
<p>First caveat, I cannot yet publish this blog post because I’ve got
nowhere to publish it! It’s all local. Next blog post will explain how
to setup a Hakyll blog using nix… locally. The blog post after that
will finally talk about how to publish. And then you’ll be able to
read this, hooray.</p>
<p>The stack used to for this blog is Hakyll on top of Haskell and nix. I
really like how Nix makes development environments fully reproducible.
It’s not easy to setup nor understand so it’s for sure a big
investment.</p>
    </section>
</article>
]]></summary>
</entry>

</feed>
