<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Linux on blog.iankulin.com</title><link>https://blog.iankulin.com/tags/linux/</link><description>Recent content in Linux on blog.iankulin.com</description><generator>Hugo</generator><language>en-AU</language><lastBuildDate>Mon, 28 Jul 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.iankulin.com/tags/linux/index.xml" rel="self" type="application/rss+xml"/><item><title>Getting Ghostty to Work on Synology</title><link>https://blog.iankulin.com/getting-ghostty-to-work-on-synology/</link><pubDate>Mon, 28 Jul 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/getting-ghostty-to-work-on-synology/</guid><description>&lt;p&gt;Ghostty is a terminal application that I don&amp;rsquo;t really &lt;em&gt;need&lt;/em&gt; (it&amp;rsquo;s &lt;a href="https://ghostty.org/docs/about"&gt;listed features&lt;/a&gt; either already exist in the MacOS terminal, or seem so esoteric or marginal that I can&amp;rsquo;t imagine any real benefit from them in my normal use), but I &lt;em&gt;wanted&lt;/em&gt; to be one of the cool kids, so I thought I&amp;rsquo;d give it a try.&lt;/p&gt;
&lt;p&gt;After fiddling around with the themes for a bit I renamed it to &amp;rsquo;term-ghosty.app&amp;rsquo; so I&amp;rsquo;d remember to use it (ie when I pop up spotlight and type &amp;rsquo;term&amp;rsquo; it will come up) and got on with my day. Ten minutes later I&amp;rsquo;d run into a problem.&lt;/p&gt;
&lt;h3 id="the-problem"&gt;The Problem&lt;/h3&gt;
&lt;p&gt;I was ssh&amp;rsquo;d into a Synology NAS, and needed to use a command from two commands ago, in my long experience, pressing the up arrow twice works universally well, but not in Ghostty on this host:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-11.31.38.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This is a purely visual glitch - if I press return at this stage, it will run &lt;code&gt;command one&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My first thought was to CTRL-U to clear the line:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-11.32.00.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I guess not. Oh well, lets &lt;code&gt;clear&lt;/code&gt; and try all this again.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-11.32.07.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So - a clue. This is a Ghostty problem, not a weird shell issue. Also, this wasn&amp;rsquo;t just a visual thing - the command was also not working.&lt;/p&gt;
&lt;p&gt;I logged in with regular terminal to confirm that everything was still working with that.&lt;/p&gt;
&lt;h3 id="xterm-ghostty"&gt;xterm-ghostty&lt;/h3&gt;
&lt;p&gt;If you google &amp;lsquo;ghostty arrow history problem&amp;rsquo; you&amp;rsquo;ll likely find a number of github issues that are all closed after the developer has posted a link to &lt;a href="https://ghostty.org/docs/help/terminfo#ssh"&gt;this part of the docs&lt;/a&gt; explaining that you need to compile the Ghostty&amp;rsquo;s terminfo into the config on this host.&lt;/p&gt;
&lt;p&gt;At first I was a bit aghast at this complicated solution - but we need to keep in mind this is a terminal program that I&amp;rsquo;m sure is only used by tech orientated people who love fiddling with things. This is reflected in other design choices in Ghostty (going into &amp;lsquo;Settings&amp;rsquo; from the menu just opens the config file in a text editor).&lt;/p&gt;
&lt;p&gt;I downloaded &lt;a href="https://iterm2.com/index.html"&gt;iTerm2&lt;/a&gt; as a likely competitor to Ghostty and tried it on the same host - everything worked perfectly. I tried Ghostty on several of my VM&amp;rsquo;s, VPS&amp;rsquo;s and LXC&amp;rsquo;s. All no problem. So what&amp;rsquo;s going on?&lt;/p&gt;
&lt;h3 id="whats-going-on"&gt;What&amp;rsquo;s Going On?&lt;/h3&gt;
&lt;p&gt;If you open your terminal, and type &lt;code&gt;echo $TERM&lt;/code&gt; it will tell you the &lt;code&gt;TERM&lt;/code&gt; value. This is used by the shell to know how to interpret the inputs it&amp;rsquo;s receiving (for example, what to do when the user wants to &lt;code&gt;clear&lt;/code&gt; the screen). Unless you are using Ghostty, it will almost certainly be set to &lt;code&gt;xterm-256color&lt;/code&gt;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.03.36.png" width="808" alt=""&gt;
&lt;p&gt;In Ghostty, it will say &lt;code&gt;xterm-ghostty&lt;/code&gt;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.04.55.png" width="772" alt=""&gt;
&lt;p&gt;The reason I don&amp;rsquo;t have this same problem with Ghostty on my MacBook or the Debian base hosts is that those operating systems have the Ghostty &amp;rsquo;terminfo&amp;rsquo; entries in them, whereas apparently Synology does not (or not yet anyway).&lt;/p&gt;
&lt;p&gt;But that does that explain why iTerm2 works on Synology. Let&amp;rsquo;s look at it&amp;rsquo;s TERM value.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.12.45.png" width="882" alt=""&gt;
&lt;p&gt;Ah, so it&amp;rsquo;s just claiming to be xterm-256color the same as the mac terminal emulator - which all hosts will know since it&amp;rsquo;s an ancient thing. (xterm is the terminal from the X-Windows systems of the 1980&amp;rsquo;s).&lt;/p&gt;
&lt;p&gt;This is a developer choice, Ghostty could also claim it was an xterm-256color and this problem would not have popped up. I&amp;rsquo;m assuming they have decided this short term pain is worth it for some long term gain (of being able to do things an xter-256color terminal emulator can not).&lt;/p&gt;
&lt;h3 id="choices"&gt;Choices&lt;/h3&gt;
&lt;p&gt;Now we have two choices to fix this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Override the TERM choice the Ghostty developer has made for this host&lt;/li&gt;
&lt;li&gt;Compile the correct &lt;code&gt;terminfo&lt;/code&gt; into the config on this host&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Option one means we&amp;rsquo;d lose any special sauce Ghostty does (which I already said I don&amp;rsquo;t need, but could conceivably regret in the future if they do something cool).&lt;/p&gt;
&lt;p&gt;Option two feels like the proper (and is the one that Ghostty recommends).&lt;/p&gt;
&lt;p&gt;I suppose there is a third choice - wait until Synology includes the Ghostty terminfo in their distro. I get the vibe from the slightly scary MOTD when I ssh in that they would really prefer you did not, so I can&amp;rsquo;t seem them going out of their way to include it. Also they are not based on another distro as far as I can see, so they are not going to accidentally include it from an upstream. I feel this choice will never bear fruit.&lt;/p&gt;
&lt;h3 id="overriding-the-term"&gt;Overriding the TERM&lt;/h3&gt;
&lt;p&gt;ssh can have a config set for a host so we can override the TERM value. If you have something like this in &lt;code&gt;~/.ssh/config&lt;/code&gt; we can fix the issue.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Host NAS-DS2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SetEnv TERM=xterm-256color
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.46.46.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And everything works again - I can up arrow to access the history without problems and the &lt;code&gt;clear&lt;/code&gt; command works as advertised. Note that I didn&amp;rsquo;t need to reload the ssh config - it gets read every time you run the ssh command.&lt;/p&gt;
&lt;p&gt;An extra reason for using this approach is that if you have a good naming convention for your hosts (I worked in the &amp;lsquo;data processing&amp;rsquo; department of a bank in the 1990&amp;rsquo;s so I learned good naming conventions for hosts) then you can wild-card this entry to for all of them. You might have guessed that all the Synology NAS&amp;rsquo;s I manage are named &lt;code&gt;NAS-DS&amp;lt;some positive integer&amp;gt;&lt;/code&gt;. Let&amp;rsquo;s change the config to say:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Host NAS-DS*
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SetEnv TERM=xterm-256color
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-12.51.17.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Nice. If I had hundreds of these to deal with, that&amp;rsquo;s definitely the solution I&amp;rsquo;d be going for. Also, if you&amp;rsquo;ve come here because you had this exact problem stop here. This is the best you are going to do.&lt;/p&gt;
&lt;h3 id="installing-the-terminfo"&gt;Installing the terminfo&lt;/h3&gt;
&lt;p&gt;The alternate approach is to extract the xterm-ghostty &lt;code&gt;terminfo&lt;/code&gt; off the current machine (in my case a MacBook) to the other host, and compile it into the available &lt;code&gt;terminfo&lt;/code&gt;&amp;rsquo;s there. This seems more invasive, but it&amp;rsquo;s a per user thing and can be reversed.&lt;/p&gt;
&lt;p&gt;The command given in the &lt;a href="https://ghostty.org/docs/help/terminfo#ssh"&gt;Ghostty docs&lt;/a&gt; is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;infocmp -x xterm-ghostty | ssh YOUR-SERVER -- tic -x -
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Let&amp;rsquo;s try it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screenshot-2025-07-26-at-13.02.16.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This is not that surprising - Synology DSM is minimal (I think it uses busybox) so it&amp;rsquo;s missing lots of these commands. You might think that&amp;rsquo;s okay, I&amp;rsquo;ll compile it on this machine then &lt;code&gt;scp&lt;/code&gt; it across. That would be something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;infocmp -x xterm-ghostty &amp;gt; xterm-ghostty.src
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tic -x -o ./terminfo xterm-ghostty.src
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scp -r ./terminfo/78 ds2_admin@NAS-DS2:~/.terminfo/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But that won&amp;rsquo;t work because you don&amp;rsquo;t have &lt;code&gt;scp&lt;/code&gt; on the Synology either. So perhaps you think you&amp;rsquo;ll enable rysnc in the NAS GUI and rsync the file in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -av ./terminfo/78 ds2_admin@NAS-DS2:~/.terminfo/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which will copy the file over, but it still won&amp;rsquo;t work. It&amp;rsquo;s just too minimal.&lt;/p&gt;
&lt;p&gt;I imagine this process works for other distros or it wouldn&amp;rsquo;t be in the Ghostty docs. But it does not work for Synology NASs in 2025.&lt;/p&gt;</description></item><item><title>Command chaining with NTFY for long running commands</title><link>https://blog.iankulin.com/command-chaining-with-ntfy-for-long-running-commands/</link><pubDate>Mon, 03 Feb 2025 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/command-chaining-with-ntfy-for-long-running-commands/</guid><description>&lt;p&gt;&lt;a href="https://ntfy.sh/"&gt;NTFY&lt;/a&gt; is a great open-source push notification service that&amp;rsquo;s self-hostable or free to use (although I suggest you &lt;a href="https://liberapay.com/ntfy"&gt;pay for it&lt;/a&gt; as I do). I&amp;rsquo;ve written before how I use it with &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;UptimeKuma&lt;/a&gt; for my uptime monitoring, but another common use is just when I&amp;rsquo;m initiating long-running commands and backgrounding them.&lt;/p&gt;
&lt;p&gt;This magic is possible since we can just &lt;code&gt;curl&lt;/code&gt; to send a NTFY notification. For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;curl -d &amp;#34;😀 demo push message via NTFY&amp;#34; ntfy.sh/blog_demo
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Since I&amp;rsquo;m subscribed to the &amp;ldquo;blog_demo&amp;rdquo; topic in NTFY, this message will be pushed to my phone and watch:&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_0056.png" width="640" alt=""&gt;
&lt;p&gt;How I use this is with &amp;lsquo;command chaining&amp;rsquo;. In Linux, you can stack commands together with the &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; characters like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir test_dir &amp;amp;&amp;amp; echo &amp;#34;success&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will create the directory, then print &amp;ldquo;success&amp;rdquo; to the shell. I could use it like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nohup rsync &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;rvits &lt;span style="color:#81a1c1"&gt;--&lt;/span&gt;bwlimit&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#b48ead"&gt;20&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)/&amp;#34;&lt;/span&gt; ds1_admin&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;&lt;span style="color:#b48ead"&gt;100.78&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;2.105&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)&amp;#34;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; output&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log &lt;span style="color:#b48ead"&gt;2&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; curl &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;d &lt;span style="color:#a3be8c"&gt;&amp;#34;💾 upload to vm500-kr complete&amp;#34;&lt;/span&gt; ntfy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sh&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;blog_demo &lt;span style="color:#81a1c1"&gt;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Both commands will run in the background, and the output of the first command is directed into the &amp;lsquo;output.log&amp;rsquo; file. If the rsync file transfer (that is going to take all night) finishes successfully, then the message saying it&amp;rsquo;s complete will be sent.&lt;/p&gt;
&lt;p&gt;What about if it fails? Well, posix has you covered here too. There&amp;rsquo;s a &lt;code&gt;||&lt;/code&gt; chaining operator that only runs if a command fails.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir invalid/name &amp;amp;&amp;amp; (echo &amp;#34;Directory created successfully.&amp;#34;) || (echo &amp;#34;Failed to create directory.&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the command above, if we already have a directory called &lt;code&gt;invalid&lt;/code&gt;, the &lt;code&gt;mkdir&lt;/code&gt; will work and we&amp;rsquo;ll get the message &amp;ldquo;Directory created successfully.&amp;rdquo;. If &lt;code&gt;invalid&lt;/code&gt; doesn&amp;rsquo;t exist, the command will fail and we&amp;rsquo;ll get the message &amp;ldquo;Failed to create directory.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Note that I&amp;rsquo;ve added some parenthesis - it makes things clearer for the reader, and the command line parser.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s apply this to our slow file transfer:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nohup rsync &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;rvits &lt;span style="color:#81a1c1"&gt;--&lt;/span&gt;bwlimit&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#b48ead"&gt;20&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)/&amp;#34;&lt;/span&gt; ds1_admin&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;&lt;span style="color:#b48ead"&gt;100.78&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;2.105&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)&amp;#34;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; output&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log &lt;span style="color:#b48ead"&gt;2&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; curl &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;d &lt;span style="color:#a3be8c"&gt;&amp;#34;💾 upload to vm500-kr complete&amp;#34;&lt;/span&gt; ntfy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sh&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;blog_demo &lt;span style="color:#81a1c1"&gt;||&lt;/span&gt; curl &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;d &lt;span style="color:#a3be8c"&gt;&amp;#34;⚠️ upload to vm500-kr failed!&amp;#34;&lt;/span&gt; ntfy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sh&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;blog_demo &lt;span style="color:#81a1c1"&gt;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we&amp;rsquo;ll get a push message for completion or failure. There is one more little bit of housekeeping to do though. When we curl ntfy like this, it actually returns some JSON:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-12-28-at-11.00.08-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-12-28-at-11.00.08-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Since we&amp;rsquo;re running this whole thing backgrounded, we really want that to go to the &lt;code&gt;output.log&lt;/code&gt; file with the other output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nohup rsync &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;rvits &lt;span style="color:#81a1c1"&gt;--&lt;/span&gt;bwlimit&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#b48ead"&gt;20&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)/&amp;#34;&lt;/span&gt; ds1_admin&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;&lt;span style="color:#b48ead"&gt;100.78&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;2.105&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/volume1/media/video/Movies/Night of the Living Dead (1968)&amp;#34;&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; output&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log &lt;span style="color:#b48ead"&gt;2&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;&amp;gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; curl &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;d &lt;span style="color:#a3be8c"&gt;&amp;#34;💾 upload to vm500-kr complete&amp;#34;&lt;/span&gt; ntfy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sh&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;blog_demo &lt;span style="color:#81a1c1"&gt;&amp;gt;&amp;gt;&lt;/span&gt; output&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log &lt;span style="color:#81a1c1"&gt;||&lt;/span&gt; curl &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;d &lt;span style="color:#a3be8c"&gt;&amp;#34;⚠️ upload to vm500-kr failed!&amp;#34;&lt;/span&gt; ntfy&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;sh&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;blog_demo &lt;span style="color:#81a1c1"&gt;&amp;gt;&amp;gt;&lt;/span&gt; output&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log &lt;span style="color:#81a1c1"&gt;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>rsync between Synology NAS</title><link>https://blog.iankulin.com/rsync-between-synology-nas/</link><pubDate>Mon, 30 Sep 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rsync-between-synology-nas/</guid><description>&lt;p&gt;A while ago, I devised a complicated system where I could drop files in a web interface running on an LXD container and the files would then magically appear in a directory on a remote NAS in the morning. It turned out to not be very robust, and I gave up on it after a while.&lt;/p&gt;
&lt;p&gt;Also, really there should be no need for it - underneath, it was just using &lt;code&gt;rsync&lt;/code&gt; to move the files, so why not just do that direct from one NAS to another? Well, mainly because my NASs are all Synology - which I love, and they&amp;rsquo;ve been great, but in an effort to make them usable by muggles, Synology tend to somewhat complicate things for Linux command line wizards.&lt;/p&gt;
&lt;p&gt;It turns out to be totally possible to command line &lt;code&gt;rsync&lt;/code&gt;, including doing it over Tailscale, but there&amp;rsquo;s a couple of gotchas along the way.&lt;/p&gt;
&lt;h3 id="rsync-the-synology-way"&gt;rsync the Synology way&lt;/h3&gt;
&lt;p&gt;A reasonable question would be why didn&amp;rsquo;t I use the Synology rsync user interface to do all this - it&amp;rsquo;s right there in Control Panel / File Services? I did actually look at doing that, but after five minutes I couldn&amp;rsquo;t figure it out, so yeah na. It&amp;rsquo;s the command line for me.&lt;/p&gt;
&lt;h3 id="steps"&gt;Steps&lt;/h3&gt;
&lt;p&gt;The plan for the rest of this post is just to run through, in approximate order, the steps you&amp;rsquo;ll need to take to get &lt;code&gt;rsync&lt;/code&gt; working from the command line to sync files between two synology NASs. It&amp;rsquo;s probably also helpful between a real system and a Synology NAS. I&amp;rsquo;m going to talk about the &amp;rsquo;local&amp;rsquo; NAS (where we&amp;rsquo;ll be running the rsync command) and &amp;lsquo;remote&amp;rsquo; one. This is just for convenience - you might have two local or two remote NASs - I don&amp;rsquo;t judge. I&amp;rsquo;m just calling mine &amp;rsquo;local&amp;rsquo; and &amp;lsquo;remote&amp;rsquo; for this post that so you know which device I&amp;rsquo;m talking about.&lt;/p&gt;
&lt;h4 id="ssh"&gt;ssh&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;rsync&lt;/code&gt; works over an ssh connection, so you need to be able to ssh from one NAS to another without entering a password first. To test it, ssh into the local NAS, then without logging out, ssh into the remote NAS from the local one. If that works without asking for a password you&amp;rsquo;ve completed this step and can just ctrl-D to drop back to the local NAS.&lt;/p&gt;
&lt;p&gt;If the issue is that it asked for a password, that just means you need to install your public ssh keys on the remote. I usually do this with the &lt;code&gt;ssh-copy-id&lt;/code&gt; command on regular Linux, Mac and BSD systems, but that&amp;rsquo;s not available at the Synology command line so we&amp;rsquo;ll have to do it the old fashioned way.&lt;/p&gt;
&lt;p&gt;Anything to do with ssh is stored in a hidden directory, &lt;code&gt;.ssh&lt;/code&gt; in a user&amp;rsquo;s home directory. For example you can check you&amp;rsquo;ve got public keys with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ls -la ~/.ssh/id_rsa.pub
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These are the keys you want to add to the remote NASs authorised keys, so we&amp;rsquo;ll use ssh (with a password) to add them to the end of that file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssh &amp;lt;user&amp;gt;@&amp;lt;remote NAS address&amp;gt; &amp;#39;cat &amp;gt;&amp;gt; ~/.ssh/authorized_keys&amp;#39; &amp;lt; ~/.ssh/id_rsa.pub
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You need to substitute your remote NASs username and address, so mayby it would look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssh nas1_admin@83.78.2.105 &amp;#39;cat &amp;gt;&amp;gt; ~/.ssh/authorized_keys&amp;#39; &amp;lt; ~/.ssh/id_rsa.pub
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When you execute this, it will ask for the remote password, but once it&amp;rsquo;s worked you should be able to ssh in and it allows that without using a password.&lt;/p&gt;
&lt;h4 id="tailscale-out"&gt;Tailscale out&lt;/h4&gt;
&lt;p&gt;Perhaps you didn&amp;rsquo;t get as far as needing the ssh password, because when you tried to ssh to the remote, ssh didn&amp;rsquo;t even recognise the domain. If you are using Tailscale to connect your devices (which I recommend) then there are two tricks needed.&lt;/p&gt;
&lt;p&gt;Trick one is to get around the fact that since DSM 7, Synology have prevented (for good security reasons) external packages from making outbound connections. So you&amp;rsquo;ll be able to use Tailscale to access the Synology web interface, or even ssh &lt;em&gt;into&lt;/em&gt; it, but you won&amp;rsquo;t be able to ssh &lt;em&gt;out&lt;/em&gt; of it. When I first discovered this, I was running &lt;code&gt;ip a&lt;/code&gt; at the command line in the local NAS and noticed that the tailscale IP was not even listed - it was as if Tailscale wasn&amp;rsquo;t running, but I knew it was since I had ssh&amp;rsquo;d in with the Tailscale address.&lt;/p&gt;
&lt;p&gt;Tailscale have a fix for &lt;a href="https://tailscale.com/kb/1131/synology#enable-outbound-connections"&gt;enabling outbound connections via Tailscale on Synology&lt;/a&gt;, you need to run a thing on reboot to enable the TUN.&lt;/p&gt;
&lt;p&gt;Trick two is that even after you&amp;rsquo;ve done that and rebooted and can see the tailscale interface when you run &lt;code&gt;ip a&lt;/code&gt;, you still won&amp;rsquo;t be able to use the Tailscale &amp;lsquo;magic&amp;rsquo; DNS but will have to use the Tailscale IP address for the remote when you ssh (and later rsync) to it. So I can&amp;rsquo;t use:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ssh nas1_admin@NAS-01&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;as I would normally from my laptop, I have to use &lt;code&gt;ssh nas1_admin@104.43.22.181&lt;/code&gt; If you are not sure of the Tailscale IP for your remote, have a look at your &lt;a href="https://login.tailscale.com/admin/machines"&gt;machines list&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="turn-rsync-on"&gt;Turn rsync on&lt;/h4&gt;
&lt;p&gt;Via the web interface on both Synologys, you&amp;rsquo;ll need to enable rsync. The setting is in &lt;code&gt;Control Panel | File Services | rsync&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2024-08-25-at-1.56.57-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-08-25-at-1.56.57-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Leave the port as 22 and don&amp;rsquo;t bother with the other settings, but do hit &lt;code&gt;Apply&lt;/code&gt; at the bottom to save the change.&lt;/p&gt;
&lt;h4 id="give-it-a-try"&gt;Give it a try&lt;/h4&gt;
&lt;p&gt;We&amp;rsquo;re now at the stage where you should be able to ssh into the remote NAS from the local one without being asked for a password, and rsync is turned on both ends, so in theory, you should be able to do something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -rvitn /volume1/ nas1_admin@104.43.22.181:/volume1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m not going to go into all the flags for rsync (the internet has plenty of good guides for that) except to say that the &amp;rsquo;n&amp;rsquo; on the end of the flags in the command above means that no files will actually be moved, it will do a &amp;lsquo;dry run&amp;rsquo; and tell you what it would have done if you let it.&lt;/p&gt;
&lt;p&gt;Note that if you have a jazillion files, this could take a while, you might be better to limit it to a smaller sub directory such as &lt;code&gt;/volume1/media/music/napster/Metallica&lt;/code&gt;/&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/yo-dawg-heard-you.jpg" width="497" alt=""&gt;
&lt;p&gt;The other bit of free rsync advice I&amp;rsquo;ll give you is to look carefully at the source and destination directories in the command above. The source sub directory has a trailing &amp;lsquo;/&amp;rsquo;, the destination does not. If you mess this up you&amp;rsquo;ll be making directories inside your directories dawg.&lt;/p&gt;
&lt;p&gt;In theory once you&amp;rsquo;re at this point, everything should work. But here&amp;rsquo;s a couple of other bumps / thoughts.&lt;/p&gt;
&lt;h4 id="eadir"&gt;@eaDir&lt;/h4&gt;
&lt;p&gt;Synology has a bunch of hidden directories with metadata stuff. My advice is don&amp;rsquo;t mess with them, but also don&amp;rsquo;t sync them over either. Tell rsync to ignore them. Same for the recycle bin.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -rvitn --exclude &amp;#39;*@eaDir*&amp;#39; --exclude &amp;#39;#recycle*&amp;#39; /volume1/ nas1_admin@104.43.22.181:/volume1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="permissions"&gt;Permissions&lt;/h3&gt;
&lt;p&gt;The user that you&amp;rsquo;re ssh&amp;rsquo;ing with needs to have permissions to all the places you are rsync&amp;rsquo;ing files to. Even though I&amp;rsquo;ve only ever had one user for each of my Synology NAS&amp;rsquo;s, and everything has been done by that one user either through the web GUI or command line, the files and directories on my NAS have a mixture of owners (my user and root) and permissions. Someone smarter than me could probably figure out why - and if your NAS has to include files from multiple users etc, you are going to need to do that. Because I like sledgehammers, all I did was ssh into the remote and:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo chown -R nas1_admin:users /volume1/media
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo chmod -R 775 /volume1/media
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="throttling-bandwidth"&gt;Throttling bandwidth&lt;/h4&gt;
&lt;p&gt;If I saturate the downlink at my remote site while I&amp;rsquo;m rsync-ing a bunch of files, the users there will be unhappy when they can&amp;rsquo;t stream video reliability or if they&amp;rsquo;re getting killed in online games due to lag.&lt;/p&gt;
&lt;p&gt;rsync has a flag for that. If we want to limit the transfer bandwidth to 500KB it could look like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -rvitn --bwlimit=500 --exclude &amp;#39;*@eaDir*&amp;#39; --exclude &amp;#39;#recycle*&amp;#39; /volume1/ nas1_admin@104.43.22.181:/volume1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="get-destructive"&gt;Get destructive&lt;/h4&gt;
&lt;p&gt;If you only want to sync the files one way from your local to remote, then we can add a flag that will delete any files on the remote machine that are not present on the local one. Obviously use with care, and run with the -n flag first to see what&amp;rsquo;s going to get chopped.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -rvitn --bwlimit=500 --exclude &amp;#39;*@eaDir*&amp;#39; --exclude &amp;#39;#recycle*&amp;#39; /volume1/ nas1_admin@104.43.22.181:/volume1 --del
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="do-something-else"&gt;Do something else&lt;/h4&gt;
&lt;p&gt;The first few times you do this, it will be exciting watching the terminal window as rsync carefully checks for each file and directory and copies them over as needed, and it will also be helpful to see what errors might pop up so you can sort them out.&lt;/p&gt;
&lt;p&gt;Eventually though, it will be so routine and error free you&amp;rsquo;d rather do something else, so you&amp;rsquo;ll wander off and leave it, then curse when you return to find your laptop turned itself off due to inactivity and wrecked the rsync. Don&amp;rsquo;t panic, rsync is robust and will pick right up next time you run it without damaging any files, but you might also consider doing it all on remote control.&lt;/p&gt;
&lt;p&gt;On the local NAS, create a file called &lt;code&gt;sync-media.sh&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5e81ac;font-style:italic"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nohup rsync -rvitn --bwlimit&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#b48ead"&gt;500&lt;/span&gt; --exclude &lt;span style="color:#a3be8c"&gt;&amp;#39;*@eaDir*&amp;#39;&lt;/span&gt; --exclude &lt;span style="color:#a3be8c"&gt;&amp;#39;#recycle*&amp;#39;&lt;/span&gt; /volume1/ nas1_admin@104.43.22.181:/volume1 &amp;gt; sync_media.log 2&amp;gt;&lt;span style="color:#eceff4"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt; &lt;span style="color:#eceff4"&gt;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Make it executable:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;chmod +x sync_media.sh
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once you run it &lt;code&gt;./sync-media.sh&lt;/code&gt; you can log off and let it do it&amp;rsquo;s thing.&lt;/p&gt;</description></item><item><title>User environment variables are not available in cron</title><link>https://blog.iankulin.com/user-environment-variables-are-not-available-in-cron/</link><pubDate>Mon, 15 Jul 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/user-environment-variables-are-not-available-in-cron/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-02-at-4.13.13-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m used to using the &lt;code&gt;docker-compose.yaml&lt;/code&gt; or &lt;code&gt;dockerfile&lt;/code&gt; to set environment variables for containers running my apps, but ran into an issue recently where the variable seemed to be set some of the time, but at others it didn&amp;rsquo;t appear to exist.&lt;/p&gt;
&lt;p&gt;I had a script set to run by &lt;code&gt;cron&lt;/code&gt; inside the container, and it turns out that the environment variables set for the container are available in the user space, but not in &lt;code&gt;cron&lt;/code&gt;, even if running with that user&amp;rsquo;s permissions. This is probably old news to established Linux users but it threw me for a while. I&amp;rsquo;d &lt;code&gt;exec&lt;/code&gt; into the container and the script would work perfectly, then wait another minute for &lt;code&gt;cron&lt;/code&gt; to run it and it would fail 🤦‍♀️ It was exasperated by my discovery that I didn&amp;rsquo;t know how to console.log debug from inside a container cron job as well - the subject of an earlier post.&lt;/p&gt;
&lt;p&gt;Once I&amp;rsquo;d narrowed it down to this issue and googleconfirmed it, I didn&amp;rsquo;t really come up with an elegant solution either. You may think &amp;ldquo;buy hey, everything in Linux is a file, it must be in proc somewhere&amp;rdquo;, and you&amp;rsquo;d be right, sort of.&lt;/p&gt;
&lt;p&gt;In Linux, to see your own environment variables, you type in &lt;code&gt;env&lt;/code&gt;. I think this probably works by grabbing them from &lt;code&gt;proc&lt;/code&gt; under &lt;code&gt;/proc/&amp;lt;PID&amp;gt;/environ&lt;/code&gt; where &lt;PID&gt; is the process id of the shell. We can get our own process id with the &lt;code&gt;ps&lt;/code&gt; command. If we&amp;rsquo;re the root user (which I am in my container) we can see &lt;em&gt;someone else&amp;rsquo;s&lt;/em&gt; process ids with &lt;code&gt;ps -u &amp;lt;username&amp;gt;&lt;/code&gt;, get the process id for the shell and look in proc. So I tried that from the cron script - it turns out no.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2024-07-02-at-6.51.16-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I could see them, but it didn&amp;rsquo;t include the environment variable passed in from Docker that was available at the entry point. This is all a bit weird to me - I&amp;rsquo;m not sure why we&amp;rsquo;re the same user, with the same permissions but with a new seperate environment. I guess there is a reason, it&amp;rsquo;s just not apparent to me.&lt;/p&gt;
&lt;h3 id="the-hack"&gt;The Hack&lt;/h3&gt;
&lt;p&gt;To get around this, I now save the environment variable I&amp;rsquo;m interested in to a file in the script that runs at the entry point:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Save the environment variable IMAGE_URL into&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# a file for later use in the cron script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;env &lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; grep IMAGE_URL &lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt; image_url&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then in my script that is run by cron, I reconstitute it from the file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Read the file saved by the entry point script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# and extract the environment variable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;while&lt;/span&gt; IFS&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#39;=&amp;#39;&lt;/span&gt; read &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;r key value&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[[&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;key &lt;span style="color:#81a1c1"&gt;==&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;IMAGE_URL&amp;#34;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;]];&lt;/span&gt; then
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;export&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;$key=$value&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fi
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;done &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/image_url.txt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That works fine. Software testers will be looking at this solution thinking &amp;ldquo;What about the case where the environment variable isn&amp;rsquo;t set, but the file from the last run is still there?&amp;rdquo; Worry not, bug finding person. It&amp;rsquo;s a container so everything&amp;rsquo;s ephemeral. The file with the environment variable can only be there on runs when that environment variable has been set.&lt;/p&gt;</description></item><item><title>Beginning Node App Security</title><link>https://blog.iankulin.com/beginning-node-app-security/</link><pubDate>Fri, 16 Feb 2024 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/beginning-node-app-security/</guid><description>&lt;p&gt;Since I&amp;rsquo;m using Tailscale to painlessly manage all my networking on the homeserver here and my remotes, I&amp;rsquo;ve had the luxury of being a bit casual about the security of my internal apps and self hosted dev tools. I&amp;rsquo;m currently iterating on a web app that requires public access, and is therefore up on a VPS and exposed to all the evils of the open internet.&lt;/p&gt;
&lt;p&gt;I am in no way a security expert, but here&amp;rsquo;s a few of the (reasonably simple) steps I&amp;rsquo;ve taken to secure my node app.&lt;/p&gt;
&lt;h3 id="put-it-behind-nginx"&gt;Put it behind Nginx&lt;/h3&gt;
&lt;p&gt;I could just change the port number of the Node app to listen to port 80 and connect it directly to the world, but Nginx is battle hardened for outward facing tasks so that seems safer. Additionally, it opens up a lot of functionality such as putting my app on a subdomain and some other security options we&amp;rsquo;ll come to. Putting Nginx in front of your app like this is called &amp;lsquo;reverse proxying&amp;rsquo;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	server_name sub.example.com;	location / { 		proxy_pass http://localhost:3000;			proxy_http_version 1.1;		proxy_set_header Upgrade $http_upgrade;		proxy_set_header Connection &amp;#39;upgrade&amp;#39;;		proxy_set_header Host $host;		proxy_cache_bypass $http_upgrade;	}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="basic-auth"&gt;Basic Auth&lt;/h3&gt;
&lt;p&gt;One of the Nginx options is the ability to turn on &amp;lsquo;&lt;a href="https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/"&gt;basic auth&lt;/a&gt;&amp;rsquo;. This can be enabled for a route, subdomain, or a whole domain. It forces a user to authenticate before being able to access resources from that area. It&amp;rsquo;s basic in the sense that the password is manually set on the server in a file that the nginx conf points to. Ideally your app will have comprehensive auth built in, but (especially when you are still developing it) basic auth is a quick and easy way to prevent all of the internet from accessing your app.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;	server_name sub.example.com;	location / {		auth_basic &amp;#34;Secure app&amp;#34;;	 auth_basic_user_file /etc/nginx/.htpasswd;	}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="https"&gt;HTTPS&lt;/h3&gt;
&lt;p&gt;Enforcing SSL connections is much easier than it used to be (thank you L&lt;a href="https://blog.iankulin.com/certbot-lets-encrypt-are-great/"&gt;et&amp;rsquo;s Encrypt and Certbot&lt;/a&gt;) and it keeps all the data being sent between your app and the user&amp;rsquo;s browser encrypted - including the username and password you are using for your auth.&lt;/p&gt;
&lt;h3 id="logging"&gt;Logging&lt;/h3&gt;
&lt;p&gt;Ensuring that logs are turned on means that you&amp;rsquo;ve got some chance of detecting problems and possible attacks. In fact, you&amp;rsquo;ll probably be aghast at the number of bots that start accessing your server as soon as it&amp;rsquo;s live. For the most part, they are probing for the existence of known vulnerabilities in well known packages - may of the php. When I look up the IP addresses for these, they almost always come from China or Eastern Europe.&lt;/p&gt;
&lt;p&gt;Note that logging does involve some future maintenance. Logs need rotated and deleted or we&amp;rsquo;ll soon be running out of disk space.&lt;/p&gt;
&lt;h3 id="fail2ban"&gt;Fail2ban&lt;/h3&gt;
&lt;p&gt;Manually checking the logs is not effective, we need to automate this a bit. The things I&amp;rsquo;m looking for in my system is brute force attempts at breaking the basic auth, and the same with SSH.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/fail2ban/fail2ban"&gt;Fail2Ban&lt;/a&gt; can automate this (and many other things, but I&amp;rsquo;m just using these two) by scanning the logs for failed attempts. There are various settings to determine the thresholds - say check for 5 failed attempts in 10 minutes then ban the IP address for 30 minutes. Once the threshold is met, Fail2Ban updates iptables (the internal firewall) to block them.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;novel&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;ironic&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;~$&lt;/span&gt; sudo fail2ban&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;client status sshdStatus &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; the jail&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; sshd&lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Filter&lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Currently failed&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Total failed&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;2960&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; &lt;span style="color:#bf616a"&gt;File&lt;/span&gt; list&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;log&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;auth&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; Actions &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Currently banned&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;6&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Total banned&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;483&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; Banned &lt;span style="color:#bf616a"&gt;IP&lt;/span&gt; list&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;218.92&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0.27&lt;/span&gt; &lt;span style="color:#b48ead"&gt;61.177&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;172.136&lt;/span&gt; &lt;span style="color:#b48ead"&gt;61.177&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;172.140&lt;/span&gt; &lt;span style="color:#b48ead"&gt;218.92&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0.113&lt;/span&gt; &lt;span style="color:#b48ead"&gt;218.92&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0.31&lt;/span&gt; &lt;span style="color:#b48ead"&gt;218.92&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;0.76&lt;/span&gt;ian&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;novel&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;ironic&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;~$&lt;/span&gt; sudo fail2ban&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;client status nginx&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;http&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;authStatus &lt;span style="color:#81a1c1;font-weight:bold"&gt;for&lt;/span&gt; the jail&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; nginx&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;http&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;auth&lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Filter&lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Currently failed&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;1&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Total failed&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;6&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; &lt;span style="color:#bf616a"&gt;File&lt;/span&gt; list&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;log&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;nginx&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;error&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;log&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; Actions &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Currently banned&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;0&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;|-&lt;/span&gt; Total banned&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	&lt;span style="color:#b48ead"&gt;1&lt;/span&gt; &lt;span style="color:#bf616a"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; Banned &lt;span style="color:#bf616a"&gt;IP&lt;/span&gt; list&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;	ian&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;novel&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;ironic&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;~$&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the output above, you can see there are 6 ip addresses currently blocked for trying to crack SSH, and there&amp;rsquo;s been 483 banned in the couple of days since I turned it on - so this is a very common attack vector. The basic auth one just has a single ban (from when I tested it). I&amp;rsquo;m not sure why I like looking at the list of bans so much, but I do!&lt;/p&gt;
&lt;h3 id="cloud-firewall"&gt;Cloud Firewall&lt;/h3&gt;
&lt;p&gt;Many VPS providers will have a cloud firewall (although they may call it something else). We can use this to lock down all the ports we are not using to massively reduce the attack surface for this machine. One of the very appealing things about this firewall which is external to the VPS is that it&amp;rsquo;s accessed via the VPS provider web interface - so it&amp;rsquo;s not possible to lock yourself out if you make a mistake - as opposed to when you&amp;rsquo;re SSHd in and fiddling with iptables.&lt;/p&gt;
&lt;p&gt;Since this VPS is just running web apps, I just have ports 80, 443 and 22 open.&lt;/p&gt;
&lt;h3 id="no-root-login"&gt;No root login&lt;/h3&gt;
&lt;p&gt;By default, Ubuntu does not allow root login by password, but once I&amp;rsquo;ve added a new user and added them to the sudo group, I turn it off entirely. Most of those SSH attempts that failed would have been trying common user names including root, so may as well take it out of the possibilities.&lt;/p&gt;
&lt;h3 id="ssh-keys"&gt;SSH keys&lt;/h3&gt;
&lt;p&gt;Apart from being more convenient, well managed SSH keys are much safer than using passwords. So my new user copies up their keys and I set that user to no login with password as well.&lt;/p&gt;
&lt;h3 id="updates"&gt;Updates&lt;/h3&gt;
&lt;p&gt;One of the wonderful things about open source and the modern web, is that as new vulnerabilities are being discovered, they are being patched. We only get them if we run updates though. It&amp;rsquo;s possible (and recommended) to use &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-keep-ubuntu-20-04-servers-updated"&gt;automatic updates&lt;/a&gt;, but I have a weekend ansible routine to do them that I like to look at the output of to check everything&amp;rsquo;s healthy.&lt;/p&gt;
&lt;h3 id="monitoring"&gt;Monitoring&lt;/h3&gt;
&lt;p&gt;I use a &lt;a href="https://blog.iankulin.com/simple-api-endpoint-in-go/"&gt;very simple monitoring system&lt;/a&gt; for all the VM&amp;rsquo;s and containers in my Tailnet - just checking the root disk space and available memory. This is exposed as an http endpoint, then checked by &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt;. That&amp;rsquo;s better than nothing, but for a production system not really enough. This is one of the areas I&amp;rsquo;ll revisit in the future.&lt;/p&gt;</description></item><item><title>Docker volume backup is more complicated than it should be</title><link>https://blog.iankulin.com/docker-volume-backup-is-more-complicated-than-it-should-be/</link><pubDate>Fri, 17 Nov 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/docker-volume-backup-is-more-complicated-than-it-should-be/</guid><description>&lt;p&gt;&lt;a href="https://unccelearn.org/course/view.php?id=128&amp;page=overview&amp;lang=en"&gt;&lt;img src="https://blog.iankulin.com/images/big.jpg" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When I set up my first Docker container (I think for &lt;a href="https://blog.iankulin.com/uptime-kuma-nfty/"&gt;Uptime Kuma&lt;/a&gt;), I had read around and understood there were two choices for persistent; &lt;em&gt;bind mounts&lt;/em&gt; (where the data inside the container is effectively a symlink to a location on the local file system) or &lt;em&gt;name volumes&lt;/em&gt; where Docker abstracted that away a bit, so you didn&amp;rsquo;t have to worry where it was - I sort of understood Docker &amp;lsquo;managed&amp;rsquo; it.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been lazily doing my &amp;lsquo;backups&amp;rsquo; by just saving snapshots of entire VM&amp;rsquo;s - which works really well, Proxmox handles the scheduling of them, I regularly test them (every month I run off the backup production server for a couple of days from the backups). I don&amp;rsquo;t mind that backing up up an entire VM for a couple of Dockerised apps is expensive in disk because local disk is cheap and it&amp;rsquo;s super convenient.&lt;/p&gt;
&lt;p&gt;However, I&amp;rsquo;ve got a couple of projects on the list where I&amp;rsquo;d like to move a container and it&amp;rsquo;s data between VM&amp;rsquo;s. One is trying out Jellyfin in Docker in an LXC, and another is moving the containers on my general utility dockerhost to a new VM with a bit larger disk since that seems easier than expanding the disk.&lt;/p&gt;
&lt;p&gt;I assumed I&amp;rsquo;d be stoping the container and doing something like &lt;code&gt;docker export portainer_data somebackupfile.name&lt;/code&gt; then moving that file over to the new system and running &lt;code&gt;docker import portainer_data somebackupfile.name&lt;/code&gt; to re-create it.&lt;/p&gt;
&lt;p&gt;But no, that&amp;rsquo;s not how it works. According to the Docker people, I need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use inspect to find out the internal data directories of the container&lt;/li&gt;
&lt;li&gt;Stop the container&lt;/li&gt;
&lt;li&gt;Create a new generic linux container&lt;/li&gt;
&lt;li&gt;Have it mount the docker volumes&lt;/li&gt;
&lt;li&gt;Also have it bind mount to the current directory&lt;/li&gt;
&lt;li&gt;Run a command inside the container to tar ball the internal data directory and save it to the bind mount&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The only real concession to usability along the way is that there&amp;rsquo;s a &lt;code&gt;--volumes_from&lt;/code&gt; flag that saves you from extracting all the volume names from a &lt;code&gt;docker inspect&lt;/code&gt; of the container whose data you want to back up.&lt;/p&gt;
&lt;h3 id="example"&gt;Example&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s run through those steps with an example. I&amp;rsquo;m going to set up &lt;a href="https://uptime.kuma.pet/"&gt;Uptime Kuma&lt;/a&gt; in Docker. I&amp;rsquo;ll use the &lt;a href="https://github.com/louislam/uptime-kuma/blob/master/docker/docker-compose.yml"&gt;suggested compose file&lt;/a&gt; which creates a named volume &lt;code&gt;uptime-kuma&lt;/code&gt;. I tested that&amp;rsquo;s up and running on port 3001 - when I visited there, it wanted me to create an admin account.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-9.55.47-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-9.55.47-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For demo purposes, I created the admin user &lt;code&gt;ian&lt;/code&gt; and set up Uptime Kuma to monitor Google for us.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-10.39.40-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-10.39.40-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you started the app from a docker compose file, you can just look in there to see what the internal data directories that are being mounted to are:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;version: &amp;#39;3.8&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; uptime-kuma:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; image: louislam/uptime-kuma:1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; container_name: uptime-kuma
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - uptime-kuma:/app/data
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ports:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &amp;#34;3001:3001&amp;#34; # &amp;lt;Host Port&amp;gt;:&amp;lt;Container Port&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; restart: always
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;volumes:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; uptime-kuma:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;or alternatively use the &lt;code&gt;docker inspect &amp;lt;container name&amp;gt;&lt;/code&gt; command. You&amp;rsquo;ll get back a barrage of Json - somewhere in there will be the mount details:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;Mounts&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Type&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;volume&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Name&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;uptimekuma_uptime-kuma&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Source&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/var/lib/docker/volumes/uptimekuma_uptime-kuma/_data&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Destination&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;/app/data&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Driver&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;local&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Mode&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;z&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;RW&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt;&lt;span style="color:#eceff4"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;Propagation&amp;#34;&lt;/span&gt;&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Either way, we now know that the internal directory for data is &lt;code&gt;/app/data&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Next stop the container with &lt;code&gt;docker stop uptime-kuma&lt;/code&gt;, then type in this bad boy based on the one in the docs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo docker run --rm --volumes-from uptime-kuma -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /app/data
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The highlighted bits are the pieces I changed for our demo - the name of our container and the internal data directory for it that we found in the steps above. Pulling down an entire &lt;a href="https://hub.docker.com/_/ubuntu"&gt;Ubuntu container&lt;/a&gt; seemed overkill - we&amp;rsquo;re just running a tar command so perhaps &lt;a href="https://hub.docker.com/_/alpine"&gt;Alpine&lt;/a&gt; or &lt;a href="https://hub.docker.com/_/busybox"&gt;Busybox&lt;/a&gt; would be fine, however, it pulled down quite quickly so it&amp;rsquo;s either smaller that I imagined or I already had the main layers locally.&lt;/p&gt;
&lt;p&gt;Now if we look in the directory where we ran that command, there should be a &lt;code&gt;backup.tar&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-10.34.38-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-10.34.38-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now, for the purposes of this demo, I&amp;rsquo;ll copy the backup.tar (and my compose file) over to another VM and we&amp;rsquo;ll see if we can recreate this install.&lt;/p&gt;
&lt;p&gt;Once I&amp;rsquo;d copied them over and &lt;a href="https://docs.docker.com/engine/install/debian/"&gt;installed Docker&lt;/a&gt;, I ran &lt;code&gt;docker compose up&lt;/code&gt; to start a new, empty Uptime Kuma. As expected, when I tried to visit the main page, it wanted me to create an admin user. Then I stopped the container. Note that you don&amp;rsquo;t want to &lt;code&gt;docker compose down&lt;/code&gt; to stop the container since that also removed it. If it&amp;rsquo;s removed, the next command won&amp;rsquo;t be able to find the name volumes it uses.&lt;/p&gt;
&lt;p&gt;Now we need copy the backed up data (which is just sitting in the current directory) into the named volume. Once again, this will be achieved by creating a new container, mounting the named volume and and current external working directory.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo docker run --rm --volumes-from uptime-kuma -v $(pwd):/backup ubuntu bash -c &amp;#34;cd /app &amp;amp;&amp;amp; tar xvf /backup/backup.tar --strip 1&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once again, I&amp;rsquo;ve highlighted the bits I&amp;rsquo;ve changed from the &lt;a href="https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes"&gt;instructions&lt;/a&gt;. It&amp;rsquo;s important to note I&amp;rsquo;ve changed the destination directory. We backed up from &lt;code&gt;/app/data&lt;/code&gt; but we&amp;rsquo;re just restoring to &lt;code&gt;/app&lt;/code&gt; - the un-taring will copy the backed up data into the existing data directory. That&amp;rsquo;s a trick for young players - when I blindly followed the official instructions, I ended up with an &lt;code&gt;/app/data/data&lt;/code&gt; directory with the backed info which was, or course, ignored, and only discoverable buy &lt;code&gt;exec&lt;/code&gt;-ing into the container to see what was happening.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-11.34.08-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-28-at-11.34.08-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="why-not-just-copy-the-local-file-system-version"&gt;Why not just copy the local file system version?&lt;/h3&gt;
&lt;p&gt;The named docker volume is just stored on our local file system, usually at &lt;code&gt;/var/lib/docker/volumes&lt;/code&gt; so it would be reasonable to wonder why we don&amp;rsquo;t just copy that. I don&amp;rsquo;t have a great explanation for why not. I assume since the &lt;a href="https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes"&gt;official docs&lt;/a&gt; suggest something different and more complex that there must be a reason. Possibly there&amp;rsquo;s some extra Docker magic (file locks, caching, etc) going on we don&amp;rsquo;t know about, or there&amp;rsquo;s some planned for the future.&lt;/p&gt;</description></item><item><title>apt update - BADSIG 871920D1991BC93C</title><link>https://blog.iankulin.com/apt-update-badsig-871920d1991bc93c/</link><pubDate>Mon, 30 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/apt-update-badsig-871920d1991bc93c/</guid><description>&lt;p&gt;I have an ansible script that runs each weekend which basically does an &lt;code&gt;apt update &amp;amp;&amp;amp; apt upgrade -Y&lt;/code&gt; on every Debian based instance. This weekend it failed on one Ubuntu host. When I went it to try it manually, this was the output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Hit&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;1&lt;/span&gt; http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;au&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;archive&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;ubuntu&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;ubuntu jammy InRelease
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Hit&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;2&lt;/span&gt; https&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;download&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;docker&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;linux&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;ubuntu jammy InRelease 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Hit&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;3&lt;/span&gt; http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;au&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;archive&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;ubuntu&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;ubuntu jammy&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;backports InRelease 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Hit&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;4&lt;/span&gt; http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;au&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;archive&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;ubuntu&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;ubuntu jammy&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;security InRelease 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Get&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;5&lt;/span&gt; http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;au&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;archive&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;ubuntu&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;ubuntu jammy&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;updates InRelease &lt;span style="color:#eceff4"&gt;[&lt;/span&gt;&lt;span style="color:#b48ead"&gt;119&lt;/span&gt; kB&lt;span style="color:#eceff4"&gt;]&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Err&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;5&lt;/span&gt; http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;au&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;archive&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;ubuntu&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;ubuntu jammy&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;updates InRelease 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; The following signatures were invalid&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; BADSIG &lt;span style="color:#b48ead"&gt;871920&lt;/span&gt;D1991BC93C Ubuntu Archive Automatic Signing Key &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;2018&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;ftpmaster&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;ubuntu&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Get&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;6&lt;/span&gt; https&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;pkgs&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;tailscale&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;stable&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;ubuntu jammy InRelease
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Fetched &lt;span style="color:#b48ead"&gt;125&lt;/span&gt; kB &lt;span style="color:#81a1c1;font-weight:bold"&gt;in&lt;/span&gt; &lt;span style="color:#b48ead"&gt;1&lt;/span&gt;s &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;125&lt;/span&gt; kB&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;s&lt;span style="color:#eceff4"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Reading package lists&lt;span style="color:#81a1c1"&gt;...&lt;/span&gt; Done
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Building dependency tree&lt;span style="color:#81a1c1"&gt;...&lt;/span&gt; Done
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Reading state information&lt;span style="color:#81a1c1"&gt;...&lt;/span&gt; Done
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#b48ead"&gt;11&lt;/span&gt; packages can be upgraded&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt; Run &lt;span style="color:#a3be8c"&gt;&amp;#39;apt list --upgradable&amp;#39;&lt;/span&gt; to see them&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;W&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; An error occurred during the signature verification&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt; The repository is &lt;span style="color:#81a1c1;font-weight:bold"&gt;not&lt;/span&gt; updated &lt;span style="color:#81a1c1;font-weight:bold"&gt;and&lt;/span&gt; the previous index files will be used&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt; GPG error&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;au&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;archive&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;ubuntu&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;ubuntu jammy&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;updates InRelease&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; The following signatures were invalid&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; BADSIG &lt;span style="color:#b48ead"&gt;871920&lt;/span&gt;D1991BC93C Ubuntu Archive Automatic Signing Key &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;2018&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;ftpmaster&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;ubuntu&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;W&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Failed to fetch http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;au&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;archive&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;ubuntu&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;ubuntu&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;dists&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;jammy&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;updates&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;InRelease The following signatures were invalid&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; BADSIG &lt;span style="color:#b48ead"&gt;871920&lt;/span&gt;D1991BC93C Ubuntu Archive Automatic Signing Key &lt;span style="color:#eceff4"&gt;(&lt;/span&gt;&lt;span style="color:#b48ead"&gt;2018&lt;/span&gt;&lt;span style="color:#eceff4"&gt;)&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;&amp;lt;&lt;/span&gt;ftpmaster&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;ubuntu&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;com&lt;span style="color:#81a1c1"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;W&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Some index files failed to download&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt; They have been ignored&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;or&lt;/span&gt; old ones used instead&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="solved"&gt;Solved&lt;/h3&gt;
&lt;p&gt;The first &lt;a href="https://ubuntuforums.org/showthread.php?t=2484710"&gt;google result&lt;/a&gt; mentions apt-cache - which &lt;a href="https://blog.iankulin.com/caching-apt-updates/"&gt;I also run&lt;/a&gt;, so a first level debug step is to delete the &lt;code&gt;/etc/apt/apt.conf.d/00aptproxy&lt;/code&gt; file that redirects apt requests to the cache I run in an LXC container. After that, if I re-run the &lt;code&gt;apt update&lt;/code&gt; it works perfectly. Seems like a problem with the cache then. I&amp;rsquo;m not sure why it would only affect this host though - I have other Ubuntu VM&amp;rsquo;s in the fleet that are not getting the original error.&lt;/p&gt;
&lt;p&gt;In any case, adding the conf back to force the server to use the cache made the error reappear - so it&amp;rsquo;s definitely related to the cache. With any type of cache, when there&amp;rsquo;s a problem related to it, deleting the contents is usually a &amp;ldquo;plan A&amp;rdquo; response. Assuming there&amp;rsquo;s some mechanism in &lt;a href="https://wiki.debian.org/AptCacherNg"&gt;Apt Cacher NG&lt;/a&gt; to do this, I went to the little stats/config webpage it serves up.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-08-at-8.41.44-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Well &amp;ldquo;Force the download of index files&amp;rdquo; sounds promising, let&amp;rsquo;s try that.&lt;/p&gt;
&lt;p&gt;I ticked the box for Force the download of index files (even having fresh ones), but it wasn&amp;rsquo;t clear to me how to make that change stick. The first button I could click further down the page was &amp;ldquo;Start Scan&amp;rdquo; which was related to some different checkboxes. I tried it anyway, but it didn&amp;rsquo;t force the downloading of index files. Time for some command line comandoing.&lt;/p&gt;
&lt;p&gt;The cache files for &lt;code&gt;aptcacher-ng&lt;/code&gt; are in &lt;code&gt;/var/cache/apt-cacher-ng/&lt;/code&gt; each distro has a directory in there.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-10-08-at-9.08.48-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Guessing the Ubuntu repository cache is probably stored in &lt;code&gt;uburep&lt;/code&gt;, I deleted that with &lt;code&gt;rm -R /var/cache/apt-cacher-ng/uburep&lt;/code&gt;. When I retried the &lt;code&gt;apt update&lt;/code&gt;, it worked perfectly, and I could see that the &lt;code&gt;/var/cache/apt-cacher-ng/uburep&lt;/code&gt; directory had re-appeared.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the immediate problem fixed. The cause of this problem is unclear. Presumably it related to a package running on this Ubuntu machine (runs docker with a couple of small services) that is not running on my other Ubuntu hosts. It probably falls into the category of &amp;ldquo;don&amp;rsquo;t worry about unless it crops up again&amp;rdquo;.&lt;/p&gt;</description></item><item><title>Caching APT updates</title><link>https://blog.iankulin.com/caching-apt-updates/</link><pubDate>Tue, 03 Oct 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/caching-apt-updates/</guid><description>&lt;p&gt;It&amp;rsquo;s bothered me for a while that all these VM&amp;rsquo;s are pulling down a lot of the same updates. As well as needlessly using some bandwidth, I&amp;rsquo;m hammering the update servers (that I don&amp;rsquo;t pay for) with the same requests over and over. I did briefly consider running my own mirror, but that&amp;rsquo;s not simple, plus I&amp;rsquo;d then be mirroring a heap of files in a complete repository that I&amp;rsquo;d never use. What I really needed was some sort of cache so once I&amp;rsquo;ll pulled down an update, it would hang around for a few days being available to other machines on the local network. Luckily, that exact thing exists - &lt;a href="https://www.unix-ag.uni-kl.de/~bloch/acng/html/index.html"&gt;APT Cacher NG&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It works pretty much as described above - all of the machines on the LAN have their APT calls proxied through a little server. If the server doesn&amp;rsquo;t have a copy of the appropriate package, it pulls it down and delivers it. If it&amp;rsquo;s got a good copy already, it just provides that.&lt;/p&gt;
&lt;h3 id="installing-the-server"&gt;Installing the server&lt;/h3&gt;
&lt;p&gt;I decided an unprivileged LXC container would be the perfect base for this service. I created one from the Debian 12 image with 1MB RAM but a largish 30GB drive. I don&amp;rsquo;t really have any feel for how big the cache will get under normal use so I erred on the large side. It&amp;rsquo;s not doing any computationally expensive work, so one CPU is plenty.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-10.42.06-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-10.42.06-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then we just install it, and start and enable it as a service.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;apt install apt-cacher-ng
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;systemctl start apt-cacher-ng
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;systemctl enable apt-cacher-ng
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;During the install, it asked me about https:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-10.46.32-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-10.46.32-am.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I said no, but then to enable it, I had to (after the installation) edit the config file at &lt;code&gt;/etc/apt-cacher-ng/acng.conf&lt;/code&gt; to uncomment the line&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PassThroughPattern: .* # this would allow CONNECT to everything&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;With that done, and the service restarted, we&amp;rsquo;re now serving proxies at localhost:3142, and also a little web page with some advice.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-2.37.46-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="installing-the-client"&gt;Installing the client&lt;/h3&gt;
&lt;p&gt;The two bits of information I&amp;rsquo;ve put red boxes around are the things we need to do to enable &lt;code&gt;apt&lt;/code&gt; on the client machines to use the cache. We need to create a file called &lt;code&gt;/etc/apt/apt.conf.d/00aptproxy&lt;/code&gt; and add the single line to it of:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Acquire::http::Proxy &amp;quot;http://192.168.100.37:3142&amp;quot;;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Note that the ip address will be different on yours, just copy it off the little web page. Since I&amp;rsquo;ve got a heap of machines to do this do, I made the &lt;code&gt;conf&lt;/code&gt; file once and pushed out out with Ansible.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-3.00.26-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-3.00.26-pm.png" width="900" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;hosts: local&lt;/code&gt; in the pkaybook refers to the &lt;code&gt;local: children&lt;/code&gt; group in my hosts ini file.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-3.14.24-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="statistics"&gt;Statistics&lt;/h3&gt;
&lt;p&gt;If you&amp;rsquo;re curious to see what the savings are, there&amp;rsquo;s another web page served by the cache at &lt;code&gt;&amp;lt;server ip&amp;gt;:3142/acng-report.html&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-20-at-3.16.27-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Further down on that page are some options that can be changed as well.&lt;/p&gt;
&lt;h3 id="resources"&gt;Resources&lt;/h3&gt;
&lt;p&gt;I learned most of this from &lt;a href="https://www.youtube.com/watch?v=t8kI4YwdvRA"&gt;this video by RickMakes&lt;/a&gt;, and &lt;a href="https://www.linuxhelp.com/how-to-set-up-apt-caching-server-using-apt-cacher-ng-on-debian-11-3"&gt;this Linux Help page&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Installing service with Ansible</title><link>https://blog.iankulin.com/installing-service-with-ansible/</link><pubDate>Sat, 30 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/installing-service-with-ansible/</guid><description>&lt;p&gt;Having written my little monitoring endpoint in Go, it needs pushed out to all my servers and VM&amp;rsquo;s. Clearly this is a job for Ansible which I&amp;rsquo;ve already &lt;a href="https://blog.iankulin.com/ansible-with-secrets/"&gt;dabbled my toes in&lt;/a&gt;. Before we get onto doing that though, we need to have a think about how to make it a service.&lt;/p&gt;
&lt;h3 id="linux-services"&gt;Linux Services&lt;/h3&gt;
&lt;p&gt;A service in Linux is just a program, but one that&amp;rsquo;s usually required to be running all the time to provide some piece of functionality. The &amp;ldquo;program&amp;rdquo; can be any executable, but to allow systemd to manage it, we need to tell it a bit about what we want in a &lt;code&gt;.service&lt;/code&gt; file. This file is used by &lt;code&gt;systemd&lt;/code&gt; to know how to manage the service. They can get quite complex, but here&amp;rsquo;s the simple one for &lt;code&gt;vitals-glimpse&lt;/code&gt; - my little monitoring API endpoint.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-19-at-11.23.21-am.png" alt="[Unit]
Description=Memory and Disk statistics server on port 10321
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/vitals-glimpse
[Install]
WantedBy=default.target"&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ExecStart&lt;/code&gt; is just saying what executable file is to be run. In this case it&amp;rsquo;s my compiled Go program. It&amp;rsquo;s a whopping 6MB so I&amp;rsquo;m assuming it&amp;rsquo;s all statically linked and standalone, so to run it we just copy it into &lt;code&gt;/usr/local/bin&lt;/code&gt; and run it from there.&lt;/p&gt;
&lt;p&gt;The two lines mentioning &lt;code&gt;.target&lt;/code&gt;s might not be obvious. These refer to the different times things happen in the machine startup sequence. &lt;code&gt;After=network.target&lt;/code&gt; means &amp;ldquo;don&amp;rsquo;t start this until the network is up and running&amp;rdquo;. You can see how it would be pointless to start a server that&amp;rsquo;s listening on a network port before networking is live. &lt;code&gt;default.target&lt;/code&gt; is just the system state when everything is going and ready for the users to interact with things, so when we specify &lt;code&gt;WantedBy=default.target&lt;/code&gt; we&amp;rsquo;re just saying &amp;ldquo;this service needs to be running by the time we are ready for user interactions&amp;rdquo;.&lt;/p&gt;
&lt;h3 id="installation"&gt;Installation&lt;/h3&gt;
&lt;p&gt;I already have my hosts file listing every machine, and an encrypted vault for my secrets (we&amp;rsquo;ve discussed those before), so the installation Ansible playbook just needs to copy the executable file into place in &lt;code&gt;/usr/local/bin&lt;/code&gt;, mark it as executable, copy the service file into place, and then start the service.&lt;/p&gt;
&lt;p&gt;If the files are already up to date and we don&amp;rsquo;t copy anything, then there&amp;rsquo;s no need touch the service, but if we have copied a new file, then we want to restart the service to pick up the change. Here&amp;rsquo;s how that all looks.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;---&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Install vitals&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;glimpse to a Debian based server
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;# ansible-playbook vg-install.yml --ask-vault-pass &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vars_files&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;./&lt;/span&gt;vault&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;yml
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hosts&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; vm100&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;dockhost
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; become&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tasks&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Copy service file
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ansible&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;builtin&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;copy&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; src&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; files&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;vitals&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;glimpse&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;service
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dest&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;etc&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;systemd&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;system&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;vitals&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;glimpse&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;service
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; notify&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Restart vitals&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;glimpse
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Copy executable
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ansible&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;builtin&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;copy&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; src&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; files&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;vitals&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;glimpse
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dest&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;usr&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;local&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;bin&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;vitals&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;glimpse
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mode&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;0755&amp;#39;&lt;/span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;# Set the executable permissions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; notify&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Restart vitals&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;glimpse
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; handlers&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt; name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; Restart vitals&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;glimpse
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ansible&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;builtin&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;service&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; vitals&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;glimpse
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; state&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; restarted
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; enabled&lt;span style="color:#eceff4"&gt;:&lt;/span&gt; yes
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first thing to know is that I have a hosts inventory file in my Ansible config, and &lt;code&gt;vm100-dockhost&lt;/code&gt; is just one of those hosts. The sudo credentials for that host are in the &lt;code&gt;vault.yml&lt;/code&gt; file mentioned in the code as &lt;code&gt;vars_file&lt;/code&gt;. I&amp;rsquo;ve started putting the command I need to run each playbook in a comment in the file so I don&amp;rsquo;t have to remember them, the command for this one: &lt;code&gt;ansible-playbook vg-install.yml --ask-vault-pass&lt;/code&gt; tells Ansible to run this playbook, and ask me for the password to decrypt the vault file.&lt;/p&gt;
&lt;p&gt;The if/then mechanism to only do something based on something earlier happening in Ansible is usually achieved with notify/handles. We put the declarative block which is optionally executed in the &lt;code&gt;handlers:&lt;/code&gt; block. The name of this block (in the case above it is &lt;code&gt;Restart vitals-glimpse&lt;/code&gt;) is specified with the &lt;code&gt;notify&lt;/code&gt; key. If either of the files are copied in, then the notify flag is set and the service is restarted.&lt;/p&gt;</description></item><item><title>Disable SSH root logins</title><link>https://blog.iankulin.com/disable-ssh-root-logins/</link><pubDate>Mon, 18 Sep 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/disable-ssh-root-logins/</guid><description>&lt;p&gt;This always makes me laugh:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-03-at-8.01.20-pm.jpg" alt="Screenshot of terminal output full of lines saying &amp;ldquo;Failed password for root&amp;rdquo;"&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s like half the traffic on the internet is &lt;a href="https://blog.iankulin.com/chinese-hackers-want-to-steal-my-hello-world-container/"&gt;bots&lt;/a&gt; trying random passwords on root accounts over ssh. This is on an Ubuntu VPS on BinaryLane that had only been spun up five minutes or so. Looks like about one attempt every 10 seconds.&lt;/p&gt;
&lt;p&gt;This is why the number three thing on my new install list is to disable root access via ssh. Here&amp;rsquo;s my system - possibly just for Ubuntu and related systems:&lt;/p&gt;
&lt;p&gt;Add a new user, and put them in the sudo group&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;add user fred
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;usermod -aG sudo fred
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then log out, and ssh back in with the user you just created. Now we want to edit the config file for the ssh daemon. Since we&amp;rsquo;re not logged in as root now, we&amp;rsquo;ll have to use &lt;code&gt;sudo&lt;/code&gt;, so we&amp;rsquo;ll also find out if that&amp;rsquo;s working.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo nano /etc/ssh/sshd_config
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If &lt;code&gt;sudo&lt;/code&gt; doesn&amp;rsquo;t work, either you stuffed up adding the new username to the sudo group, or you don&amp;rsquo;t have sudo installed. If the problem is the latter, log out, and ssh back in as root and install it with &lt;code&gt;apt install sudo&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;There is probably a line with &lt;code&gt;PermitRootLogin&lt;/code&gt; in it. It may be commented out, or set to &lt;code&gt;yes&lt;/code&gt;. But we want it to end up looking like this&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PermitRootLogin no
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We&amp;rsquo;ll need to restart the daemon to pick up the config changes.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo systemctl restart sshd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now if you log out, and try to ssh back in as root, it should fail. If it doesn&amp;rsquo;t, a likely issue is that there&amp;rsquo;s other configuration files being included. I feel I&amp;rsquo;ve mentioned before that a common pattern with Linux config files, at least on the Debian based systems I use, is that there&amp;rsquo;s a main config file that you probably shouldn&amp;rsquo;t mess with, but it pulls in subsidiary config files, often in a subdirectory called &lt;code&gt;conf.d&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;In the case of this file, there&amp;rsquo;s a line up the top saying&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Include /etc/ssh/sshd_config.d/*.conf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;which does exactly that.&lt;/p&gt;
&lt;h3 id="spy-vs-spy"&gt;Spy vs Spy&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;d love to know what passwords these bots are trying. I was thinking it wouldn&amp;rsquo;t be all that hard to write something that would face the password login process and run it on port 22 to see. I asked ChatGPT about this, but goodie-goddie that it is, all I got was a warning about ethics and some ssh security tips.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-08-04-at-6.04.02-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-08-04-at-6.04.02-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not the first person to &lt;a href="https://www.darkreading.com/endpoint/a-common-password-list-accounts-for-nearly-all-cyberattacks"&gt;think of this&lt;/a&gt;, so I might come back to this idea later. If I was running brute force ssh I guess I&amp;rsquo;d use one of the common password lists from one of the big leaks, so it might not be that exciting. It would also be interesting to see what the first command they tried to run is as well.&lt;/p&gt;</description></item><item><title>Error wiping old drive in Proxmox</title><link>https://blog.iankulin.com/error-wiping-old-drive-in-proxmox/</link><pubDate>Thu, 31 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/error-wiping-old-drive-in-proxmox/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.19.42-pm-copy.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.19.42-pm-copy.png" width="568" alt="Error: disk/partition '/dev/sda3' has a holder (500)"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When I popped in an NVME drive and freshly installed Proxmox to it, I assumed I&amp;rsquo;d just be able to wipe the SDD that had previously been the boot drive to set it up as a ZFS pool. However, when I tried to do the wipe, I was greeted with the error:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;disk/partition &amp;#39;/dev/sda3&amp;#39; has a holder (500)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I assume this means there&amp;rsquo;s a flag set on one of the Proxmox partitions to prevent accidental deletion or Proxmox thought that&amp;rsquo;s where it was running from. It&amp;rsquo;s likely that it&amp;rsquo;s related to this message I had during installation that I haven&amp;rsquo;t seen before:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_5830.jpg" alt="Detected existing &amp;lsquo;pve&amp;rsquo; Volume Group(s)! Do you want to: rename VG backed by PV &amp;lsquo;/dev/sda3&amp;rsquo; to &amp;lsquo;pve-OLD-D4DDE7DC&amp;rsquo; or cancel the installation?"&gt;&lt;/p&gt;
&lt;p&gt;Since I didn&amp;rsquo;t want to cancel the installation, I went ahead and told it okay. On the non-graphical &amp;lsquo;console&amp;rsquo; version of the installer, this message is truncated, and the only option available is abort. I guess that&amp;rsquo;s an installer bug. So if you are adding a extra boot drive to an existing Proxmox node, I suggest using the graphical installer.&lt;/p&gt;
&lt;p&gt;When I Googled around for the &amp;ldquo;has a holder&amp;rdquo; error, there were several unanswered requests for help for this, several speculative answers, and &lt;a href="https://www.reddit.com/r/Proxmox/comments/xff5ri/how_do_i_wipe_an_old_drive/"&gt;one that worked&lt;/a&gt;.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/66d29d7d-bc29-4747-b92a-7fc7c790227f_text.gif" width="400" alt=""&gt;
&lt;p&gt;You need to use &lt;code&gt;fdisk&lt;/code&gt; to remove each partition. Take a note of the drive name - I could see in the Proxmox GUI that mine was sda, so the command to run was:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fdisk /dev/sda&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;You probably need to have a &lt;a href="https://www.howtogeek.com/106873/how-to-use-fdisk-to-manage-partitions-on-linux/"&gt;read-up on&lt;/a&gt; &lt;code&gt;[fdisk](https://www.howtogeek.com/106873/how-to-use-fdisk-to-manage-partitions-on-linux/)&lt;/code&gt; if you&amp;rsquo;re not familiar with it, but basically, you&amp;rsquo;re in the command mode, for one of the partitions (my &lt;code&gt;sda&lt;/code&gt; had three) if you press the &lt;code&gt;d&lt;/code&gt; key here it marks that partition for deletion. Even though the error message had said it was the last partition that was causing the headache, I just went ahead and deleted all of them. There&amp;rsquo;s no warnings as you do this, and actually no changes have been made yet, that happens when you press &lt;code&gt;w&lt;/code&gt; to write the changes. No warning here either. 🙂&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.29.16-pm.png" alt="fdisk screenshot"&gt;&lt;/p&gt;
&lt;p&gt;That gave an error saying the third partition was still in use by the kernel, so I followed the advice to reboot, then I was able to wipe the drive in the Proxmox web GUI.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.30.09-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-22-at-12.30.09-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Bloody VIM</title><link>https://blog.iankulin.com/bloody-vim/</link><pubDate>Thu, 10 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/bloody-vim/</guid><description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Vim is a highly configurable text editor built to make creating and changing any kind of text very efficient. It is included as &amp;ldquo;vi&amp;rdquo; with most UNIX systems and with Apple OS X.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.vim.org/"&gt;vim.org&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You will encounter vi/vim as the incomprehensible text editor that pops up by default when you need to edit something in your sysadmin journey. Perhaps you issued the command to edit your Ansible vault, perhaps you forgot to add a message to a commit. It&amp;rsquo;s going to be unavoidable.&lt;/p&gt;
&lt;p&gt;Here is the minimum knowledge you need to survive these encounters.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It has modes. When it opens up, you are in &amp;lsquo;moving around mode&amp;rsquo;. Use the cursor keys to get to where you need to.&lt;/li&gt;
&lt;li&gt;When you actually want to edit something, press &lt;code&gt;i&lt;/code&gt; now you&amp;rsquo;re in &lt;em&gt;INSERT&lt;/em&gt; mode and it&amp;rsquo;s behaving how 99% of the world expect an editor to behave. When you type things, they go into the document where the cursor is.&lt;/li&gt;
&lt;li&gt;When you are done, press &lt;em&gt;escape&lt;/em&gt; to get out of INSERT mode, then these keys one at a time &lt;code&gt;: w q&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The underlying theory of Vim - that you spend more time moving around in text than editing it, is true of most sysadmin and programming tasks. So Vim has powerful, non-intuitive commands to do that efficiently. It&amp;rsquo;s impressive to watch people who have learned these arcane ways move around a file. They don&amp;rsquo;t have girlfriends. There are also powerful, non-intuitive commands for editing.&lt;/p&gt;</description></item><item><title>Finding the host IP from inside a Docker container</title><link>https://blog.iankulin.com/finding-the-host-ip-from-inside-a-docker-container/</link><pubDate>Mon, 07 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/finding-the-host-ip-from-inside-a-docker-container/</guid><description>&lt;p&gt;Having successfully set up and tested my node.js api handling app behind nginx on a development VM in the homelab, I decided to move it to my VPS so I could start using it for real. I had a bit of trouble finding the nginx.conf files on the VPS, until I remembered I was running nginx in a docker container on this machine!&lt;/p&gt;
&lt;p&gt;I got everything set up, I could hit the domain in a web browser and get served the static page, and I could &amp;lt;domain_name&amp;gt;:3000/api/gnp_temp.txt and get the file delivered by the node script, but if I tried &amp;lt;domain_name&amp;gt;/api/gnp_temp.txt - &amp;ldquo;Bad Gateway&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;I looked at the nginx.conf over and over - it seemed fine. The location block was clearly being triggered, otherwise I&amp;rsquo;d be getting a 404. I dived into stack overflow to no avail. It was if http://localhost:3000 just wasn&amp;rsquo;t working. But it definitely was when I &lt;code&gt;curl&lt;/code&gt;ed it from the command line.&lt;/p&gt;
&lt;p&gt;In desperation I started writing out an explanation to ChatGPT about the setup and my problem, and before I pressed enter realised - from nginx&amp;rsquo;s point of view, http://localhost:3000 was an address &lt;em&gt;inside&lt;/em&gt; the container 🤦, what I needed was the address of the host, from the point of view of the docker container. Surely that must be a common requirement that&amp;rsquo;s been solved.&lt;/p&gt;
&lt;p&gt;From reading around, it seems like I should be able to just substitute &lt;code&gt;host.docker.internal&lt;/code&gt; but that didn&amp;rsquo;t seem to work. I opened a shell into the container to look at &lt;code&gt;ip a&lt;/code&gt; or &lt;code&gt;/sbin/ip route|awk '/default/ { print $3 }'&lt;/code&gt; but of course these containers are slim installs without the general tools you need.&lt;/p&gt;
&lt;p&gt;Docker networking is a whole thing that I should learn, but I haven&amp;rsquo;t yet, I just start up containers with whatever the defaults for networking are. But I figured the host must be part of the docker network, and a quick look in &lt;code&gt;ip a | grep docker&lt;/code&gt; produced this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;3: docker0: &amp;lt;NO-CARRIER,BROADCAST,MULTICAST,UP&amp;gt; mtu 1500 qdisc noqueue state DOWN group default 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Bingo. I popped that IP address into my nginx.conf and everything started working perfectly. That was forty minutes of learning I wouldn&amp;rsquo;t have had to live through if I could have just turned around to a work colleague and asked.&lt;/p&gt;</description></item><item><title>nginx in Front of a node.js app</title><link>https://blog.iankulin.com/nginx-in-front-of-a-node-js-app/</link><pubDate>Fri, 04 Aug 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/nginx-in-front-of-a-node-js-app/</guid><description>&lt;p&gt;NGINX is a great webserver and reverse proxy - as in it can hand off requests to other web-servers. That&amp;rsquo;s the situation I want to have set up on my VPS. I want NGINX to handle incoming requests - some of them will just be sorted out by returning static HTML, others (like the weather api I&amp;rsquo;ve been playing with) need to be handed off to other services to respond to.&lt;/p&gt;
&lt;p&gt;In the situation I&amp;rsquo;m looking at, I want requests that have the route /api (eg example.com/api/weather) to be passed to a node.js program I&amp;rsquo;ve written. All the other http requests should just be treated as requests for static pages and dealt with by NGINX.&lt;/p&gt;
&lt;p&gt;So I guess is part V of my adventures in the weather API, if you just want to know how to set up NGINX to serve static pages AND pass some routes off to node, you don&amp;rsquo;t need to be up to date on these.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/outside-temperature-from-an-api-in-a-shell-script/"&gt;Outside Temperature From an API in a Shell Script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/complicating-the-temperature-api/"&gt;Complicating the Temperature API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/how-to-deploy-a-node-js-app/"&gt;Using Node.js to serve a static file&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.iankulin.com/how-to-deploy-a-node-js-app/"&gt;How to Deploy a Node.js App&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="nginx-configuration"&gt;nginx Configuration&lt;/h3&gt;
&lt;p&gt;Once nginx and node.js are installed (with &lt;a href="https://gist.github.com/IanKulin/bd6d1a78f9a9fa9a859384a26ca95235"&gt;the Ansible script&lt;/a&gt; if you want to rock the dev ops tattoo) you&amp;rsquo;ll need to configure nginx. On my Debian systems, the config file &lt;code&gt;nginx.conf&lt;/code&gt; is in &lt;code&gt;/etc/nginx/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a line in that file that includes all the &lt;code&gt;*.conf&lt;/code&gt; files in &lt;code&gt;/etc/nginx/conf.d&lt;/code&gt;. This is a common pattern I see in some distros - the main config files are not really meant to be messed with, but then there&amp;rsquo;s a directory to add config files to whihc are included. The theoretical advantage of this is that the distro maintainers can roll out a new version of a package and change the main config file, and your stuff will still work.&lt;/p&gt;
&lt;p&gt;The way they have done this with the nginx.conf means that the only changes we can make in the &lt;code&gt;conf.d&lt;/code&gt; directory are to do with virtual hosts, but that&amp;rsquo;s going to be 99% of the things we would want to change. So much so, I&amp;rsquo;m not even going to show you the &lt;code&gt;nginx.conf&lt;/code&gt; file, just our little &lt;code&gt;/etc/nginx/conf.d/nodeapi.conf&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; server &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; listen &lt;span style="color:#b48ead"&gt;80&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; server_name &lt;span style="color:#b48ead"&gt;192.168&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;.&lt;/span&gt;&lt;span style="color:#b48ead"&gt;100.40&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;# Serve static files&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; root &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;var&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;www&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#616e87;font-style:italic"&gt;# pass api requests to node&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; location &lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;api &lt;span style="color:#eceff4"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; proxy_pass http&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;//&lt;/span&gt;localhost&lt;span style="color:#eceff4"&gt;:&lt;/span&gt;&lt;span style="color:#b48ead"&gt;3000&lt;/span&gt;&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; proxy_set_header Host &lt;span style="color:#81a1c1"&gt;$&lt;/span&gt;host&lt;span style="color:#eceff4"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#eceff4"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Okay, we are listening on port 80, and this &amp;ldquo;server block&amp;rdquo; is only for requests like http://192.168.100.40. The purpose of &lt;code&gt;server_name&lt;/code&gt; is that we might run the websites for several domains from one nginx installation. For example, we might be serving example.com and otherexample.com from the same VPS.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;root /var/www;&lt;/code&gt; - tells nginx to grab the files from that directory, so that&amp;rsquo;s the static web server part.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;location /api {&lt;/code&gt; - is telling nginx &amp;ldquo;all the requests with /api on the end are dealt with differently, look in this block for instructions&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;proxy_pass http://localhost:3000;&lt;/code&gt; - send them all to a server on this machine listening on port 3000. This is where the node/express server is running.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;proxy_set_header Host $host;&lt;/code&gt; - it doesn&amp;rsquo;t matter for the purposes of this api, but it&amp;rsquo;s often nice to tell the server being proxied who the real host receiving the request is. If we didn&amp;rsquo;t do this, the node app would only be able to see that &amp;ldquo;localhost&amp;rdquo; was making a request. By doing this, it knows the request was to the server running nginx, in this case 192.168.100.40, but usually a real domain name. This might be needed if our node app was also servicing more than one domain.&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s it. We&amp;rsquo;re done. You do need to have your node app running on port 3000 on the same machine, but as long as that&amp;rsquo;s happening this should all be working. Do remember to restart nginx each time you make a config changes with &lt;code&gt;sudo service nginx restart&lt;/code&gt;, and it would also be good practice to check the config files with &lt;code&gt;sudo nginx&lt;/code&gt; -t before that.&lt;/p&gt;</description></item><item><title>ZFS Basics on Proxmox</title><link>https://blog.iankulin.com/zfs-basics-on-proxmox/</link><pubDate>Sat, 29 Jul 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/zfs-basics-on-proxmox/</guid><description>&lt;p&gt;I&amp;rsquo;m a keen listener of the &lt;a href="https://2.5admins.com/"&gt;2.5 Admins&lt;/a&gt; podcast in which there&amp;rsquo;s frequent enumeration of the advantages of &lt;a href="https://itsfoss.com/what-is-zfs/"&gt;ZFS&lt;/a&gt; as a file system. So much so, that I&amp;rsquo;ve had occasional twinges or regret about the money I spent on the Synology - although it has been boringly reliable and does everything I need.&lt;/p&gt;
&lt;p&gt;Proxmox has some built in support for ZFS, including through the web GUI. So I&amp;rsquo;ve been itching to give it a try.&lt;/p&gt;
&lt;p&gt;I had a 256GB M2 NVME sitting around - I bought it with the plan to try it as the root drive in one of the servers. That was when I was worried that one of the servers&amp;rsquo; drives was about dead because the SMART data said it was at 100% use. I&amp;rsquo;ve since discovered that various companies interpret that different ways, so probably it&amp;rsquo;s 100% okay.&lt;/p&gt;
&lt;p&gt;I started to think a little &lt;a href="https://www.techtarget.com/searchstorage/definition/JBOD"&gt;JBOD&lt;/a&gt; with a couple of NVME SSD mirrored drives would be a fun project. There&amp;rsquo;s no way I could do that inside the case to get the proper PCI access, but the my HP 800 G2&amp;rsquo;s all have USB 3 so it shouldn&amp;rsquo;t be terrible - probably a lot better than the spinning rust NAS over 1GB Ethernet.&lt;/p&gt;
&lt;p&gt;I purchased this little UNITEK S1206A dual bay enclosure and another stick of 256GB Samsung SSD.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_5742.jpg" width="600" alt=""&gt;
&lt;img src="https://blog.iankulin.com/images/s-l960.jpg" width="600" alt=""&gt;
&lt;p&gt;The instructions for the unit show sticking a layer of silicon over the top of the gum sticks, and then a thin piece of aluminum. I&amp;rsquo;ve heard these get hot, but it wasn&amp;rsquo;t clear to me if I should peel that paper off first. So I&amp;rsquo;ve done nothing for the moment while I do some more research.&lt;/p&gt;
&lt;p&gt;The process of getting it set up in Proxmox was simple. If you select the node in the web interface, and go in to &lt;em&gt;Disks&lt;/em&gt;, you can see a list of the physical disks attached. The NVME drives showed up as NTFS so I wiped them by selecting the drive and pressing the &lt;em&gt;Wipe Disk&lt;/em&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-6.19.00-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;You can see on the screenshot above, that further down in the Disks list it says ZFS. That&amp;rsquo;s where you go to create the ZFS pool. I probably need to pause here, and go over some of the ZFS terminology.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.victormendonca.com/2020/11/03/zfs-for-dummies/"&gt;&lt;img src="https://blog.iankulin.com/images/zfs-components-1.png" width="706" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To start in the middle, we have the concept of a ZFS Pool. This is, well, a pool of storage that&amp;rsquo;s available to be used. It has a size, and we can see how much space is available. The pool is made up of vdev (virtual devices). A vdev could be a single physical drive, or multiple drives in some kind of RAID arrangement.&lt;/p&gt;
&lt;p&gt;In my situation, with the two NVME drives, my zpool will be made up of a single vdev comprising two physical drives which have been mirrored.&lt;/p&gt;
&lt;p&gt;In the zpool, we can create &lt;em&gt;datasets&lt;/em&gt; where we can actually put some data. You can think of these as directories in the sense they have a name and we can create directories and store data inside them, but in ZFS, the datasets in a zpool can have different settings (such as compression, de-duplication) applied to them. This is also the level where snapshots can be taken for backups.&lt;/p&gt;
&lt;p&gt;To create the ZFS pool in Proxmox, again select the node, then select ZFS in the list under &lt;em&gt;Disks&lt;/em&gt;. At the top is a button for &lt;em&gt;Create ZFS&lt;/em&gt;. Select the wiped drives, chose your RAID and give it a name. By tradition the pools are usually called &amp;rsquo;tank&amp;rsquo; - if you look at a few tutorials you&amp;rsquo;ll see that all over the place.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-8.26.42-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Once that was done, tank appeared as storage in the list under my node. I moved the drives of these dev guests across to it so the zpool would have something to do. I did notice that this process would rush through, then pause for a few seconds - something I haven&amp;rsquo;t noticed when moving guest droves between the NAS and internal SSDs. Early reviews of Samsung pm981 NVME SSD &lt;a href="https://www.tomshardware.com/reviews/samsung-pm981-980-nvme-ssd,5323.html"&gt;noted a sustained write dropoff&lt;/a&gt;, so this might be something to come back and have a look at later.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-07-04-at-8.23.10-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;If we drop into the shell now, we can have a look at the datasets.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve-prod1:/# zfs list
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;NAME USED AVAIL REFER MOUNTPOINT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tank 32.0G 199G 96K /tank
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tank/vm-300-disk-0 16.5G 209G 6.04G -
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tank/vm-321-disk-0 5.16G 202G 1.67G -
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tank/vm-322-disk-0 5.16G 202G 1.62G -
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tank/vm-323-disk-0 5.16G 202G 1.60G -
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve-prod1:/# 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So my pool is &lt;code&gt;tank&lt;/code&gt;, and there&amp;rsquo;s been datasets created for each of the VM guests&amp;rsquo; disks. We can create a data set to start using the pool as well.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;zfs create tank/temp_set
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That creates a dataset called &lt;code&gt;temp_set&lt;/code&gt; in the &lt;code&gt;tank&lt;/code&gt; zpool. It will have been mounted for us too. Let&amp;rsquo;s create a 1 GB file in there.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve-prod1:/# cd /tank/temp_set
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve-prod1:/tank/temp_set# head -c 1G &amp;lt;/dev/urandom &amp;gt;myfile
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve-prod1:/tank/temp_set# ls
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;myfile
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then if we list the datasets again.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve-prod1:/tank/temp_set# zfs list
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;NAME USED AVAIL REFER MOUNTPOINT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tank 33.0G 198G 104K /tank
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tank/temp_set 1.00G 198G 1.00G /tank/temp_set
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tank/vm-300-disk-0 16.5G 208G 6.04G -
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tank/vm-321-disk-0 5.16G 201G 1.67G -
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tank/vm-322-disk-0 5.16G 201G 1.62G -
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tank/vm-323-disk-0 5.16G 201G 1.60G -
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve-prod1:/tank/temp_set# 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once you&amp;rsquo;ve created a dataset, you can just use it as a regular place to store stuff. ZFS will go on doing it&amp;rsquo;s magic in the background to keep your data safe with copy-on-write and other magic. It&amp;rsquo;s good ZFS practice to do a &lt;em&gt;scrub&lt;/em&gt; every now and then. This causes ZFS to use whatever information it&amp;rsquo;s got to check the integrity of all your data.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve-prod1:/# zpool scrub tank
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve-prod1:/# zpool status -v tank
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pool: tank
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; state: ONLINE
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; scan: scrub repaired 0B in 00:00:53 with 0 errors on Tue Jul 4 20:58:16 2023
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;config:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; NAME STATE READ WRITE CKSUM
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tank ONLINE 0 0 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mirror-0 ONLINE 0 0 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sdb ONLINE 0 0 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sdc ONLINE 0 0 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;errors: No known data errors
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve-prod1:/# 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;While I&amp;rsquo;ve been writing this post, I&amp;rsquo;ve been copying data to and fro (I&amp;rsquo;d try things out, then have to delete and repeat to get the screen shot I wanted, and at one stage I decided to change the name of the zpool so all the disk images had to be moved off then back on after I&amp;rsquo;d recreated it etc) for about 90 minutes, and I&amp;rsquo;ve just been in the server rooms to see if the external NVME enclosure is hot. It&amp;rsquo;s warm to the touch, I&amp;rsquo;d guess 40° - so not alarming for this level of use. That box is pretty well ventilated.&lt;/p&gt;
&lt;p&gt;If you want a good summary of ZFS, particularly the thinking behind it, this is a great overview.&lt;/p&gt;
&lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;"&gt;
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/lsFDp-W1Ks0?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"&gt;&lt;/iframe&gt;
 &lt;/div&gt;
</description></item><item><title>Outside Temperature From an API in a Shell Script</title><link>https://blog.iankulin.com/outside-temperature-from-an-api-in-a-shell-script/</link><pubDate>Wed, 03 May 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/outside-temperature-from-an-api-in-a-shell-script/</guid><description>&lt;p&gt;I&amp;rsquo;m interested in &lt;a href="https://blog.iankulin.com/linux-shell-script-for-temperature-logging/"&gt;collecting some internal temperature data&lt;/a&gt; from my servers to look at the effect of adding an NMVe drive. Last week we had a couple of warm days immediately followed by a couple of cool ones. I imagine a 20° ambient temperature change could effect the server temperatures, so I thought it would be good to add that to my temperature logs.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t have a weather station or other automated system for collecting the temperature, but there are several commercial sources for this data which, while probably not as good as a sensor in the server room, will be fine for our purposes.&lt;/p&gt;
&lt;p&gt;One of the more well known weather APIs was &lt;a href="https://darksky.net/dev"&gt;Dark Sky&lt;/a&gt;, they got bought up by Apple and now similar data is available in the &lt;a href="https://developer.apple.com/weatherkit/get-started/"&gt;WeatherKit API&lt;/a&gt;. I hold a developer program membership, so that would be free to use for the frequency I need, but the API and sign up looked a bit complex, so I looked elsewhere.&lt;/p&gt;
&lt;p&gt;OpenWeather have a &lt;a href="https://openweathermap.org/current"&gt;simple API&lt;/a&gt; (including one intended to make changing over from Dark Sky easy), &lt;a href="https://openweathermap.org/price"&gt;a good free tier&lt;/a&gt;, and simple sign up - no credit card required. On the free tier I can pull the current weather for a location 22 times a minute continuously. Since I&amp;rsquo;m only collecting my server temps on a five minute cycle, that will be more than fine.&lt;/p&gt;
&lt;p&gt;Even thought the API would allow it, it seems wasteful, and greedy (since I&amp;rsquo;m not paying for it), to pull the same data three times (for each of the three servers), so to complicate things (and learn some interesting stuff) I decided to poll the OpenWeather API once every five minutes from my VPS, process that current weather JSON down to just the temperature I was after, then expose that as a http endpoint. Then each of my servers would poll the VPS to get that outside temp as part of their logging.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/20230425-weather.drawio-1.png" width="435" alt=""&gt;
&lt;p&gt;This will all extend involve some scripting that I haven&amp;rsquo;t encountered yet.&lt;/p&gt;
&lt;h3 id="vps--weather-api"&gt;VPS / Weather API&lt;/h3&gt;
&lt;p&gt;The OpenWeather API couldn&amp;rsquo;t be more straightforward, you sign up with an email and get an API token, then it&amp;rsquo;s just this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;https://api.openweathermap.org/data/2.5/weather?lat={lat}&amp;amp;lon={lon}&amp;amp;appid={API key}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There&amp;rsquo;s a couple of options for language and units, I went with &lt;em&gt;metric&lt;/em&gt;, then you get have some JSON.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;coord&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;lon&amp;#34;: 118,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;lat&amp;#34;: -33.93
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;weather&amp;#34;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;id&amp;#34;: 803,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;main&amp;#34;: &amp;#34;Clouds&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;description&amp;#34;: &amp;#34;broken clouds&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;icon&amp;#34;: &amp;#34;04d&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;base&amp;#34;: &amp;#34;stations&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;main&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;temp&amp;#34;: 12.59,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;feels_like&amp;#34;: 11.68,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;temp_min&amp;#34;: 12.59,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;temp_max&amp;#34;: 12.59,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;pressure&amp;#34;: 1007,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;humidity&amp;#34;: 68,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;sea_level&amp;#34;: 1007,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;grnd_level&amp;#34;: 976
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;visibility&amp;#34;: 10000,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;wind&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;speed&amp;#34;: 7.39,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;deg&amp;#34;: 307,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;gust&amp;#34;: 11.23
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;clouds&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;all&amp;#34;: 64
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;dt&amp;#34;: 1682401802,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;sys&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;country&amp;#34;: &amp;#34;AU&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;sunrise&amp;#34;: 1682375848,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;sunset&amp;#34;: 1682415263
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;timezone&amp;#34;: 28800,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;id&amp;#34;: 2070753,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;name&amp;#34;: &amp;#34;Gnowangerup&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;#34;cod&amp;#34;: 200
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From this, I want to extract the temperature, and the unix timestamp &amp;ldquo;dt&amp;rdquo;. Here&amp;rsquo;s my bash script.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5e81ac;font-style:italic"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;weather_text&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;curl -s &lt;span style="color:#a3be8c"&gt;&amp;#34;https://api.openweathermap.org/data/2.5/weather?lat=-33.93&amp;amp;lon=118.00&amp;amp;appid=somegiantrandomUIDtypenumber&amp;amp;units=metric&amp;#34;&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;temp_text&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;echo&lt;/span&gt; $weather_text &lt;span style="color:#eceff4"&gt;|&lt;/span&gt; awk -F&lt;span style="color:#a3be8c"&gt;&amp;#39;&amp;#34;temp&amp;#34;:&amp;#39;&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;{print $2}&amp;#39;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;|&lt;/span&gt; cut -d&lt;span style="color:#a3be8c"&gt;&amp;#39;,&amp;#39;&lt;/span&gt; -f1&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;time_text&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;echo&lt;/span&gt; $weather_text &lt;span style="color:#eceff4"&gt;|&lt;/span&gt; awk -F&lt;span style="color:#a3be8c"&gt;&amp;#39;&amp;#34;dt&amp;#34;:&amp;#39;&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#39;{print $2}&amp;#39;&lt;/span&gt; &lt;span style="color:#eceff4"&gt;|&lt;/span&gt; cut -d&lt;span style="color:#a3be8c"&gt;&amp;#39;,&amp;#39;&lt;/span&gt; -f1&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;log_file&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/home/ian/iankulin.com/www/gnp_temp.txt&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;printf&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;%s,%s&amp;#34;&lt;/span&gt; $temp_text $time_text &amp;gt; $log_file
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Of note, and that I haven&amp;rsquo;t already discussed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;weather_text&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;curl &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;s &lt;span style="color:#a3be8c"&gt;&amp;#34;https://api.openweathermap.org/data/2.5/weather?lat=-33.93&amp;amp;lon=118.00&amp;amp;appid=somegiantrandomUIDtypenumber&amp;amp;units=metric&amp;#34;&lt;/span&gt;&lt;span style="color:#bf616a"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;curl&lt;/code&gt; basically sends out a network request the same as if you had typed it into the top of your browser. If it was a web page, it would return the text of the HTML, but in this case it returns the JSON I showed before - although less formatted.&lt;/p&gt;
&lt;p&gt;weather_text is a variable to which we are assigning the return value of the curl - ie the string of JSON. Note the backticks `` the curl is enclosed in. This is how the script knows to execute the command and assign the results rather than assigning some text beginning with &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;temp_text=`echo $weather_text | awk -F&amp;#39;&amp;#34;temp&amp;#34;:&amp;#39; &amp;#39;{print $2}&amp;#39; | cut -d&amp;#39;,&amp;#39; -f1`
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Oh man, this took me on a journey. Firstly, keep in mind I&amp;rsquo;ve prettified the JSON above, actually the string looked like this, so it wasn&amp;rsquo;t possible to process it on a line by line.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{&amp;#34;coord&amp;#34;:{&amp;#34;lon&amp;#34;:118,&amp;#34;lat&amp;#34;:-33.93},&amp;#34;weather&amp;#34;:[{&amp;#34;id&amp;#34;:803,&amp;#34;main&amp;#34;:&amp;#34;Clouds&amp;#34;,&amp;#34;description&amp;#34;:&amp;#34;broken clouds&amp;#34;,&amp;#34;icon&amp;#34;:&amp;#34;04d&amp;#34;}],&amp;#34;base&amp;#34;:&amp;#34;stations&amp;#34;,&amp;#34;main&amp;#34;:{&amp;#34;temp&amp;#34;:12.59,&amp;#34;feels_like&amp;#34;:11.68,&amp;#34;temp_min&amp;#34;:12.59,&amp;#34;temp_max&amp;#34;:12.59,&amp;#34;pressure&amp;#34;:1007,&amp;#34;humidity&amp;#34;:68,&amp;#34;sea_level&amp;#34;:1007,&amp;#34;grnd_level&amp;#34;:976},&amp;#34;visibility&amp;#34;:10000,&amp;#34;wind&amp;#34;:{&amp;#34;speed&amp;#34;:7.39,&amp;#34;deg&amp;#34;:307,&amp;#34;gust&amp;#34;:11.23},&amp;#34;clouds&amp;#34;:{&amp;#34;all&amp;#34;:64},&amp;#34;dt&amp;#34;:1682401802,&amp;#34;sys&amp;#34;:{&amp;#34;country&amp;#34;:&amp;#34;AU&amp;#34;,&amp;#34;sunrise&amp;#34;:1682375848,&amp;#34;sunset&amp;#34;:1682415263},&amp;#34;timezone&amp;#34;:28800,&amp;#34;id&amp;#34;:2070753,&amp;#34;name&amp;#34;:&amp;#34;Gnowangerup&amp;#34;,&amp;#34;cod&amp;#34;:200}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We are assigning to the variable &lt;code&gt;temp_text&lt;/code&gt; the contents of this command, where $weather_text is the JSON string.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;echo $weather_text | awk -F&amp;#39;&amp;#34;temp&amp;#34;:&amp;#39; &amp;#39;{print $2}&amp;#39; | cut -d&amp;#39;,&amp;#39; -f1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The vertical lines are called &lt;em&gt;pipes&lt;/em&gt; &lt;code&gt;|&lt;/code&gt; they send the output of the command on their left into the command to their right. So there&amp;rsquo;s three different things happening here. The &lt;code&gt;echo&lt;/code&gt; just outputs the JSON, then we process it twice more.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;awk -F&amp;#39;&amp;#34;temp&amp;#34;:&amp;#39; &amp;#39;{print $2}&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://www.geeksforgeeks.org/awk-command-unixlinux-examples/"&gt;awk&lt;/a&gt; is one of the great text processing commands along with &lt;code&gt;grep&lt;/code&gt; and &lt;code&gt;sed&lt;/code&gt;. The way it is being used here is to break the string into multiple parts, where the parts are delimited by the text &lt;code&gt;&amp;quot;temp&amp;quot;:&lt;/code&gt; which in our case is just two parts. Then we are outputting the second part ready for the next processing. So at this stage, the text would look like this.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;12.59,&amp;#34;feels_like&amp;#34;:11.68,&amp;#34;temp_min&amp;#34;:12.59,&amp;#34;temp_max&amp;#34;:12.59,&amp;#34;pressure&amp;#34;:1007,&amp;#34;humidity&amp;#34;:68,&amp;#34;sea_level&amp;#34;:1007,&amp;#34;grnd_level&amp;#34;:976},&amp;#34;visibility&amp;#34;:10000,&amp;#34;wind&amp;#34;:{&amp;#34;speed&amp;#34;:7.39,&amp;#34;deg&amp;#34;:307,&amp;#34;gust&amp;#34;:11.23},&amp;#34;clouds&amp;#34;:{&amp;#34;all&amp;#34;:64},&amp;#34;dt&amp;#34;:1682401802,&amp;#34;sys&amp;#34;:{&amp;#34;country&amp;#34;:&amp;#34;AU&amp;#34;,&amp;#34;sunrise&amp;#34;:1682375848,&amp;#34;sunset&amp;#34;:1682415263},&amp;#34;timezone&amp;#34;:28800,&amp;#34;id&amp;#34;:2070753,&amp;#34;name&amp;#34;:&amp;#34;Gnowangerup&amp;#34;,&amp;#34;cod&amp;#34;:200}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then I need to do the same sort of thing again - split the string using a delimiter, and just keep the part with the termperature in it. This time we&amp;rsquo;ll use a comma , as the delimiter, and only keep the part in front of it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cut -d&amp;#39;,&amp;#39; -f1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We&amp;rsquo;re saying cut this string in to bits were the delimiter &lt;code&gt;-d&lt;/code&gt; is a comma, then output the first field.&lt;/p&gt;
&lt;p&gt;You might be wondering why I didn&amp;rsquo;t just use &lt;code&gt;awk&lt;/code&gt; again - I could have, but &lt;code&gt;cut&lt;/code&gt; is simpler. The reason I didn&amp;rsquo;t use &lt;code&gt;cut&lt;/code&gt; both times is that it can only take a single character as a delimiter. In fact, the first version I wrote of this script only used &lt;code&gt;cut&lt;/code&gt;, and I had the delimiters as colon for the first cut and comma for the second. As I was writing it, I was thinking that I should stress in the blog post about it that it was quite fragile - a small change in the JSON (for example adding a field, or changing the order - both things that would not cause a problem to a good Swift or JS JSON library) would break it. Then the weather changed and so was two layers of clouds, and the script broke and output the time as &lt;code&gt;{&amp;quot;all&amp;quot;&lt;/code&gt; instead of a number.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-25-at-3.00.13-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;printf&lt;/code&gt; just outputs the two values - temperature and timestamp as plain text with a comma between them into a text file that&amp;rsquo;s in the root of the Nginx webserver.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-25-at-8.06.34-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-25-at-8.06.34-pm.png" width="794" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now that&amp;rsquo;s in place, I just edited &lt;code&gt;/etc/crontab&lt;/code&gt; to have the new script run every five minutes to update the file with the temperature and timestamp.&lt;/p&gt;
&lt;h3 id="server-temp-logging"&gt;Server Temp Logging&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;ve already seen most of this, but I&amp;rsquo;ve made a couple of additions.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#5e81ac;font-style:italic"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;#check drivetemp has been loaded - needed for ssd temp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; ! lsmod &lt;span style="color:#eceff4"&gt;|&lt;/span&gt; grep -wq drivetemp&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; &lt;span style="color:#81a1c1;font-weight:bold"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modprobe drivetemp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;#collect the temp data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pch_name&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;cat /sys/class/hwmon/hwmon0/name&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pch_temp&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;cat /sys/class/hwmon/hwmon0/temp1_input&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cpu_name&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;cat /sys/class/hwmon/hwmon1/name&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cpu_temp&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;cat /sys/class/hwmon/hwmon1/temp1_input&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssd_name&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;cat /sys/class/hwmon/hwmon2/name&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssd_temp&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;cat /sys/class/hwmon/hwmon2/temp1_input&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;#this should contain the current outside temp and unix time&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;outside_temp&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;curl -s &lt;span style="color:#a3be8c"&gt;&amp;#34;https://iankulin.com/gnp_temp.txt&amp;#34;&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;log_file&lt;span style="color:#81a1c1"&gt;=&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;&amp;#34;/var/log/temps.csv&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;# Print the temperatures to a log file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1"&gt;printf&lt;/span&gt; &lt;span style="color:#a3be8c"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;$(&lt;/span&gt;date +&lt;span style="color:#a3be8c"&gt;&amp;#39;%d/%m/%Y,%T&amp;#39;&lt;/span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;)&lt;/span&gt;&lt;span style="color:#a3be8c"&gt;,%s,%d,%s,%d,%s,%d,out,%s\n&amp;#34;&lt;/span&gt; $pch_name $pch_temp $cpu_name $cpu_temp $ssd_name $ssd_temp $outside_temp &amp;gt;&amp;gt; $log_file
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We&amp;rsquo;ve already discussed how the curl works - this one is picking up the script we wrote to run on the VPS earlier. More interesting is checking for the &lt;code&gt;drivetemp&lt;/code&gt; module.&lt;/p&gt;
&lt;p&gt;The drivetemp module needs to be loaded into the Linux kernel before we can read the SSD temperature with the line.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ssd_temp=`cat /sys/class/hwmon/hwmon2/temp1_input`
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once it&amp;rsquo;s loaded, it stays there, unless computer is shutdown for any reason. There&amp;rsquo;s a &lt;a href="https://www.baeldung.com/linux/run-script-on-startup"&gt;number of places&lt;/a&gt; we can execute things on startup, but really this &lt;code&gt;drivetemp&lt;/code&gt; module is only needed for this script, so we should do it here. As far as I can make out, telling Linux to load a module that&amp;rsquo;s already loaded does not do any harm, and at once every five minutes it&amp;rsquo;s hardly going to cause a performance issue. Nevertheless, some sort of programmer ethics compels me to only do it if its needed.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;#check drivetemp has been loaded - needed for ssd temp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#81a1c1;font-weight:bold"&gt;if&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;!&lt;/span&gt; lsmod &lt;span style="color:#81a1c1"&gt;|&lt;/span&gt; grep &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;wq drivetemp&lt;span style="color:#eceff4"&gt;;&lt;/span&gt; then
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modprobe drivetemp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fi
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;lsmod&lt;/code&gt; returns a list of the loaded modules, this is passed to the &lt;code&gt;grep&lt;/code&gt;. &lt;code&gt;grep&lt;/code&gt; looks through lines of input and usually returns any lines that match. However in this case, we&amp;rsquo;re using the &lt;code&gt;-q&lt;/code&gt; (quiet) option. With this option on, instead of lines of text, you get nothing on the standard output, instead it sets the exit code to 0 (true) if it&amp;rsquo;s found, or 1 (false) if not.&lt;/p&gt;
&lt;p&gt;Since I&amp;rsquo;m interested in only running the &lt;code&gt;modprobe&lt;/code&gt; if &lt;code&gt;drivetemp&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; found, I have to negate the result of the &lt;code&gt;grep&lt;/code&gt; with &lt;code&gt;!&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;After that, all the temperature data is collected, then written out to a log fie for later processing.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-25-at-9.00.47-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="the-results"&gt;The Results&lt;/h3&gt;
&lt;p&gt;Here&amp;rsquo;s 24 hours of the five minute temperature logs. For each server I averaged the three different temperatures (PCH, CPU core, and SSD drive) and graphed them along with the outside temperature from OpenWeather. &lt;code&gt;pve-prod1&lt;/code&gt; is the only one doing any real work here. It hosts my Jellyfin media server on a VM, and another VM with a collection of utilities such as Uptime Kuma. The Y axis is degrees centigrade.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/20230427-server-temps.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/img_4315.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The spike in &lt;code&gt;pve-dev&lt;/code&gt;1 at 2100 was caused by me stress testing one core to 100% load for ten minutes. I think I can see &lt;code&gt;pve-prod2&lt;/code&gt; (which sits directly on top of &lt;code&gt;pve-dev1&lt;/code&gt;) warming up a little as well. But strangely, and perhaps I&amp;rsquo;m imagining it, it seems like &lt;code&gt;pve-prod1&lt;/code&gt; (which sits on top of the stack) was a bit cooler in that time?&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t remember if I watched some TV between 6 and 8pm, but it looks like I did, and the spike at 2am will be the nightly snapshots being taken and sent off to the NAS.&lt;/p&gt;
&lt;p&gt;You can see that &lt;code&gt;pve-prod2&lt;/code&gt; and &lt;code&gt;pve-dev1&lt;/code&gt; were turned on to run this test, and it takes about 40 minutes for them to warm up. It&amp;rsquo;s interesting to notice the bigger amplitude of the production machine compared to the others just idling. And also interesting that &lt;code&gt;pve-dev1&lt;/code&gt; (which wasn&amp;rsquo;t running any load till I ran the stress test on it) was just generally warmer that &lt;code&gt;pve-prod1&lt;/code&gt; which was running a small work load.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t remember if I watched some TV between 6 and 8pm, but it looks like I did, and the spike at 2am will be the nightly snapshots being taken and sent off to the NAS.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at pve-dev1 while the stress test was running.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/pve-dev1-temp.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It makes sense that the PCH which is mm away from, and directly connected to, the CPU would warm up as the CPU was hammered with square root calculations, and since the drive temp is a up a little so I guess that reflects the ambient temperature inside the case.&lt;/p&gt;
&lt;p&gt;The CPU temperature hadn&amp;rsquo;t plateaued yet, so it might be interesting to run it until it does one day and see what that looks like.&lt;/p&gt;</description></item><item><title>Git/GutHub - macOS - marking file as executable</title><link>https://blog.iankulin.com/git-guthub-macos-marking-file-as-executable/</link><pubDate>Sun, 30 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/git-guthub-macos-marking-file-as-executable/</guid><description>&lt;p&gt;I&amp;rsquo;m working on the world&amp;rsquo;s shortest shell script - it&amp;rsquo;s called by &lt;code&gt;cron&lt;/code&gt; to pull down a JSON weather report to a text file using &lt;code&gt;curl&lt;/code&gt; so I can expose it on an Nginx endpoint. The purpose is to allow me to hammer that weather API from multiple machines I control without violating the TOS of my free API key.&lt;/p&gt;
&lt;p&gt;Because I&amp;rsquo;m learning all the things, instead of just creating this on the VPS where it runs, it&amp;rsquo;s cloned from my GitHub repo for that machine. I&amp;rsquo;m creating and editing the file in VS Code on macOS, pushing to Github, then pulling the changes on the Ubuntu VPS. The intention is that this will eventually become automated with a Github action.&lt;/p&gt;
&lt;p&gt;The problem I&amp;rsquo;ve run into is that I want the file permissions so show the file is executable so when it arrives on the VPS - so no &lt;code&gt;chmod&lt;/code&gt; is required to make it usable.&lt;/p&gt;
&lt;p&gt;Some googling suggested that the executable flag (but none of the other file permissions) is stored and handled by git, and furthermore, there&amp;rsquo;s a git command to set it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git update-index --chmod=+x bin/fetchWeather.sh 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So I wrote my (one line) script, applied the command above, committed and pushed, then pulled it down on the VPS and the bit wasn&amp;rsquo;t set. So somewhere in this chain there&amp;rsquo;s a problem.&lt;/p&gt;
&lt;p&gt;At this stage, it&amp;rsquo;s helpful to know that if the executable bit is set for a file, GitHub shows this in the header of the file where it says how many lines etc.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.26.25-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.26.41-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;In my case, it was showing that the file was not marked as executable in GitHub, so the problem was that the &lt;code&gt;git update-index&lt;/code&gt; was not working for me for some reason.&lt;/p&gt;
&lt;p&gt;A bit more investigation turned up that there&amp;rsquo;s a setting in the &lt;code&gt;.git/config&lt;/code&gt; file called &lt;code&gt;filemode&lt;/code&gt; that controls if the originating file system executable status is preserved. That sounded promising - I was expecting to find that is was set to false, and I could change it to true, and it would fix my problem. I had a quick look and, oh, it&amp;rsquo;s already set to true.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.36.54-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.36.54-pm.png" width="656" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Seems like it&amp;rsquo;s involved though, so perhaps (my thinking went) I should change it to false and see if the problem goes away&amp;hellip;. and it did. I changed this value to &lt;code&gt;false&lt;/code&gt;, applied the executable bit with the &lt;code&gt;git update-index&lt;/code&gt; command, committed, pushed it to GitHub (it was marked executable), pulled it down to the VPS, it was still marked executable!&lt;/p&gt;
&lt;p&gt;My whole tech life, I&amp;rsquo;ve never been happy with solutions to problems where I don&amp;rsquo;t understand the underlying reasons. If things just start working when you&amp;rsquo;re fiddling around and you&amp;rsquo;re not clear on why, it feels like they could change back with just as easily and with no more reason.&lt;/p&gt;
&lt;p&gt;A clue to what&amp;rsquo;s going on (many readers will already have figured this out) was given to me by ChatGPT. When I was asking it about this issue, it kept insisting I should &lt;code&gt;chmod&lt;/code&gt; the file to be executable before I committed it. I had to be really clear with it that this wasn&amp;rsquo;t possible on macOS because it doesn&amp;rsquo;t have that sort of file permissions&amp;hellip;&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/cain.jpg" width="140" alt=""&gt;
&lt;p&gt;Of course, in fact, it does. &lt;a href="https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/BSD/BSD.html"&gt;macOS is based on FreeBSD&lt;/a&gt; (&amp;ldquo;without the good bits&amp;rdquo; goes the old joke told at Unix conferences). I&amp;rsquo;d just somehow forgotten this - I guess in Linux I&amp;rsquo;m used to explicitly seeing them every time I look at a directory contents, but never see it on Mac. Even if you go into &amp;ldquo;Get Info&amp;rdquo; for a file in Finder on the mac, you can see the read/write permissions, but not the executable bit status.&lt;/p&gt;
&lt;p&gt;So how do you set and view the executable status on mac? Exactly the same as on any Unix.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.52.17-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-22-at-4.52.17-pm.png" width="1000" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I did that, and changed the /&lt;code&gt;git/config filemode&lt;/code&gt; back to &lt;code&gt;true&lt;/code&gt;. Committed and pushed the file up (without worrying about the &lt;code&gt;git update-index&lt;/code&gt;) and it showed up in GitHub as executable, pulled it down, still executable.&lt;/p&gt;</description></item><item><title>Linux Shell Script for Temperature Logging</title><link>https://blog.iankulin.com/linux-shell-script-for-temperature-logging/</link><pubDate>Thu, 27 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/linux-shell-script-for-temperature-logging/</guid><description>&lt;p&gt;A potential solution to my concern about the either perfect, or nearly dead, SSD would be to add a NVMe disk to the M.2 slot in the HP Elitedesk 800 G2&amp;rsquo;s. I&amp;rsquo;d use those to boot from and run Proxmox, then the existing SSD&amp;rsquo;s on each node in the cluster would just be part of the CephFS pool that has some redundancy built into it and hosts the VMs that are not using the NAS for their storage.&lt;/p&gt;
&lt;p&gt;These &amp;lsquo;gumstick&amp;rsquo; NVMe drives are remarkably good value in the smaller sizes at the moment, with Samsung 250GB NVMe&amp;rsquo;s costing less than a pack of cigarettes in Australia.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-20-at-7.02.57-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;A small concern I&amp;rsquo;ve got about that, and about the (very cute looking) way I&amp;rsquo;ve just got the computers all stacked on top of each other, is about the internal temperatures. I noticed SSD temperatures in the SMART data I was looking the other day, and I&amp;rsquo;ve seen CPU temperatures somewhere, so this data is available. So I set out on a quest to log some of it so I could do a before and after (NMVe installation) look at the temperatures.&lt;/p&gt;
&lt;p&gt;&lt;a href="http://www.pyroelectro.com/tutorials/cron-automation/check.html"&gt;This article&lt;/a&gt; was very close to what I wanted - a shell script to run in a cron job that would log the drive and CPU temperatures. The script goes a fair way beyond that, but my main issue was that it uses a couple of packages - &lt;code&gt;sensors&lt;/code&gt;, and &lt;code&gt;hddtemp&lt;/code&gt;. I like to avoid dependencies if I can, but I also thought the temp data was pretty simple and is probably just sitting in the &lt;code&gt;/sys/&lt;/code&gt; directory tree somewhere.&lt;/p&gt;
&lt;p&gt;That sort of turned out to be true. In /sys/class/hwmon/ there&amp;rsquo;s a couple of directories (actually symlinks) for bits of hardware that can be monitored for temperature, and in those directories are text files with some values, the ones we&amp;rsquo;re interested in being &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;temp1_input&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve-dev1:~# tree /sys/class/hwmon/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/sys/class/hwmon/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── hwmon0 -&amp;gt; ../../devices/virtual/thermal/thermal_zone0/hwmon0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├── hwmon1 -&amp;gt; ../../devices/platform/coretemp.0/hwmon/hwmon1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└── hwmon2 -&amp;gt; ../../devices/pci0000:00/0000:00:17.0/ata1/host0/target0:0:0/0:0:0:0/hwmon/hwmon2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;3 directories, 0 files
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve-dev1:~# ls /sys/class/hwmon/hwmon0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;device name power subsystem temp1_input uevent
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve-dev1:~# cat /sys/class/hwmon/hwmon0/name /sys/class/hwmon/hwmon0/temp1_input
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pch_skylake
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;45500
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://commons.wikimedia.org/w/index.php?curid=9817206"&gt;&lt;img src="https://blog.iankulin.com/images/intel_5_series_architecture.png" width="232" alt=""&gt;&lt;/a&gt;
&lt;em&gt;Intel 5 architecture - Anas hashmi&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The first two are temperatures of the &lt;a href="https://commons.wikimedia.org/w/index.php?curid=9817206"&gt;Platform Controller Hub (PCH)&lt;/a&gt; and actual CPU. Both of these values were already just sitting there.&lt;/p&gt;
&lt;p&gt;The third value, for the SSD temperature didn&amp;rsquo;t appear until added by running &lt;code&gt;modprobe drivetemp&lt;/code&gt; to load a kernel module.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the three values I want to log sorted then, but how to go about it? &lt;a href="http://www.pyroelectro.com/tutorials/cron-automation/check.html"&gt;That first article&lt;/a&gt; I mentioned had a shell script using printf() to output some values to a log file, then the script was triggered by a cron job. Two things I&amp;rsquo;ve never done before, so let&amp;rsquo;s dive in. Here&amp;rsquo;s the finished code.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-20-at-8.32.11-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I wrote MS-DOS batch files in the 1980s so this wasn&amp;rsquo;t completely alien to me. A few points were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Everyone else&amp;rsquo;s shell scripts start with &lt;code&gt;#!/bin/bash&lt;/code&gt; so I assume that&amp;rsquo;s compulsory. Other lines starting with &lt;code&gt;#&lt;/code&gt; are comments.&lt;/li&gt;
&lt;li&gt;In that list of variables, each one is filled with the results of the command being assigned to it in the single quotes. So if typing in &lt;code&gt;cat /sys/class/hwmon/hwmon0/name&lt;/code&gt; at the terminal prompt would result in the output &lt;code&gt;pch_skylake&lt;/code&gt;, then the variable &lt;code&gt;pch_name&lt;/code&gt; will contain &lt;code&gt;pch_skylake&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If you&amp;rsquo;re not coming to this from a programming background, this printf() command is going to look weird.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;printf &amp;#34;$(date +&amp;#39;%d/%m/%Y,%T&amp;#39;),%s,%d,%s,%d,%s,%d\n&amp;#34; $pch_name $pch_temp $cpu_name $cpu_temp $ssd_name $ssd_temp &amp;gt;&amp;gt; $log_file
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;How these work is that the first part is the string to print, but it has some placeholders (all those &lt;code&gt;%&lt;/code&gt;letters). At runtime, the values from the end of the line are inserted into them. Like this:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve-dev1:~# printf &amp;#34;Hello %s\n&amp;#34; &amp;#34;Ian&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Hello Ian
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;%s&lt;/code&gt; is a placeholder for a some text, then we supply the text at the end - &lt;code&gt;&amp;quot;Ian&amp;quot;&lt;/code&gt;. In this case, &lt;code&gt;&amp;quot;Ian&amp;quot;&lt;/code&gt; is a string literal, if we&amp;rsquo;d used a variable (as in our logging script) then the contents of the variable would be used instead. The &lt;code&gt;\n&lt;/code&gt; at the end of the string is a newline character so whatever comes after starts on a new line.&lt;/p&gt;
&lt;p&gt;At this point I know enough about Linux permissions that I knew I&amp;rsquo;d have to set the shell script file to be executable with a &lt;code&gt;chmod 755&lt;/code&gt;, and to call it with the &lt;code&gt;./&lt;/code&gt; in front of it that I was perplexed about a couple of days ago.&lt;/p&gt;
&lt;p&gt;Again, the original article gave an example of the line to put into /etc/crontab. It just needed the path to my script and it was good to go.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Example of job definition:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# .---------------- minute (0 - 59)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# | .------------- hour (0 - 23)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# | | .---------- day of month (1 - 31)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# | | | | |
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# * * * * * user-name command to be executed
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;*/5 * * * * root /root/bin/tempCheck.sh 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next thing you know, the log file is slowly growing at &lt;code&gt;/var/log/temps.csv&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;20/04/2023,20:30:01,pch_skylake,45000,coretemp,38000,drivetemp,38000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;20/04/2023,20:35:01,pch_skylake,45000,coretemp,37000,drivetemp,38000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;20/04/2023,20:40:01,pch_skylake,44500,coretemp,37000,drivetemp,38000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;20/04/2023,20:45:01,pch_skylake,45000,coretemp,37000,drivetemp,38000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;20/04/2023,20:50:01,pch_skylake,44500,coretemp,37000,drivetemp,38000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;20/04/2023,20:55:01,pch_skylake,44500,coretemp,37000,drivetemp,38000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;20/04/2023,21:00:01,pch_skylake,45000,coretemp,37000,drivetemp,38000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;20/04/2023,21:05:01,pch_skylake,45500,coretemp,38000,drivetemp,38000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;20/04/2023,21:10:01,pch_skylake,45000,coretemp,38000,drivetemp,38000
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;20/04/2023,21:15:01,pch_skylake,45500,coretemp,37000,drivetemp,38000
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Obviously I&amp;rsquo;m going to graph this, and also obviously, I&amp;rsquo;m going to run a CPU stress test in a VM in the middle of the data collection.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/temp-chart.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>Why use './' in front of filenames?</title><link>https://blog.iankulin.com/why-use-in-front-of-filenames/</link><pubDate>Sun, 23 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/why-use-in-front-of-filenames/</guid><description>&lt;p&gt;In Linux (and MS-DOS I guess) the period signifies the current directory, so if I have a file in the current directory called &lt;code&gt;test.txt&lt;/code&gt;, I can refer to it as &lt;code&gt;test.txt&lt;/code&gt; or &lt;code&gt;./test.txt&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@enrico-rider:~$ cat test.txt
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@enrico-rider:~$ cat ./test.txt
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I mostly see this in references to files in HTML and have often wondered why. Here it is being used in a Udemy course I&amp;rsquo;m following.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-10.49.00-am.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s one of those things that&amp;rsquo;s difficult to Google, so these days my reflex is to ask ChatGPT such questions.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-11.17.53-am.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-11.17.53-am.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Okay. That makes sense for executable files. If you just type in the name, Linux will look in the current directory, then if not found, in each of the directories in your $PATH variable. But if you add the ./ to the front, it will only look in your current directory. This claim of ChatGPT&amp;rsquo;s is easily tested, lets try with &lt;code&gt;cat&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@enrico-rider:~$ cat test.txt
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@enrico-rider:~$ ./cat test.txt
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-bash: ./cat: No such file or directory
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@enrico-rider:~$ cp /usr/bin/cat cat
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@enrico-rider:~$ ./cat test.txt
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So that checks out, but it doesn&amp;rsquo;t explain the main place I see it - in HTML.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-12.00.12-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Again, that makes sense. But it still doesn&amp;rsquo;t answer why the instructor in my course is using it for:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cp /etc/passwd ./users.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-12.06.52-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Lol - I feel this is a real edge case. I can see it being more a problem with the first file rather than the second one. eg.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-12.21.05-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-19-at-12.21.05-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So, TL:DR; using &amp;lsquo;./&amp;rsquo; in front of a filename can be useful when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;executing a shell script in the current directory (to avoid ambiguity with other executable files in your PATH);&lt;/li&gt;
&lt;li&gt;when running a command that takes filenames as arguments, and the filename might be confused with an argument;&lt;/li&gt;
&lt;li&gt;in HTML to avoid confusion between the directory the current file is in and the root directory of the web server.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Mounting NFS shares into LXC containers</title><link>https://blog.iankulin.com/mounting-nfs-shares-into-lxc-containers/</link><pubDate>Fri, 21 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/mounting-nfs-shares-into-lxc-containers/</guid><description>&lt;p&gt;I&amp;rsquo;m playing with &lt;a href="https://syncthing.net/"&gt;Syncthing&lt;/a&gt; with the idea that it might be a good replacement for Dropbox. There wasn&amp;rsquo;t a Docker container listed in the install options, so I thought this might be a good app to run in an LXC.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m going to use a share from the NAS, and I&amp;rsquo;m assuming I&amp;rsquo;ll need it mount it into the container for Syncthing to access. I&amp;rsquo;m experienced enough to know that I&amp;rsquo;m going to want a privileged container, and I thought I&amp;rsquo;d done all the NFS sharing correctly, but when I tried to mount the NFS share, I was getting an error.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@ct356-syncthing:~# showmount -e 192.168.100.32
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Export list for 192.168.100.32:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/volume1/syncthing 192.168.100.37
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/volume1/proxmox 192.168.100.24,192.168.100.31,192.168.100.28,192.168.100.23
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@ct356-syncthing:~# mount -t nfs 192.168.100.32:/volume1/syncthing /mnt/syncthing
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mount.nfs: access denied by server while mounting 192.168.100.32:/volume1/syncthing
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is just part of the security nature of the LXC containter getting in our way. We can edit the &lt;code&gt;.conf&lt;/code&gt; for the container, or just change it in the container options via the web GUI and restart the container.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-18-at-7.19.41-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-18-at-7.19.41-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-18-at-7.49.05-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I learned this from &lt;code&gt;[theorangeone](https://theorangeone.net/posts/mount-nfs-inside-lxc/)&lt;/code&gt;.&lt;/p&gt;</description></item><item><title>Running Multiple Linux Commands in One Line</title><link>https://blog.iankulin.com/running-multiple-linux-commands-in-one-line/</link><pubDate>Wed, 19 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/running-multiple-linux-commands-in-one-line/</guid><description>&lt;p&gt;Since I&amp;rsquo;m constantly standing up Linux virtual machines and containers - almost always of the &lt;code&gt;apt&lt;/code&gt; variety, I&amp;rsquo;m constantly typing in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;apt update
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;apt upgrade
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then hitting enter again to allow whatever installation is needed to proceed. I&amp;rsquo;ve noticed in some of the commands I&amp;rsquo;ve been pasting in from installation instructions or StackExchange solutions have been separated by characters that look like it allows several commands to be run one after the other. To cut a long story short, the commands above could be entered like this with double ampersands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;apt update &amp;amp;&amp;amp; apt upgrade
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The ampersands mean that the second command will run after the first one as long as it completes without an error. To avoid having to hit enter again, we can add the &lt;code&gt;-y&lt;/code&gt; flag.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;apt update &amp;amp;&amp;amp; apt upgrade -y
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;According to the &lt;a href="https://www.makeuseof.com/how-to-run-multiple-linux-commands-at-once/"&gt;MakeUseOf article&lt;/a&gt; I learned about the double ampersands from, we could also have used a semicolon if we want each command to run regardless of the success of the previous one, or a double pipe if you only wanted the second command run run if the first one fails.&lt;/p&gt;</description></item><item><title>Linux on HP Mini 110</title><link>https://blog.iankulin.com/linux-on-hp-mini-110/</link><pubDate>Mon, 17 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/linux-on-hp-mini-110/</guid><description>&lt;p&gt;I&amp;rsquo;ve been furthering my Linux education by playing with some desktop distros in VMs, but it&amp;rsquo;s not a great experience accessing them through the Proxmox web GUI. The alternative to this is to use a good &lt;a href="https://en.wikipedia.org/wiki/Simple_Protocol_for_Independent_Computing_Environments"&gt;SPICE&lt;/a&gt; client on the remote desktop, but there is &lt;a href="https://forum.proxmox.com/threads/access-vm-thru-spice-on-osx.66727/"&gt;not a simple good solution&lt;/a&gt; for this for MacOS.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been playing with the idea of picking up an old i3/i5 Thinkpad - these are around the AUD130 mark on eBay, to run a Linux distro with the main idea being to use it to SPICE into my VMs.&lt;/p&gt;
&lt;p&gt;This weekend at my parents house, I&amp;rsquo;ve been going through the cupboard secure wiping a couple of the discarded laptops, and found a fancy looking HP Mini 110-1131dx.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.notebookcheck.net/HP-Mini-110-Series.24414.0.html"&gt;&lt;img src="https://blog.iankulin.com/images/9563377_ra.jpg" width="500" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.notebookcheck.net/HP-Mini-110-Series.24414.0.html"&gt;&lt;img src="https://blog.iankulin.com/images/9563377cv3a.jpg" width="500" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This was a netbook, quite a cute little thing, and like most HP hardware - well made and popular enough that I should be able to googlesolve any issues I encounter. The Atom N270 that mousepowers it is a single core 32bit baby, but there&amp;rsquo;s also a moderate graphics accelerator chip - the &lt;a href="https://www.notebookcheck.net/Intel-Graphics-Media-Accelerator-950.2177.0.html"&gt;GMA 950&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Mint or Ubuntu would probably be my first choices for a desktop distro, but given the very low specs of the HP 110, I&amp;rsquo;m guessing that&amp;rsquo;s not going to be a good experience even if you go back to the most recent 32 bit versions. After a bit of googling around, I decided &lt;a href="https://antixlinux.com/"&gt;antiX&lt;/a&gt; or &lt;a href="https://lubuntu.net/"&gt;Lubuntu&lt;/a&gt; might be good choices (I&amp;rsquo;m partial to the &lt;code&gt;apt get&lt;/code&gt; family of distros).&lt;/p&gt;
&lt;h3 id="antix"&gt;antiX&lt;/h3&gt;
&lt;p&gt;As with most modern distros, the install was a painless experience - booting from the USB and following prompts. The UI was pleasant and crisp, and I especially liked the background on the desktop with some live statistics.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.linuxinsider.com/story/antix-linux-not-pretty-but-highly-functional-86942.html"&gt;&lt;img src="https://blog.iankulin.com/images/86942_antix-3-small.jpg" width="620" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;From the base install, the wireless would not work properly. On the HP 100, there&amp;rsquo;s a little momentary switch on the front left of the keyboard with an indicator light. It was correctly indicating that the wireless was disabled (by glowing orange) and if I flicked it, I could see in the settings the bluetooth was going off and on, but not the wireless.&lt;/p&gt;
&lt;h2 id="lubuntu"&gt;Lubuntu&lt;/h2&gt;
&lt;p&gt;Again, a painless install experience. The ISO was about twice the size at 2.7GB and the install took a lot longer, although much of it was familiar to me from the numerous Debian and Ubuntu server installs I&amp;rsquo;ve done. Once it was installed and booted, the desktop seemed a bit chunky and dated compared to the flatter antiX, and although slower it was very usable.&lt;/p&gt;
&lt;img src="https://blog.iankulin.com/images/img_4846b.jpg" width="1000" alt=""&gt;
&lt;p&gt;The wifi didn&amp;rsquo;t work, although the indicator was blue suggesting it was turned on. In the menu was an option to check for needed propitiatory drivers, and when I plugged into the Ethernet and ran this, it decided there was a Broadcom chipset wifi that it knew the drivers for. I allowed it to fetch and install them, and the wireless came to life.&lt;/p&gt;
&lt;p&gt;The proprietary drivers not being installed is a common and reasonable thing, and almost certainly the issue with my antiX install, so it seemed like it was probably worth having another go at that plugged into the ethernet and enabling whatever is needed to allow the non-FOSS stuff.&lt;/p&gt;
&lt;h2 id="antix-wifi"&gt;antiX wifi&lt;/h2&gt;
&lt;p&gt;My first thought was that perhaps I could just enable the non-free option for the Debian sources. The way that the apt package manager works is that there&amp;rsquo;s a list of sources it checks with. Some distros are strict about non-FOSS stuff and this needs changed in /etc/apt/sources to &lt;a href="https://serverfault.com/questions/240920/how-do-i-enable-non-free-packages-on-debian"&gt;make it check the non-free parts of the repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;antiX had a slightly different setup, with a whole directory of sources, but the non-free option was set. I also mucked around with the &lt;code&gt;rfkill&lt;/code&gt; command which kept saying the softblock was on - although I could see, by using rfkill list that the hardware button was working exactly how it should - flick it once and the hardware block for wifi and bluetooth was activated, flick it again and it went off.&lt;/p&gt;
&lt;p&gt;I also jumped off the cliff of just trying commands that I found on the internet that were suggested for similar sounding situations, and that I only had the shakiest idea of what they did. I&amp;rsquo;m certain that the problem at this stage is that I need to install those Broadcom 43 drivers. Without something poping up to ask me if I want to do that (which is exactly the sort of thing a lean distro wouldn&amp;rsquo;t have) I&amp;rsquo;m a bit lost.&lt;/p&gt;
&lt;p&gt;Antix seemed so right for my purposes, I might come back and try it again when my Linux knowledge is a bit better. In the mean time, I need a popular distro, lighter weight than Lubuntu, so I&amp;rsquo;ll give Mint a shot.&lt;/p&gt;
&lt;h2 id="mint-xfce"&gt;Mint Xfce&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://distrowatch.com/"&gt;Distrowatch&lt;/a&gt; currently lists Mint as #3, so it meets my &amp;ldquo;popular&amp;rdquo; criterion, and is has a Xfce (lightweight desktop environment) version, so perhaps it will run crisply on the Atom, but still hold my hand to install these wifi drivers.&lt;/p&gt;
&lt;p&gt;As with all the other distros, it went on smoothly. Once I booted in to it, some sort of system checker popped up to complain about the Broadcom drivers, and offered to extract them from the install USB - that didn&amp;rsquo;t work for me since I&amp;rsquo;d used &lt;a href="https://www.ventoy.net/en/index.html"&gt;Ventoy&lt;/a&gt; for the install, and the ISO was not loaded after I&amp;rsquo;d rebooted. Heading back up to the office to reconnect to an Ethernet cable allowed the system to download the drivers and five minutes later I was on wifi.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure if I&amp;rsquo;ve used it enough to be sure, but the performance with Mint Xfce seems similar to Lubuntu - ie not as good as antiX. Also, there&amp;rsquo;s a scary message saying long term support runs out in nine days since I had to go back to version 19.3 to find a 32 bit version.&lt;/p&gt;</description></item><item><title>Recursively Deleting Files in Linux</title><link>https://blog.iankulin.com/recursively-deleting-files-in-linux/</link><pubDate>Fri, 14 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/recursively-deleting-files-in-linux/</guid><description>&lt;p&gt;I&amp;rsquo;ve been using this rsync command to backup files from my NAS to a USB drive. The &amp;ndash;excludes are to avoid copying over some junk hidden files - some created by MacOS and some by Synology.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo rsync -rvit --exclude &amp;#39;*@eaDir*&amp;#39; --exclude &amp;#39;.DS_Store&amp;#39; /volume1/media/ /volumeUSB1/usbshare1-2/media --del
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;.DS_Store&lt;/code&gt; files seem to be dropped by MacOS every time I view a directory on the NAS from my MacBook. They&amp;rsquo;re not doing any harm, and they presumably do something handy for the Mac - remembering the view settings for that folder or some such. Nevertheless, they annoy me. It makes sense to not back them up - they don&amp;rsquo;t serve any useful purpose in that context.&lt;/p&gt;
&lt;p&gt;If I wanted to delete them anyway, how would I go about it? They are scattered randomly around, including in sub-directories of sub-directories. Is there some recursive flag I can add to &lt;code&gt;rm&lt;/code&gt; to accomplish this?&lt;/p&gt;
&lt;p&gt;I found a better solution on &lt;a href="https://askubuntu.com/questions/377438/how-can-i-recursively-delete-all-files-of-a-specific-extension-in-the-current-di"&gt;AskUbuntu&lt;/a&gt;, we can use the &lt;code&gt;find&lt;/code&gt; command. This way it&amp;rsquo;s easy to safely test your filename matching first before you destroy any files.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;find . -name &amp;#34;.DS_Store&amp;#34; -type f
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;-name&lt;/code&gt; is clear enough, and the &lt;code&gt;-type f&lt;/code&gt; option is just saying to look for files (rather than directories etc). The period at the start is the location, ie, start in the directory above so the current working directory is included. Once you&amp;rsquo;ve tuned this, you can add &lt;code&gt;-delete&lt;/code&gt; to go nuclear.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;find . -name &amp;#34;.DS_Store&amp;#34; -type f -delete
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Using NAS for Proxmox backups</title><link>https://blog.iankulin.com/using-nas-for-proxmox-backups/</link><pubDate>Mon, 10 Apr 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/using-nas-for-proxmox-backups/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/moving-a-vm-between-two-proxmox-hosts/"&gt;A few weeks ago&lt;/a&gt;, I was very excited to be able to take a snapshot of a virtual machine, copy it across the network from that Proxmox node, copy it back across the network to a different Proxmox node, start it there, and have it up and running, without it noticing it was actually on different hardware.&lt;/p&gt;
&lt;p&gt;Backing up a VM is pretty simple, you just click on the node, choose &lt;em&gt;Backup&lt;/em&gt; and click the &lt;em&gt;Backup Now&lt;/em&gt; button. The ease, and completeness of backing up a VM is one of the main reasons I&amp;rsquo;m using Proxmox for my systems.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-12.02.59-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-12.02.59-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;By default, VM backups are saved to the &amp;ldquo;local drive&amp;rdquo; - actually the &lt;code&gt;/var/lib/vz&lt;/code&gt; directory. This would not be useful if the physical machine dies, but also it&amp;rsquo;s not convenient to restore to a different machine. Ideally you&amp;rsquo;d have a central place to store these files that was accessible to all the Proxmox nodes.&lt;/p&gt;
&lt;p&gt;This is exactly the situation I&amp;rsquo;ve setup with my lab, the NAS is the storage for the VM backups. Each of the Proxmox nodes uses the same directory for backups, so moving a machine from one node to another is a simple as backing it up on one node, stopping the VM, and restoring it on another node just by choosing the backup file to restore in the web GUI.&lt;/p&gt;
&lt;h3 id="steps"&gt;Steps&lt;/h3&gt;
&lt;p&gt;Proxmox can use all sorts of shares as a location for backups (and other files such as the ISO&amp;rsquo;s used to boot new machines), but the simplest is probably &lt;a href="https://en.wikipedia.org/wiki/Network_File_System"&gt;NFS&lt;/a&gt;. This is also straightforward to do from the Synology NAS.&lt;/p&gt;
&lt;p&gt;In the web interface for the NAS, go into &lt;em&gt;Control Panel&lt;/em&gt;, &lt;em&gt;Shared Folder&lt;/em&gt; and create a new shared folder. I called mine Proxmox. One of the tabs there is for NFS permissions - just add the IP address of the Proxmox node that you&amp;rsquo;d life to access the folder.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-1.46.02-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not much harder from the Proxmox end. Although the storage you add will appear at the node level in the &lt;em&gt;Server View&lt;/em&gt; of the web GUI, it is added at the &lt;em&gt;Datacenter&lt;/em&gt; level.&lt;/p&gt;
&lt;p&gt;Go into &lt;em&gt;Storage&lt;/em&gt;, select &lt;em&gt;Add&lt;/em&gt; and choose &lt;em&gt;NFS&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.00.04-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.00.04-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Then enter an ID (this will be the name of the storage in Proxmox) and the IP address. If you wait half a second, then you can click the dropdown for all the folders that are shared from that IP address.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.06.19-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The last field is content - this refers the the type of Proxmox stuff you want to keep in there - for backups, you just need VZDumps, but I usually click on everything since I&amp;rsquo;ll also use it for ISOs for new VMs and templates for LXCs.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.11.03-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Once you&amp;rsquo;ve added that, the storage will appear in the server view, but also as an option when you go into &lt;em&gt;Backup&lt;/em&gt; for a VM and select &lt;em&gt;Backup Now&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-04-07-at-2.15.53-pm.png" alt=""&gt;&lt;/p&gt;</description></item><item><title>rsync episode IV - a sudo hope</title><link>https://blog.iankulin.com/rsync-episode-iv-a-sudo-hope/</link><pubDate>Thu, 30 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rsync-episode-iv-a-sudo-hope/</guid><description>&lt;p&gt;With all those earlier rsync bumps out of the way, I was ready to try my first rsync backup at the command line to sync my movies directory on the NAS to a (NTFS formatted) USB drive plugged into the same NAS. This is to be one of the simplest since there&amp;rsquo;s no remote server involved, just copying from mount point directory to another - so no drama with remote permissions.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a lot of files involved, and I knew from running the dry run that there would be a lot of output. I could see a few error messages, but each of the file copies was taking a while so I was confident they, at least, were working. If you missed the last episode, here&amp;rsquo;s where I landed for this rsync command.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -avi --exclude &amp;#39;*@eaDir*&amp;#39; /volume1/media/video/Movies/ /volumeUSB1/usbshare/media/video/Movies --del
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Before I worried about the error messages, I had a look to see if the files had been copied correctly, but they had not. Even though each file was taking about the right amount of time to copy, the new files were not making it to the destination directories. Nor did they seem to be anywhere else. So I guess go back to the error messages and try to understand them. Here&amp;rsquo;s a &lt;em&gt;very&lt;/em&gt; condensed selection of the output.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync: failed to set times on &amp;#34;/volumeUSB1/usbshare/media/video/Movies/Jungle Book (1942)&amp;#34;: Operation not permitted (1)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;gt;f+++++++++ Jungle Book (1942)/Jungle Book (1942).mkv
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;gt;f+++++++++ Jungle Book (1942)/trailer.mp4
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync: mkstemp &amp;#34;/volumeUSB1/usbshare/media/video/Movies/Jungle Book (1942)/.Jungle Book (1942).mkv.Wd141R&amp;#34; failed: Operation not permitted (1)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync: mkstemp &amp;#34;/volumeUSB1/usbshare/media/video/Movies/Jungle Book (1942)/.trailer.mp4.TNu7UC&amp;#34; failed: Operation not permitted (1)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This log is from one of my later attempts. This video was new on the NAS so an earlier running of the rsync would have had some more lines about creating the directory on the USB - and I had a through the file browser in the NAS. The correct directory had been created, but there were no files in it.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s have a look at this output a bit at a time:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync: failed to set times on &amp;#34;/volumeUSB1/usbshare/media/video/Movies/Jungle Book (1942)&amp;#34;: Operation not permitted (1)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;rsync is trying to update the date/time of the destination folder to match the source one - something similar to the &lt;code&gt;touch&lt;/code&gt; command. &lt;code&gt;not permitted&lt;/code&gt; sounds like a &lt;a href="https://blog.iankulin.com/could-it-be-a-permissions-problem/"&gt;permissions issue&lt;/a&gt;. There was one of these messages for every directory and they were all near the start of the log.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;gt;f+++++++++ Jungle Book (1942)/Jungle Book (1942).mkv
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;gt;f+++++++++ Jungle Book (1942)/trailer.mp4
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These are not errors, but encouraging output. The code at the beginning says what&amp;rsquo;s going on - they are files, and are being copied from the source to the destination. These showed up for every file that was in the source but not the destination, and the times for the file copies felt about right - the .mkv file took a while, the trailer was quick. These messages were all together in the middle of the log.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync: mkstemp &amp;#34;/volumeUSB1/usbshare/media/video/Movies/Jungle Book (1942)/.Jungle Book (1942).mkv.Wd141R&amp;#34; failed: Operation not permitted (1)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync: mkstemp &amp;#34;/volumeUSB1/usbshare/media/video/Movies/Jungle Book (1942)/.trailer.mp4.TNu7UC&amp;#34; failed: Operation not permitted (1)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;[mkstemp](https://man7.org/linux/man-pages/man3/mkstemp.3.html)&lt;/code&gt; is a command for creating a temporary file. It&amp;rsquo;s common in operating systems to use a hidden temporary file when transferring a file - you save into the temp file then rename it when its successfully completed. I&amp;rsquo;m guessing that&amp;rsquo;s what&amp;rsquo;s happening here, and it&amp;rsquo;s failing for permissions reasons. What I don&amp;rsquo;t understand is why they would all be grouped together at the end of the log instead of back next to each individual copy.&lt;/p&gt;
&lt;p&gt;But anyway, this is clearly a permissions problem. I can easily check this just by trying the copy manually. I&amp;rsquo;m still logged in as the same user who executed the &lt;code&gt;rsync&lt;/code&gt; command so the &lt;code&gt;cp&lt;/code&gt; will have the same rights etc and so should also fail&amp;hellip;&lt;/p&gt;
&lt;p&gt;No - this copy worked perfectly. No error message, and when I used the NAS filebrowser, the new file had correctly copied onto the USB drive.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cp /volume1/media/video/Movies/&amp;#39;Jungle Book (1942)&amp;#39;/&amp;#39;Jungle Book (1942).mkv&amp;#39; /volumeUSB1/usbshare/media/video/Movies/&amp;#39;Jungle Book (1942)&amp;#39;/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So somehow &lt;code&gt;rsync&lt;/code&gt; is running with lower permissions than &lt;code&gt;cp&lt;/code&gt; when executed by the same user? Well, (grasping at straws now) what if I &lt;code&gt;sudo&lt;/code&gt; it? I tried that, and (1) there&amp;rsquo;s no &lt;em&gt;set times&lt;/em&gt; error, (2) all the file copies worked, and (3) no &lt;em&gt;mkstemp&lt;/em&gt; errors.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo rsync -avi --exclude &amp;#39;*@eaDir*&amp;#39; /volume1/media/video/Movies/ /volumeUSB1/usbshare/media/video/Movies --del
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I did notice one other difference, there was lots of &amp;lsquo;o&amp;rsquo; in the itemize changes output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.d..tpo.... Jungle Book (1942)/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;gt;f..tpo.... Jungle Book (1942)/Jungle Book (1942).mkv
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;gt;f+++++++++ Jungle Book (1942)/trailer.mp4
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From the &lt;a href="https://download.samba.org/pub/rsync/rsync.1#opt--itemize-changes"&gt;man page&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.d..tpo....&lt;/code&gt; - not being copied, it&amp;rsquo;s a directory, modification time is different and is being updated, permissions are different and are being updated, the owner is different and is being updated&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;gt;f..tpo....&lt;/code&gt; - is being copied to destination, it&amp;rsquo;s a file, modification time is different and is being updated, permissions are different and are being updated, the owner is different and is being updated&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;gt;f+++++++++&lt;/code&gt; - is being copied to destination, it&amp;rsquo;s a file, everything is being newly created so will be the same as the source&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s a major step forward, the files are syncing correctly, and if I rerun the rsync and haven&amp;rsquo;t made any changes to the source files, it zips through. I do notice it is still updating the permissions and owner each time. This doesn&amp;rsquo;t produce an error message, but since it thinks they need done every run it suggests it is not happening correctly.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s possible the owner/permissions issue is related to the USB drive being NTFS formatted. It&amp;rsquo;s also possible I can get rsync to stop trying to change those since they are not important in this context. the &lt;code&gt;-a&lt;/code&gt; flag (short for archive) is a shortcut that pulls in a number of other flags. Perhaps I can just pick the ones I need and eliminate the owner and permissions ones.&lt;/p&gt;
&lt;p&gt;Having to sudo to get this to work does not seem like a great solution - presumably this will come back to bite me when I try and automate it. So there is still some figuring out to do, but at least one step of my backup is down now.&lt;/p&gt;</description></item><item><title>rsync basics</title><link>https://blog.iankulin.com/rsync-basics/</link><pubDate>Sun, 26 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/rsync-basics/</guid><description>&lt;p&gt;I&amp;rsquo;ve started down the path of improved storage management, including embracing the &lt;a href="https://www.backblaze.com/blog/the-3-2-1-backup-strategy/"&gt;3-2-1&lt;/a&gt; mantra. I&amp;rsquo;ve settled on a RAID6 NAS for local, mirrored to an off-site NAS, and an offline local USB drive.&lt;/p&gt;
&lt;p&gt;While I&amp;rsquo;ve been setting those up, my services have been live, so files have been changing on my main storage, which I&amp;rsquo;ve then switched to the bigger NAS, and I&amp;rsquo;ve been trying to keep data in sync by remembering what changes have been made where, and manually replicating them. That&amp;rsquo;s not sustainable and not the plan.&lt;/p&gt;
&lt;h3 id="beyond-compare"&gt;Beyond Compare&lt;/h3&gt;
&lt;p&gt;Many years ago, on Windows, I paid for a file/text comparison app called &lt;a href="https://www.scootersoftware.com/features.php"&gt;Beyond Compare&lt;/a&gt; - it says on their website that these are perpetual licences, and I&amp;rsquo;ve had many years of value out of it. I think I bought it about the same time I dabbled in version control thanks to the excellent book Code Complete (&lt;a href="https://www.amazon.com.au/Code-Complete-Steve-McConnell/dp/0735619670"&gt;this link&lt;/a&gt; is to the second version, but I&amp;rsquo;ve had both over the years).&lt;/p&gt;
&lt;p&gt;If you have to do manual syncing jobs between different directories or machines, this is an excellent graphical tool to do that with. I have never tried any other software because it&amp;rsquo;s always just done exactly what I needed with no hassle. Highly recommend.&lt;/p&gt;
&lt;p&gt;However, doing things manually is no way to do backups - you need a setup that runs without human intervention so it actually gets done.&lt;/p&gt;
&lt;h2 id="rsync"&gt;rsync&lt;/h2&gt;
&lt;p&gt;I never fail to be amazed at the substantial but unassuming command line tools from the Linux world, and this is another one. &lt;code&gt;rsync&lt;/code&gt; is perfect for this specific job - of keeping my remote backup in sync with the production storage. Here&amp;rsquo;s a few examples of how it works.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s say I&amp;rsquo;ve got two directories in my home folder called localdir and remotedir. localdir has some files in it and I want them copied to the remotedir. &lt;code&gt;rsync&lt;/code&gt; can do that for us with this command. The &lt;code&gt;-a&lt;/code&gt; flag does a few things, including recursing into directories and preserving some of the file attributes when it copies files. I found if I didn&amp;rsquo;t do this, and just used -&lt;code&gt;r&lt;/code&gt; for the recursion, rsync&amp;rsquo;s system for checking for changes didn&amp;rsquo;t work.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -a localdir/ remotedir
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-1.13.44-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Okay, that&amp;rsquo;s not impressive, I could have done the same thing with &lt;code&gt;cp&lt;/code&gt; to copy those files. And actually I could do that for my remote backup as well, except there&amp;rsquo;s no point burning up resources for copying identical files on top of each other. Really we just want to copy the files that have changed, or new files that have been added.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s try that by editing &lt;code&gt;file1.txt&lt;/code&gt;, and adding a new &lt;code&gt;file0.txt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-1.18.57-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;You might be thinking that perhaps rsync really is just copying all the files again. We can add a couple more flags to get rsync to tell us what is getting copied (&lt;code&gt;-v&lt;/code&gt; for verbose, and &lt;code&gt;-i&lt;/code&gt; which gives some output explaining how it decided a file needed copied.&lt;/p&gt;
&lt;p&gt;Without making any changes, let&amp;rsquo;s re-run the rsync with those flags. We shouldn&amp;rsquo;t see any files updated.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-1.24.51-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So no files transferred, but if we edit a one and change the timestamp on another one, that should trigger a couple of files to be copied.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-1.27.48-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The optimisation in rsync extends further than just copying the files that have changed, the &lt;a href="https://download.samba.org/pub/rsync/rsync.1"&gt;man page&lt;/a&gt; says &amp;ldquo;&lt;em&gt;It is famous for its delta-transfer algorithm, which reduces the amount of data sent over the network by sending only the differences between the source files and the existing files in the destination.&lt;/em&gt;&amp;rdquo;&lt;/p&gt;
&lt;h3 id="deletions"&gt;Deletions&lt;/h3&gt;
&lt;p&gt;So that covers copying most of the changes in the source, but what about if a local file is deleted, does that propagate to the destination and delete the file there? Naturally, there is also an option for this; simply add &lt;code&gt;--del&lt;/code&gt; to the end of the command:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-3.35.49-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="remote-hosts"&gt;Remote hosts&lt;/h3&gt;
&lt;p&gt;So far, all of these examples have been between directories on a single instance. What about on a remote machine? There&amp;rsquo;s a couple of steps.&lt;/p&gt;
&lt;p&gt;First, the machine where the rsync is being executed must have ssh access to the other machine. This is just the usual ssh setup - &lt;code&gt;ssh-keygen&lt;/code&gt; some keys if you don&amp;rsquo;t have any, then copy them over with &lt;code&gt;ssh-copy-id&lt;/code&gt; and we&amp;rsquo;re ready to go&lt;/p&gt;
&lt;p&gt;The second part is to add an ssh like address to the remote directory. So instead of just&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -avi localdir/ remotedir --del
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;it will be&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rsync -avi localdir/ ian@192.168.100.33:remotedir --del
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Before I&amp;rsquo;ve run this, I&amp;rsquo;ve sshed in and created the directory &lt;code&gt;remotedir&lt;/code&gt; on the target machine, but then it&amp;rsquo;s simple as&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-25-at-5.06.01-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="reading"&gt;Reading&lt;/h3&gt;
&lt;p&gt;To figure all this out, I&amp;rsquo;ve leaned heavily on this tutorial &amp;ldquo;&lt;a href="https://www.digitalocean.com/community/tutorials/how-to-use-rsync-to-sync-local-and-remote-directories"&gt;How To Use Rsync to Sync Local and Remote Directories&lt;/a&gt;&amp;rdquo; from Digital Ocean, and for the stuff about deleting remote files when they are deleted locally, on &lt;a href="https://askubuntu.com/questions/476041/how-do-i-make-rsync-delete-files-that-have-been-deleted-from-the-source-folder"&gt;this Stack Exchange&lt;/a&gt; question.&lt;/p&gt;
&lt;p&gt;The source of truth, is the surprisingly readable &lt;a href="https://download.samba.org/pub/rsync/rsync.1"&gt;man page&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Recursive list of files in Linux</title><link>https://blog.iankulin.com/recursive-list-of-files-in-linux/</link><pubDate>Wed, 08 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/recursive-list-of-files-in-linux/</guid><description>&lt;p&gt;I&amp;rsquo;ve spent a few hours over the weekend migrating a media library from an external USB drive to the NAS, and in the process reorganised it, and in many cases bulk changed file names. I&amp;rsquo;ve also added a heap of metadata.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d like to check that I haven&amp;rsquo;t missed any files, but a side by side listing of each data source won&amp;rsquo;t do the trick, so I&amp;rsquo;ll probably end up pulling the data into a spreadsheet, but I&amp;rsquo;d like to get as close as possible with Linux-fu first.&lt;/p&gt;
&lt;p&gt;Before I go over my trial and error, and eventual solution, here&amp;rsquo;s how I&amp;rsquo;ve set up my test data for the examples. I thought I&amp;rsquo;d better start with something simple and small for testing commands.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-5.02.17-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-5.02.17-pm.png" width="495" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is actually the output of the &lt;code&gt;tree&lt;/code&gt; command on a &lt;code&gt;test&lt;/code&gt; directory I&amp;rsquo;ve created in my home directory. (I had to install it - &lt;code&gt;sudo apt install tree&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;What I need to end up with is something that recursively lists all the files, with one file per line, and it needs to include the directory tree to reach it. I should be able to pipe it through something to ignore lines that are just directories (and any other fluff).&lt;/p&gt;
&lt;h3 id="ls"&gt;ls&lt;/h3&gt;
&lt;p&gt;My go to for listing files is &lt;code&gt;ls -all&lt;/code&gt;, perhaps than can help us? It lists one line per file (along with permissions etc), so if we add &lt;code&gt;-R&lt;/code&gt; for recursive, that could be it. Here&amp;rsquo;s the output for &lt;code&gt;ls -all -R test&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;total 16
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;drwxr-xr-x 4 ian ian 4096 Mar 6 16:36 .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;drwxr-xr-x 4 ian ian 4096 Mar 6 16:36 ..
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;drwxr-xr-x 2 ian ian 4096 Mar 6 17:01 dir1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;drwxr-xr-x 2 ian ian 4096 Mar 6 17:01 dir2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir1:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;total 8
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;drwxr-xr-x 2 ian ian 4096 Mar 6 17:01 .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;drwxr-xr-x 4 ian ian 4096 Mar 6 16:36 ..
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-rw-r--r-- 1 ian ian 0 Mar 6 17:01 ignore.me
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-rw-r--r-- 1 ian ian 0 Mar 6 17:00 media1.ex1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-rw-r--r-- 1 ian ian 0 Mar 6 17:00 media1.ex2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-rw-r--r-- 1 ian ian 0 Mar 6 17:01 media3.ex1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-rw-r--r-- 1 ian ian 0 Mar 6 16:36 somefile
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-rw-r--r-- 1 ian ian 0 Mar 6 16:36 somefile2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir2:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;total 8
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;drwxr-xr-x 2 ian ian 4096 Mar 6 17:01 .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;drwxr-xr-x 4 ian ian 4096 Mar 6 16:36 ..
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-rw-r--r-- 1 ian ian 0 Mar 6 17:01 ignore.me
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-rw-r--r-- 1 ian ian 0 Mar 6 17:01 media4.ex1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-rw-r--r-- 1 ian ian 0 Mar 6 17:01 media5.ex1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-rw-r--r-- 1 ian ian 0 Mar 6 17:01 media6.ex2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-rw-r--r-- 1 ian ian 0 Mar 6 16:37 somefile
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-rw-r--r-- 1 ian ian 0 Mar 6 16:37 somefile3
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So we get one line per file, but the directory is on it&amp;rsquo;s own at the beginning of each directory listing.&lt;/p&gt;
&lt;h3 id="find"&gt;find&lt;/h3&gt;
&lt;p&gt;Based on &lt;a href="https://www.cyberciti.biz/faq/how-to-show-recursive-directory-listing-on-linux-or-unix/"&gt;this post&lt;/a&gt;, there is a command, &lt;code&gt;find&lt;/code&gt;, that might do what we want. The simple version would be &lt;code&gt;find test&lt;/code&gt; (remember test is the directory name).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian@vm102-jellyfin:~$ find test
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir2/ignore.me
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir2/media4.ex1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir2/somefile
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir2/media5.ex1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir2/somefile3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir2/media6.ex2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir1/media3.ex1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir1/ignore.me
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir1/somefile
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir1/media1.ex1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir1/media1.ex2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;test/dir1/somefile2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Well that is real close, but there&amp;rsquo;s no way to discern between a file and directory. In that same post, it;s suggested to use the &lt;code&gt;-ls&lt;/code&gt; option to see some more detail. Let&amp;rsquo;s try find &lt;code&gt;test -ls&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-5.20.59-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;This looks pretty close. If there was someway of using that &amp;rsquo;d&amp;rsquo; in the first position of the permissions output to eliminate those lines, we&amp;rsquo;d be well on our way. I have a feeling this is a &lt;code&gt;grep&lt;/code&gt; question. I have some basic grep, so for example I know I could pull all of those directories with &lt;code&gt;find test -ls | grep ' d'&lt;/code&gt;, or even invert it with the &lt;code&gt;-v&lt;/code&gt; flag to get just the files (which is out eventual goal).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-5.36.37-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;However, this is pretty hacky. A space followed by a lowercase could easily occur in a filename. What I really need to do is look at just that column which I think is character number 18. Off to &lt;a href="https://unix.stackexchange.com/questions/32170/find-all-lines-in-a-file-with-a-certain-character-at-a-certain-position"&gt;Stack Exchange&lt;/a&gt; I guess&amp;hellip;&lt;/p&gt;
&lt;h3 id="grep-with-regex"&gt;grep with regex&lt;/h3&gt;
&lt;p&gt;Okay, it turns out we can use regex with grep. I&amp;rsquo;m no expert in that either, but in regex the caret ^ represents the start of the line, a fullstop represents any character, and we can repeat that however many times we want by following it with a number in (escaped) curly braces. Something like &lt;code&gt;'^.{17}d'&lt;/code&gt; should do it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-5.44.46-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Okay! We&amp;rsquo;re getting close. I also want to ignore all the metadata and just see the media files. This can be determined by the extensions - probably .avi .mp4 .mkv .mv4. With this test data, we&amp;rsquo;ll pretend it&amp;rsquo;s .ex1 and .ex2&lt;/p&gt;
&lt;h3 id="combining-grep-tests-with-logical-or"&gt;Combining grep tests with logical or&lt;/h3&gt;
&lt;p&gt;I guess I could build some sort of super regex combined with the first one, but I&amp;rsquo;m only dealing with thousands of files, not millions so the extra overhead of piping through another grep is not going to be a drama, and I can simplify my work. In the same way that the caret ^ marks the start of a line, the dollar $ marks the end of it. So to just get the .ex1 files something like &lt;code&gt;'\.ex1$'&lt;/code&gt; should do it. The backslash at the start is to escape the period, because here we want that to mean a literal full stop, and not a wildcard for any character.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-5.55.25-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Nice, but remember I&amp;rsquo;ve got a big list of extensions, so I need to logical or a few together. This is done by putting the expressions with a pipe between them. I had a couple of goes at this with no luck and that familiar feeling of being out of my depth with regex. However, there&amp;rsquo;s a grep way out of this, because the grep flag -e allows us to OR matching expressions.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-6.06.52-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re definitely getting somewhere. You might think at this point that the chance of a directory name ending in .mp4 or one of the other media extensions is no low we could ignore it, and you&amp;rsquo;d probably be right. But as a matter of programmer pride, I never like to leave a future problem, so I&amp;rsquo;ll be keeping the directory rejecting grep. So now my command looks like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;find test -ls | grep -v &amp;#39;^.{17}d&amp;#39; | grep -e &amp;#39;\.ex1$&amp;#39; -e &amp;#39;\.ex2$&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Any experienced regex people would be pointing out the match for .ex1 .ex2 can easily be merged into a simple expression, but remember when I do this for real I&amp;rsquo;ve got a list of more complex extensions to test for.&lt;/p&gt;
&lt;h3 id="cut"&gt;cut&lt;/h3&gt;
&lt;p&gt;All that text at the beginning of these lines is not needed. Surely I can trim that off somehow? Yep - there&amp;rsquo;s a command &lt;code&gt;cut&lt;/code&gt; that does exactly that. The -b flag specifies which byte to extract, and this can also be a range. putting a dash after the position number says to output all of the bytes after that position. So if we applied &lt;code&gt;cut -b 5-&lt;/code&gt; to the string &lt;code&gt;123456789&lt;/code&gt;, the output would be &lt;code&gt;56789&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-6.26.18-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Bingo. Just one more problem. In my data, I have a heap of files with a valid extension but I want to exclude them based on their file name. Every directory with a movie has a trailer named &lt;code&gt;trailer.mp4&lt;/code&gt;, so I need to eliminate them. To simulate this, lets add in another extension with our test data.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-6.29.48-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;So I want to take out the lines that include &amp;lsquo;/ignore.me&amp;rsquo;. I should be able to do this with another &lt;code&gt;grep -v&lt;/code&gt; regex on a line end. Something like &lt;code&gt;grep -v 'ignore.me$'&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-03-06-at-6.33.22-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;And we&amp;rsquo;re done! I&amp;rsquo;ll just direct this into a file, run it on both disks and pull them into Excel to separate the file names and directories, and sort them to compare.&lt;/p&gt;</description></item><item><title>Could it be a permissions problem?</title><link>https://blog.iankulin.com/could-it-be-a-permissions-problem/</link><pubDate>Sun, 05 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/could-it-be-a-permissions-problem/</guid><description>&lt;p&gt;Unix, and therefore Linux, was built from the ground up as a multi-user system. Thanks to this, great security is baked in, for example every file has permission attributes for it&amp;rsquo;s owner, the group the owner is a member of, and then everyone. For example, it might be a good idea if I can read, write and execute my own files, but the other members of my group can just read them, and any other user on the system has none of those rights.&lt;/p&gt;
&lt;p&gt;I &lt;a href="https://blog.iankulin.com/folder-ownership-problems-with-jellyfin/"&gt;talked a bit about this&lt;/a&gt; when I was solving the first round of problems with getting Jellyfin working. I actually solved all of those problems - they were permissions related. Once I&amp;rsquo;d figured out the group id of the jellyfin user and applied that when mounting the NAS I had a week of blissful media consumption on the TV (via Google TV Chromecast), on my laptop, and phone. The eventual plan for this little box is to move it offsite though, so I needed TailScale, which has worked perfectly and effortlessly everywhere else I&amp;rsquo;d tried it, but it turns out it is not happy living in an LXC container on Proxmox which is where my Jellyfin instance was running.&lt;/p&gt;
&lt;p&gt;Googling around, it sounds like it is possible, but more work than I was prepared to invest, and didn&amp;rsquo;t actually needed to. The little i3 it is going to live on has plenty of headroom to run a full VM so I decided to do that.&lt;/p&gt;
&lt;p&gt;I started from scratch with a Debian VM and had it working perfectly, but then an hour or so after I&amp;rsquo;d downloaded all the metadata for my content, some, but not all of the posters would disappear. Once again, the problem turned out to be permissions (Jellyfin wanted write access to the media locations even though I&amp;rsquo;d told it not to save metadata there). I solved that with the &lt;a href="https://pimylifeup.com/chmod-777/"&gt;cursed 777&lt;/a&gt; then did something terrible to the jellyfin process so now it would not start again.&lt;/p&gt;
&lt;p&gt;Sadly, I had not saved a snapshop before I started messing around with things I only half understand.&lt;/p&gt;
&lt;p&gt;In the process of looking for a solution to the jellyfin process retrying starting five times then dying after I&amp;rsquo;d tried to restart it for a reason I can&amp;rsquo;t even recall, I stumbled on a StackExchange that was my exact problem. In the thread of answers, one of them was just &amp;ldquo;Use the container version&amp;rdquo;. I took slight offence at this, as did OP, but when the commenter pointed out that these fiddly installation problems that lead you all over the place and are painful because your configuration is different to everyone else&amp;rsquo;s, and the problem could lie there - is the exact problem containers are intended to solve.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve taken that advice on board and installed the official container version. First problem I&amp;rsquo;ve run into - Jellyfin can&amp;rsquo;t see my media folder - permissions.&lt;/p&gt;
&lt;p&gt;ANY problem you have running something on Linux, you should always start thinking about it in terms of permissions. Who is the user, what are they acessing?&lt;/p&gt;</description></item><item><title>Problems mounting network share at boot</title><link>https://blog.iankulin.com/problems-mounting-network-share-at-boot/</link><pubDate>Wed, 01 Mar 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/problems-mounting-network-share-at-boot/</guid><description>&lt;p&gt;I had Jellyfin working nicely in an LXC container in Proxmox, but could not get Tailscale working in the container. Since this is going to be an important part of accessing my media away from home, I decided it was probably worth the extra bulk to run JellyFin in a VM.&lt;/p&gt;
&lt;p&gt;Following &lt;a href="https://blog.iankulin.com/accessing-a-synology-nas-from-linux/"&gt;my own instructions&lt;/a&gt;, I had the mount command in the /etc/fstab file so it would persist across reboots. It looked a bit like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;//192.168.100.25/media /mnt/media cifs username=jelly,password=jellypass,uid=1000,gid=115,file_mode=0660,dir_mode=07
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The problem I had was that this was not mounting the NAS at boot time, but if I reran it with &lt;code&gt;mount -a&lt;/code&gt; it worked perfectly. A likely cause for this problem is that the network interface is not properly up at the time the mount is being tried.&lt;/p&gt;
&lt;p&gt;I found a few different suggestions for this, but the one that worked for me and I liked the most is from the first answer in &lt;a href="https://askubuntu.com/questions/43363/how-to-auto-mount-using-sshfs/1242516#1242516"&gt;this StackExchange question&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;//192.168.100.25/media /mnt/media cifs username=jelly,password=jellypass,noauto,x-systemd.automount,_netdev,uid=1000,gid=115,file_mode=0660,dir_mode=07
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From that answer, the explanation for these is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;noauto&lt;/code&gt; will stop the no-brainer actions like forcibly mounting whatsoever at booting regardless if the network is up or not.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x-systemd.automount&lt;/code&gt; is the smart daemon that knows when to mount.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;_netdev&lt;/code&gt; tag will also identify that it uses network devices, thus it will wait until the network is up.​​​​​​&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I had tried &lt;code&gt;_netdev&lt;/code&gt; by itself, but that wasn&amp;rsquo;t enough magic. The three together was.&lt;/p&gt;</description></item><item><title>Sudoers' file not working</title><link>https://blog.iankulin.com/sudoers-file-not-working/</link><pubDate>Mon, 27 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/sudoers-file-not-working/</guid><description>&lt;p&gt;A couple of weeks ago, I posted &lt;a href="https://blog.iankulin.com/sudo-incident-reports-where-do-they-go/"&gt;about the sudoers&amp;rsquo; file&lt;/a&gt;, and how there was a special tool for editing it since breaking it is a bad idea, and that in fact I needn&amp;rsquo;t bother, since I can just add my user to the sudoers&amp;rsquo; group with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;usermod -a -G sudo ian
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That worked (on Unbuntu) since &lt;code&gt;/etc/sudoers&lt;/code&gt; contained a line saying:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Allow members of group sudo to execute any command
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;%sudo	ALL=(ALL:ALL) ALL
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I tried the same trick on a fresh Debian install today, and no dice:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-26-at-3.32.49-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I assumed this might mean that the &lt;code&gt;sudoers&lt;/code&gt; file is different on Debian than Ubuntu, but no, that same line granting permission to the &lt;code&gt;sudo&lt;/code&gt; group is there. My next guess is that I hadn&amp;rsquo;t correctly added ian to that group. But no, that looks okay.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-26-at-3.48.51-pm.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/offon.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Yup. Log out, log in&amp;hellip;&lt;/p&gt;</description></item><item><title>Folder ownership problems with Jellyfin</title><link>https://blog.iankulin.com/folder-ownership-problems-with-jellyfin/</link><pubDate>Wed, 22 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/folder-ownership-problems-with-jellyfin/</guid><description>&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-5.32.36-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-5.32.36-pm.png" width="800" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;After being so blase about the file permissions when mounting the share to the Linux file system, and testing that root could read and write to the share, I ran into problems immediately when trying to add the media folder as a library in Jellyfin - getting the error &amp;ldquo;The path could not be found. Please ensure the path is valid and try again.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I definitely had the path correct - I could copy it from the dialog and cd to it at the CLI. So I suspected it was a permissions thing. The app might not have read permissions for the directory.&lt;/p&gt;
&lt;p&gt;If, as root, I ls -l (-l for long) any of the directories in this path, they look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@jellyfin:/mnt/media/video# ls -l
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;total 0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;drwxrwx--- 2 1000 1000 0 Feb 18 09:13 Movies
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;drwxrwx--- 2 1000 1000 0 Feb 18 04:30 Shows
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Those letters at the start of each listing &lt;a href="https://detailed.wordpress.com/2017/10/28/understanding-ls-command-output/"&gt;have a meaning&lt;/a&gt;. The first &lt;code&gt;d&lt;/code&gt; just means it&amp;rsquo;s a directory. Then there&amp;rsquo;s three groups of three letters:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;d rwx rwx --- (I&amp;#39;ve just added those spaces to make things clear)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These three lots of letters are the &lt;em&gt;permissions&lt;/em&gt; in the order of &lt;em&gt;owner&lt;/em&gt;, &lt;em&gt;group&lt;/em&gt; &amp;amp; &lt;em&gt;everybody&lt;/em&gt;. So for these directories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The owner can read, write &amp;amp; execute (&lt;code&gt;rwx&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Members of this group can read, write &amp;amp; execute (&lt;code&gt;rwx&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Everybody else can&amp;rsquo;t read, can&amp;rsquo;t write, and can&amp;rsquo;t execute (&lt;code&gt;---&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This raises the question, who is the owner of this directory, and what is the group we are talking about? The answer to those questions are the next items in the listing.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://detailed.wordpress.com/2017/10/28/understanding-ls-command-output/"&gt;&lt;img src="https://blog.iankulin.com/images/ls-command3.jpg" width="686" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In our case the owner is &lt;code&gt;1000&lt;/code&gt; and the group is &lt;code&gt;1000&lt;/code&gt;. Where did these come from? Well, they were in the mount command I used in &lt;code&gt;etc/fstab&lt;/code&gt; yesterday:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;//192.168.100.25/media /mnt/media cifs username=jelly,password=jellypass,uid=1000,gid=1000,file_mode=0660,dir_mode=07
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So most likely that&amp;rsquo;s the source of my troubles. As I mentioned, I tested this from the command line logged in as root, and it worked fine. And I was imagining since I&amp;rsquo;d installed Jellyfin as root that Jellyfin would have all those rights, but perhaps (as would be wise) Jellyfin is running as a different user, and I need to add that user to the 1000 group in order to make this work.&lt;/p&gt;
&lt;p&gt;How to I find out what user Jellyfin is running as? A good place to start is to look at the running processes with the &lt;code&gt;ps&lt;/code&gt; command:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-6.02.04-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-6.02.04-pm.png" width="998" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Well, lookee here. User &lt;code&gt;jellyfin&lt;/code&gt; is running this process. We can see what groups she&amp;rsquo;s a member of by running the &lt;code&gt;groups&lt;/code&gt; command.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@jellyfin:/# groups jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jellyfin : jellyfin
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So, not a member of the &lt;code&gt;1000&lt;/code&gt; group then. We can use the &lt;code&gt;getent&lt;/code&gt; command to see the group numbers for users:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@jellyfin:/# getent group jellyfin 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jellyfin:x:115:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@jellyfin:/# getent group root 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root:x:0:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@jellyfin:/# 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Okay, so that&amp;rsquo;s likely out problem. To confirm it, we could change the group for the directory tree and files, or add &lt;code&gt;jellyfin&lt;/code&gt; to the &lt;code&gt;1000&lt;/code&gt; group. Since I now know that &lt;code&gt;jellyfin&lt;/code&gt; is a member of the &lt;code&gt;115&lt;/code&gt; group, and that I just plucked &lt;code&gt;1000&lt;/code&gt; out of the air, I&amp;rsquo;m inclined to remount the share with a &lt;code&gt;gid=115&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-6.24.11-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-02-18-at-6.24.11-pm.png" width="757" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And&amp;hellip;. it works.&lt;/p&gt;</description></item><item><title>External USB Drives in Linux</title><link>https://blog.iankulin.com/external-usb-drives-in-linux/</link><pubDate>Sat, 18 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/external-usb-drives-in-linux/</guid><description>&lt;p&gt;Many modern Linux distros will auto-mount USB drives - they just pop up in the graphical file manager as users would expect. When you&amp;rsquo;re running server, older, or smaller versions, that&amp;rsquo;s probably not going to be the case, and you&amp;rsquo;ll have to do it old school.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s look at some basics. &lt;code&gt;[lsblk](https://man7.org/linux/man-pages/man8/lsblk.8.html)&lt;/code&gt; will list the &amp;lsquo;block&amp;rsquo; devices. Your output will almost certainly be a bit different than this.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve:~# lsblk
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sda 8:0 0 119.2G 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├─sda1 8:1 0 1007K 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├─sda2 8:2 0 512M 0 part /boot/efi
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└─sda3 8:3 0 118.7G 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-swap 253:0 0 7.7G 0 lvm [SWAP]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-root 253:1 0 39.8G 0 lvm /
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-data_tmeta 253:2 0 1G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ └─pve-data-tpool 253:4 0 54.6G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ ├─pve-data 253:5 0 54.6G 1 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ ├─pve-vm--100--disk--0 253:6 0 10G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ ├─pve-vm--101--disk--0 253:7 0 10G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ ├─pve-vm--300--disk--0 253:8 0 8G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ ├─pve-vm--102--disk--0 253:9 0 4M 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ └─pve-vm--102--disk--1 253:10 0 32G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └─pve-data_tdata 253:3 0 54.6G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └─pve-data-tpool 253:4 0 54.6G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-data 253:5 0 54.6G 1 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-vm--100--disk--0 253:6 0 10G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-vm--101--disk--0 253:7 0 10G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-vm--300--disk--0 253:8 0 8G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-vm--102--disk--0 253:9 0 4M 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └─pve-vm--102--disk--1 253:10 0 32G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you look at the &lt;code&gt;type&lt;/code&gt; column, you can see this machine has one &lt;em&gt;disk&lt;/em&gt;, with three &lt;em&gt;partitions&lt;/em&gt;, and the last partition has a heap of &lt;em&gt;logical volumes&lt;/em&gt;. Let&amp;rsquo;s plug the thumb drive in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve:~# lsblk
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sda 8:0 0 119.2G 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├─sda1 8:1 0 1007K 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├─sda2 8:2 0 512M 0 part /boot/efi
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└─sda3 8:3 0 118.7G 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-swap 253:0 0 7.7G 0 lvm [SWAP]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-root 253:1 0 39.8G 0 lvm /
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-data_tmeta 253:2 0 1G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ └─pve-data-tpool 253:4 0 54.6G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ ├─pve-data 253:5 0 54.6G 1 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ ├─pve-vm--100--disk--0 253:6 0 10G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ ├─pve-vm--101--disk--0 253:7 0 10G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ ├─pve-vm--300--disk--0 253:8 0 8G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ ├─pve-vm--102--disk--0 253:9 0 4M 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; │ └─pve-vm--102--disk--1 253:10 0 32G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └─pve-data_tdata 253:3 0 54.6G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └─pve-data-tpool 253:4 0 54.6G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-data 253:5 0 54.6G 1 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-vm--100--disk--0 253:6 0 10G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-vm--101--disk--0 253:7 0 10G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-vm--300--disk--0 253:8 0 8G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ├─pve-vm--102--disk--0 253:9 0 4M 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; └─pve-vm--102--disk--1 253:10 0 32G 0 lvm 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sdb 8:16 1 14.5G 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└─sdb1 8:17 1 14.5G 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There we are, down the bottom. Our disk is &lt;code&gt;sdb&lt;/code&gt;, and partition is &lt;code&gt;sdb1&lt;/code&gt;. So the OS knows it exists - it&amp;rsquo;s recognised, but to use it we need to &lt;code&gt;mount&lt;/code&gt; it to the file system somewhere. Mounting it will let us see and interact with the files on the drive.&lt;/p&gt;
&lt;p&gt;By convention, removable media is often mounted in &lt;code&gt;/media&lt;/code&gt; or &lt;code&gt;/mnt&lt;/code&gt;, but it can be wherever you like. Let&amp;rsquo;s make a directory for it in &lt;code&gt;/media&lt;/code&gt; and mount it there.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve:/# mkdir /media/external
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve:/# mount /dev/sdb1 /media/external
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve:/# ls /media/external
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#39;02 Advance Australia Fair 1 verse vocal.mp3&amp;#39; &amp;#39;Year 3 Pack.pdf&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#39;Araw ng Kasarinl&amp;#39;$&amp;#39;\341&amp;#39;&amp;#39;n.mp4&amp;#39;	 &amp;#39;Year 4 Pack.pdf&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#39;System Volume Information&amp;#39;		 &amp;#39;Year 5 Pack.pdf&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#39;Year 1 Pack.pdf&amp;#39;			 &amp;#39;Year 6 Pack.pdf&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#39;Year 2 Pack.pdf&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve:/# 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Success!&lt;/p&gt;
&lt;p&gt;If we do the lsblk again, you&amp;rsquo;ll see out mount point in the listing&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve:/# lsblk
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sda 8:0 0 119.2G 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├─sda1 8:1 0 1007K 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;├─sda2 8:2 0 512M 0 part /boot/efi
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└─sda3 8:3 0 118.7G 0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sdb 8:16 1 14.5G 0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;└─sdb1 8:17 1 14.5G 0 part /media/external
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Of course, just as in Windows, we need to tell the OS when we want to remove a removable drive to ensure that any caches are flushed and that we don&amp;rsquo;t inadvertently lose data when we yank it out. This is the &lt;em&gt;unmounting&lt;/em&gt; process.&lt;/p&gt;
&lt;p&gt;We can unmount a drive with the &lt;code&gt;[umount](https://man7.org/linux/man-pages/man8/umount.8.html)&lt;/code&gt; command.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve:/# ls /media/external
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#39;02 Advance Australia Fair 1 verse vocal.mp3&amp;#39; &amp;#39;Year 3 Pack.pdf&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#39;Araw ng Kasarinl&amp;#39;$&amp;#39;\341&amp;#39;&amp;#39;n.mp4&amp;#39;	 &amp;#39;Year 4 Pack.pdf&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#39;System Volume Information&amp;#39;		 &amp;#39;Year 5 Pack.pdf&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#39;Year 1 Pack.pdf&amp;#39;			 &amp;#39;Year 6 Pack.pdf&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#39;Year 2 Pack.pdf&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve:/# umount /media/external
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve:/# ls /media/external
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@pve:/# 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>SSH &amp; the scary warning</title><link>https://blog.iankulin.com/ssh-the-scary-warning/</link><pubDate>Wed, 08 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/ssh-the-scary-warning/</guid><description>&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-8.41.11-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The first time you connect to a new server with ssh, it asks you something like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ ~ &amp;gt; ssh ian@192.168.100.20 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;The authenticity of host &amp;#39;192.168.100.20 (192.168.100.20)&amp;#39; can&amp;#39;t be established.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ED25519 key fingerprint is SHA256:ZcNTcOjO/0fOLC5iNChf8Q8MHN7z2d+VV0qz7XqH1g4.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;This key is not known by any other names
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Warning: Permanently added &amp;#39;192.168.100.20&amp;#39; (ED25519) to the list of known hosts.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once you&amp;rsquo;ve said yes, it adds the server &amp;lsquo;fingerprint&amp;rsquo; to the known hosts file, then next time you ssh there, it feels safe - we know this server.&lt;/p&gt;
&lt;p&gt;But&amp;hellip;. if you&amp;rsquo;re playing around with virtual machines. Loading them, booting them, rebuilding them, cloning them etc. You might try and connect to a VM which is a different one from before, but which has the same ip address. SSH will not be happy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ ~ &amp;gt; ssh ian@192.168.100.20
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Someone could be eavesdropping on you right now (man-in-the-middle attack)!
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;It is also possible that a host key has just been changed.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;The fingerprint for the ED25519 key sent by the remote host is
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;SHA256:ZcNTcOjO/0fOLC5iNChf8Q8MHN7z2d+VV0qz7XqH1g4.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Please contact your system administrator.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Add correct host key in /Users/user/.ssh/known_hosts to get rid of this message.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Offending ECDSA key in /Users/user/.ssh/known_hosts:9
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Host key for 192.168.100.20 has changed and you have requested strict checking.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Host key verification failed.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It is right to be suspicious. From its point of view, it goes to Joe&amp;rsquo;s house each day, and it&amp;rsquo;s always Joe who answers the door. Today, it&amp;rsquo;s someone completely different but who says they are Joe. But since we know this is a different server, this is an expected result, so I&amp;rsquo;d like to ignore it.&lt;/p&gt;
&lt;p&gt;Although the message says to add the new fingerprint to the known_hosts file, it&amp;rsquo;s easier just to delete the old ones. Then when I try to connect to this server again, it will think it&amp;rsquo;s a new one and ask me to accept it. To delete this ip address (or hostname) out of the known hosts file, I just need to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;➜ ~ &amp;gt; ssh-keygen -R 192.168.100.20
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Host 192.168.100.20 found: line 7
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Host 192.168.100.20 found: line 8
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Host 192.168.100.20 found: line 9
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/Users/user/.ssh/known_hosts updated.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And we&amp;rsquo;re good to go again. But thank you ssh for being so careful.&lt;/p&gt;</description></item><item><title>Chinese Hackers Want to steal my Hello World container</title><link>https://blog.iankulin.com/chinese-hackers-want-to-steal-my-hello-world-container/</link><pubDate>Mon, 06 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/chinese-hackers-want-to-steal-my-hello-world-container/</guid><description>&lt;p&gt;A smart thing to do after setting up a server on the internet, is to set up SSH keys and then turn passwords off for SSH. The reason for this is that scanning for open port 22 on IP addresses, then brute forcing password files on them is pretty much hacker 101. So if you have passwords turned on, and especially if you have a weak password you are really inviting someone to take over your server as root and add it to their botnet army for liking Putin&amp;rsquo;s twitter posts or whatever.&lt;/p&gt;
&lt;p&gt;When I was writing &lt;a href="https://blog.iankulin.com/sudo-incident-reports-where-do-they-go/"&gt;the post about looking for the sudo attempt&lt;/a&gt; &amp;lsquo;report&amp;rsquo;, you might have noticed some sshd timeouts:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-12.08.21-pm.jpg" alt=""&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s what&amp;rsquo;s going on there. SSH has a timeout value of about a minute. I&amp;rsquo;d also guess those kex_exchange_identification messages are suspicious as well. I thought I&amp;rsquo;d google one of the IP addreses:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-12.18.14-pm.png"&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-12.18.14-pm.png" width="895" alt=""&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Oh, so it&amp;rsquo;s China, and multiple people are reporting SSH brute force attacks:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-12.20.35-pm.jpg" alt=""&gt;&lt;/p&gt;</description></item><item><title>sudo Incident Reports - where do they go?</title><link>https://blog.iankulin.com/sudo-incident-reports-where-do-they-go/</link><pubDate>Sat, 04 Feb 2023 00:00:00 +0000</pubDate><guid>https://blog.iankulin.com/sudo-incident-reports-where-do-they-go/</guid><description>&lt;p&gt;Even though it&amp;rsquo;s &lt;em&gt;my&lt;/em&gt; server, I still have a pang of guilt when this happens.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.iankulin.com/images/screen-shot-2023-01-28-at-10.40.43-am-copy.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;I always imagine &lt;a href="https://en.wikipedia.org/wiki/Richard_Stallman"&gt;Richard Stallman&lt;/a&gt; (or someone with a similar 2000&amp;rsquo;s database administrator beard) looking at me disappointedly and shaking his head slowly.&lt;/p&gt;
&lt;p&gt;It does raise the question though - since it&amp;rsquo;s my server, shouldn&amp;rsquo;t I be getting a text message from CERN or something?&lt;/p&gt;
&lt;h4 id="where-is-this-report"&gt;Where is this report?&lt;/h4&gt;
&lt;p&gt;(&lt;a href="https://xkcd.com/838/"&gt;Relevant xkcd&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Like everything, the answer is &amp;lsquo;it&amp;rsquo;s logged&amp;rsquo;. We can use the &lt;code&gt;journalctl&lt;/code&gt; command to look at the logs, on this server that&amp;rsquo;s been running less than 20 hours, there&amp;rsquo;s already several thousand lines to look through if you just enter &lt;code&gt;journalctl&lt;/code&gt;, so I&amp;rsquo;m going to just send all the high priority logs to a file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;journalctl -p 3 &amp;gt; errors.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then since this just happened, it should be at the end of the file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tail errors.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan 28 12:10:40 enrico-rider sshd[5168]: fatal: Timeout before authentication for 110.41.153.190 port 41826
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan 28 12:11:01 enrico-rider sshd[5170]: fatal: Timeout before authentication for 110.41.153.190 port 41856
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan 28 12:23:15 enrico-rider sshd[5222]: fatal: Timeout before authentication for 61.177.173.39 port 29421
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan 28 12:23:26 enrico-rider sshd[5223]: fatal: Timeout before authentication for 61.177.173.39 port 49692
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan 28 12:23:37 enrico-rider sshd[5226]: fatal: Timeout before authentication for 61.177.173.39 port 10416
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan 28 12:39:51 enrico-rider sshd[5517]: fatal: Timeout before authentication for 61.177.172.108 port 53867
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan 28 12:50:06 enrico-rider sshd[5653]: error: kex_exchange_identification: Connection closed by remote host
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan 28 13:03:53 enrico-rider sshd[5696]: error: kex_exchange_identification: Connection closed by remote host
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan 28 13:24:58 enrico-rider sshd[5804]: fatal: Timeout before authentication for 61.177.173.39 port 46041
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan 28 13:40:06 enrico-rider sudo[6077]: ian : user NOT in sudoers ; TTY=pts/0 ; PWD=/home/ian ; USER=root ; COMMAND=/usr/bin/docker ps
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There we go, it really has been reported!&lt;/p&gt;
&lt;h4 id="how-to-add-a-user-to-the-sudoers-file"&gt;How to add a user to the sudoers file&lt;/h4&gt;
&lt;p&gt;To avoid these terrible reports, it sounds like I need to add myself to the &amp;lsquo;sudoers file&amp;rsquo;. I won&amp;rsquo;t be able to do that as myself, so I&amp;rsquo;ll log back in as &lt;code&gt;root&lt;/code&gt; for a bit. The reason I don&amp;rsquo;t just operate as &lt;code&gt;root&lt;/code&gt; all the time is that I quite like the constant reminder that I&amp;rsquo;m about to do something administratory - so I should have a second thought before I sudo that shell command I just copied out of a stackoverflow answer.&lt;/p&gt;
&lt;p&gt;Since the error message says I&amp;rsquo;m not in the sudoers file, I should just add my name right? Well yes, and no. That is possible, but it&amp;rsquo;s slightly dangerous - it has a specific format, and if you stuff things up bad things can happen. For this reason there&amp;rsquo;s a special command to edit it (visudo) which refuses to save it if you make a mistake.&lt;/p&gt;
&lt;p&gt;The sudoers file is at &lt;code&gt;/etc/sudoers&lt;/code&gt;, if you &lt;code&gt;cat&lt;/code&gt; it, it has a heap of commented out stuff, but there&amp;rsquo;s be a section that looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# User privilege specification
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root	ALL=(ALL:ALL) ALL
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Members of the admin group may gain root privileges
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;%admin ALL=(ALL) ALL
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# Allow members of group sudo to execute any command
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;%sudo	ALL=(ALL:ALL) ALL
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;# See sudoers(5) for more information on &amp;#34;@include&amp;#34; directives:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@includedir /etc/sudoers.d
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That last line includes any files in the &lt;code&gt;/ect/sudoers.d&lt;/code&gt; as part of this one, so if we really did want to add &lt;code&gt;ian&lt;/code&gt; to this file, we&amp;rsquo;d do it there, but still by using the &lt;code&gt;visudo&lt;/code&gt; command to do it safely.&lt;/p&gt;
&lt;p&gt;But, we don&amp;rsquo;t need to. The &lt;code&gt;%admin&lt;/code&gt; and &lt;code&gt;%sudo&lt;/code&gt; lines are granting these permissions to groups, so all we need to do is add &lt;code&gt;ian&lt;/code&gt; to the &lt;code&gt;sudo&lt;/code&gt; group and those permissions will be granted, safely.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;usermod -a -G sudo ian
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Success:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#d8dee9;background-color:#2e3440;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-zed" data-lang="zed"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;enrico&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;rider&lt;span style="color:#81a1c1"&gt;:~&lt;/span&gt;&lt;span style="color:#bf616a"&gt;$&lt;/span&gt; docker ps
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Got &lt;span style="color:#81a1c1;font-weight:bold"&gt;permission&lt;/span&gt; denied while trying to connect to the Docker daemon socket at unix&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt;&lt;span style="color:#616e87;font-style:italic"&gt;///var/run/docker.sock: Get &amp;#34;http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/json&amp;#34;: dial unix /var/run/docker.sock: connect: permission denied
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;enrico&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;rider&lt;span style="color:#81a1c1"&gt;:~&lt;/span&gt;&lt;span style="color:#bf616a"&gt;$&lt;/span&gt; sudo docker ps
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#eceff4"&gt;[&lt;/span&gt;sudo&lt;span style="color:#eceff4"&gt;]&lt;/span&gt; password for ian&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bf616a"&gt;520&lt;/span&gt;ed656ef12 &lt;span style="color:#8fbcbb"&gt;dockersamples/&lt;/span&gt;&lt;span style="color:#bf616a"&gt;101&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;tutorial &lt;span style="color:#bf616a"&gt;&amp;#34;&lt;/span&gt;nginx &lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;g &lt;span style="color:#bf616a"&gt;&amp;#39;&lt;/span&gt;daemon of&lt;span style="color:#bf616a"&gt;…&amp;#34;&lt;/span&gt; &lt;span style="color:#bf616a"&gt;14&lt;/span&gt; hours ago Up &lt;span style="color:#bf616a"&gt;14&lt;/span&gt; hours &lt;span style="color:#bf616a"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#bf616a"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#bf616a"&gt;0&lt;/span&gt;&lt;span style="color:#eceff4"&gt;.&lt;/span&gt;&lt;span style="color:#bf616a"&gt;0&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;:&lt;/span&gt;&lt;span style="color:#bf616a"&gt;80&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&amp;gt;&lt;/span&gt;&lt;span style="color:#bf616a"&gt;80&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;tcp&lt;span style="color:#eceff4"&gt;,&lt;/span&gt; &lt;span style="color:#81a1c1"&gt;:::&lt;/span&gt;&lt;span style="color:#bf616a"&gt;80&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;-&amp;gt;&lt;/span&gt;&lt;span style="color:#bf616a"&gt;80&lt;/span&gt;&lt;span style="color:#81a1c1"&gt;/&lt;/span&gt;tcp pedantic_bartik
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ian&lt;span style="color:#bf616a"&gt;@&lt;/span&gt;enrico&lt;span style="color:#81a1c1"&gt;-&lt;/span&gt;rider&lt;span style="color:#81a1c1"&gt;:~&lt;/span&gt;&lt;span style="color:#bf616a"&gt;$&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item></channel></rss>