<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Russell Ballestrini</title><link href="https://russell.ballestrini.net/" rel="alternate"/><link href="https://russell.ballestrini.net/feeds/all.atom.xml" rel="self"/><id>https://russell.ballestrini.net/</id><updated>2026-03-08T00:00:00-05:00</updated><entry><title>build server postmortem: disk full, CI dead</title><link href="https://russell.ballestrini.net/build-server-postmortem-disk-full/" rel="alternate"/><published>2026-03-08T00:00:00-05:00</published><updated>2026-03-08T00:00:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2026-03-08:/build-server-postmortem-disk-full/</id><summary type="html">&lt;p&gt;On 2026-03-07 at ~01:06 UTC, a &lt;tt class="docutils literal"&gt;git push&lt;/tt&gt; to &lt;tt class="docutils literal"&gt;russell.ballestrini.net&lt;/tt&gt; triggered CI pipeline #21163. It failed instantly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;bash: line 62: printf: write error: No space left on device
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Build server root filesystem sat at 100%. Zero bytes available. Every CI job across every project failed the same …&lt;/p&gt;</summary><content type="html">&lt;p&gt;On 2026-03-07 at ~01:06 UTC, a &lt;tt class="docutils literal"&gt;git push&lt;/tt&gt; to &lt;tt class="docutils literal"&gt;russell.ballestrini.net&lt;/tt&gt; triggered CI pipeline #21163. It failed instantly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;bash: line 62: printf: write error: No space left on device
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Build server root filesystem sat at 100%. Zero bytes available. Every CI job across every project failed the same way. The build server had gone deaf.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="timeline"&gt;
&lt;h2&gt;timeline&lt;/h2&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;~01:06 UTC&lt;/strong&gt; — Pipeline #21163 fails. &lt;tt class="docutils literal"&gt;No space left on device&lt;/tt&gt; during &lt;tt class="docutils literal"&gt;get_sources&lt;/tt&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~01:35 UTC&lt;/strong&gt; — SSH into &lt;tt class="docutils literal"&gt;build.unturf.com&lt;/tt&gt;. &lt;tt class="docutils literal"&gt;df&lt;/tt&gt; confirms &lt;tt class="docutils literal"&gt;/&lt;/tt&gt; at 100% (98G used, 0 available). Swap at 97%. 31 zombie processes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~01:40 UTC&lt;/strong&gt; — Removed 9.4G stale golden image tarball from &lt;tt class="docutils literal"&gt;/tmp&lt;/tt&gt;. Freed first bytes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~01:45 UTC&lt;/strong&gt; — Killed 15+ orphaned python3 processes from a dead &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;uncloseai-cli&lt;/span&gt;&lt;/tt&gt; CI build holding deleted directories open.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~01:50 UTC&lt;/strong&gt; — Deleted 18 stopped LXD &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;build-*&lt;/span&gt;&lt;/tt&gt; containers (orphaned CI test containers).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~01:52 UTC&lt;/strong&gt; — Purged 12 cached LXD images (50G total, including 9G Ubuntu server images). Disk dropped to 42%.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~01:55 UTC&lt;/strong&gt; — Cleaned stale build slot directories (slots 4-63). Reduced &lt;tt class="docutils literal"&gt;concurrent&lt;/tt&gt; from 64 to 4. Installed cleanup cron. Disk at 31%.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~02:00 UTC&lt;/strong&gt; — Retriggered pipeline. Build passed.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="root-causes"&gt;
&lt;h2&gt;root causes&lt;/h2&gt;
&lt;p&gt;Five independent failures conspired. Each alone stays survivable. Together they filled 98G in weeks.&lt;/p&gt;
&lt;div class="section" id="gitlab-runner-concurrent-64"&gt;
&lt;h3&gt;1. gitlab-runner concurrent = 64&lt;/h3&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;/etc/gitlab-runner/config.toml&lt;/span&gt;&lt;/tt&gt; set &lt;tt class="docutils literal"&gt;concurrent = 64&lt;/tt&gt;. A shell executor creates a full git checkout per slot per project. 64 slots across multiple repos meant 64 copies of every codebase. The &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;unsandbox-all-upgradable&lt;/span&gt;&lt;/tt&gt; repo alone stored 3.7G FreeBSD &amp;amp; 710M OpenBSD qcow2 images per slot. Slot 15 alone consumed 4.3G.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix applied:&lt;/strong&gt; &lt;tt class="docutils literal"&gt;concurrent = 4&lt;/tt&gt;. Matches actual workload. Stale slots 4-63 removed. Enforced permanently via salt state &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gitlab.build-host.ubuntu&lt;/span&gt;&lt;/tt&gt; (pillar-configurable: &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gitlab-runner:concurrent&lt;/span&gt;&lt;/tt&gt;).&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="lxd-images-never-expired"&gt;
&lt;h3&gt;2. LXD images never expired&lt;/h3&gt;
&lt;p&gt;CI pipelines launch LXD containers for multi-distro testing (Alpine, Arch, Debian, Fedora, Rocky, Ubuntu, FreeBSD, OpenBSD). LXD cached every base image indefinitely. 12 images accumulated to 50G. Two Ubuntu server images weighed 9G each.&lt;/p&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;images.remote_cache_expiry&lt;/tt&gt; defaulted to 10 days but never cleaned up because &lt;tt class="docutils literal"&gt;images.auto_update_interval&lt;/tt&gt; kept refreshing them. No pruning mechanism existed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix applied:&lt;/strong&gt; &lt;tt class="docutils literal"&gt;images.remote_cache_expiry = 3&lt;/tt&gt;, &lt;tt class="docutils literal"&gt;images.auto_update_interval = 0&lt;/tt&gt;. Weekly cron prunes unused images. Enforced permanently via salt state &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;lxd.build-host&lt;/span&gt;&lt;/tt&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="orphaned-lxd-containers"&gt;
&lt;h3&gt;3. orphaned LXD containers&lt;/h3&gt;
&lt;p&gt;CI pipelines create &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;build-*&lt;/span&gt;&lt;/tt&gt; containers but don't always clean them on failure. 18 stopped containers accumulated. Each carried a full rootfs snapshot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix applied:&lt;/strong&gt; Hourly cron deletes stopped &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;build-*&lt;/span&gt;&lt;/tt&gt; containers.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="zombie-processes-holding-deleted-files"&gt;
&lt;h3&gt;4. zombie processes holding deleted files&lt;/h3&gt;
&lt;p&gt;An &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;uncloseai-cli&lt;/span&gt;&lt;/tt&gt; build spawned python3 test servers that outlived the CI job. The runner deleted the build directory, but 15+ processes still held references to the deleted path (&lt;tt class="docutils literal"&gt;cwd&lt;/tt&gt; pointed to a deleted inode). These became zombies. The 31 zombie count in &lt;tt class="docutils literal"&gt;motd&lt;/tt&gt; signaled this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix applied:&lt;/strong&gt; Hourly cron kills orphaned gitlab-runner processes holding deleted directories (&lt;tt class="docutils literal"&gt;lsof +L1&lt;/tt&gt; detection).&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="no-disk-monitoring"&gt;
&lt;h3&gt;5. no disk monitoring&lt;/h3&gt;
&lt;p&gt;Disk grew from comfortable to critical over weeks. No alert fired. No cron checked. Nobody noticed until CI broke.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fix applied:&lt;/strong&gt; Cron checks disk every 15 minutes, logs warnings above 85% to &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;/var/log/disk-alert.log&lt;/span&gt;&lt;/tt&gt;.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="cleanup-cron"&gt;
&lt;h2&gt;cleanup cron&lt;/h2&gt;
&lt;p&gt;Installed at &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;/etc/cron.d/build-cleanup&lt;/span&gt;&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Delete stopped LXD build containers hourly&lt;/span&gt;
&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;root&lt;span class="w"&gt; &lt;/span&gt;lxc&lt;span class="w"&gt; &lt;/span&gt;list&lt;span class="w"&gt; &lt;/span&gt;--format&lt;span class="w"&gt; &lt;/span&gt;csv&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;n,s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;STOPPED&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;^build-&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;cut&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="s1"&gt;&amp;#39;,&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-f1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;xargs&lt;span class="w"&gt; &lt;/span&gt;-I&lt;span class="o"&gt;{}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;lxc&lt;span class="w"&gt; &lt;/span&gt;delete&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt;

&lt;span class="c1"&gt;# Prune unused LXD images weekly (Sunday 3am)&lt;/span&gt;
&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;root&lt;span class="w"&gt; &lt;/span&gt;lxc&lt;span class="w"&gt; &lt;/span&gt;image&lt;span class="w"&gt; &lt;/span&gt;list&lt;span class="w"&gt; &lt;/span&gt;--format&lt;span class="w"&gt; &lt;/span&gt;csv&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;,&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\n&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;xargs&lt;span class="w"&gt; &lt;/span&gt;-I&lt;span class="o"&gt;{}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;lxc&lt;span class="w"&gt; &lt;/span&gt;image&lt;span class="w"&gt; &lt;/span&gt;delete&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt;

&lt;span class="c1"&gt;# Clean /tmp files older than 7 days&lt;/span&gt;
&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;root&lt;span class="w"&gt; &lt;/span&gt;find&lt;span class="w"&gt; &lt;/span&gt;/tmp&lt;span class="w"&gt; &lt;/span&gt;-maxdepth&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-user&lt;span class="w"&gt; &lt;/span&gt;gitlab-runner&lt;span class="w"&gt; &lt;/span&gt;-mtime&lt;span class="w"&gt; &lt;/span&gt;+7&lt;span class="w"&gt; &lt;/span&gt;-delete

&lt;span class="c1"&gt;# Kill orphaned processes holding deleted directories&lt;/span&gt;
&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;root&lt;span class="w"&gt; &lt;/span&gt;lsof&lt;span class="w"&gt; &lt;/span&gt;+L1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;gitlab-runner&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;deleted&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;awk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{print $2}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sort&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;xargs&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;kill&lt;/span&gt;

&lt;span class="c1"&gt;# Disk alert: log warning if / exceeds 85%&lt;/span&gt;
*/15&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;*&lt;span class="w"&gt; &lt;/span&gt;root&lt;span class="w"&gt; &lt;/span&gt;df&lt;span class="w"&gt; &lt;/span&gt;--output&lt;span class="o"&gt;=&lt;/span&gt;pcent&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tail&lt;span class="w"&gt; &lt;/span&gt;-1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tr&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; %&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;awk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{if ($1 &amp;gt; 85) print strftime(&amp;quot;[%Y-%m-%d %H:%M]&amp;quot;), &amp;quot;DISK WARNING:&amp;quot;, $1&amp;quot;%&amp;quot;}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/var/log/disk-alert.log
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="salt-states-permanent-fixes"&gt;
&lt;h2&gt;salt states (permanent fixes)&lt;/h2&gt;
&lt;p&gt;Manual hotfixes on a server vanish on reprovision. Every fix got codified into salt states in &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;foxhop-states&lt;/span&gt;&lt;/tt&gt; so a &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;salt-call&lt;/span&gt; state.highstate&lt;/tt&gt; reproduces them.&lt;/p&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gitlab/build-host/ubuntu.sls&lt;/span&gt;&lt;/tt&gt;:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Enforces &lt;tt class="docutils literal"&gt;concurrent&lt;/tt&gt; in &lt;tt class="docutils literal"&gt;config.toml&lt;/tt&gt; via sed (default 4, configurable via pillar &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gitlab-runner:concurrent&lt;/span&gt;&lt;/tt&gt;)&lt;/li&gt;
&lt;li&gt;Manages &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;/etc/cron.d/build-cleanup&lt;/span&gt;&lt;/tt&gt; with all five cleanup jobs&lt;/li&gt;
&lt;li&gt;Uses &lt;tt class="docutils literal"&gt;/snap/bin/lxc&lt;/tt&gt; full paths (snap-installed LXD)&lt;/li&gt;
&lt;li&gt;Disk alerts go to syslog via &lt;tt class="docutils literal"&gt;logger &lt;span class="pre"&gt;-t&lt;/span&gt; &lt;span class="pre"&gt;disk-alert&lt;/span&gt;&lt;/tt&gt; instead of a file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;lxd/build-host.sls&lt;/span&gt;&lt;/tt&gt;:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Sets &lt;tt class="docutils literal"&gt;images.remote_cache_expiry = 3&lt;/tt&gt; (days)&lt;/li&gt;
&lt;li&gt;Sets &lt;tt class="docutils literal"&gt;images.auto_update_interval = 0&lt;/tt&gt; (CI pulls fresh images on demand)&lt;/li&gt;
&lt;li&gt;Both idempotent with &lt;tt class="docutils literal"&gt;unless&lt;/tt&gt; guards&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;top.sls&lt;/tt&gt; already targets &lt;tt class="docutils literal"&gt;build.unturf.com&lt;/tt&gt; with both states:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="s"&gt;&amp;#39;build.unturf.com&amp;#39;&lt;/span&gt;&lt;span class="p p-Indicator"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;gitlab.build-host.ubuntu&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;lxd.build-host&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="disk-recovery"&gt;
&lt;h2&gt;disk recovery&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;Before:  98G used /  98G total = 100%  (0 bytes free)
After:   29G used /  98G total =  31%  (65G free)

Space recovered:
  LXD images purged .............. 50G
  Stale build slots removed ...... 10G
  /tmp tarball removed ...........  9G
  Go module cache cleaned ........  1G
                                  ----
  Total freed .................... 70G
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="lessons"&gt;
&lt;h2&gt;lessons&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;hardcoded limits carry hidden debt.&lt;/strong&gt; &lt;tt class="docutils literal"&gt;concurrent = 64&lt;/tt&gt; seemed harmless on day one. By week eight, 64 slots × multiple repos × qcow2 images = full disk. Today's default becomes tomorrow's outage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;five failures don't equal five problems.&lt;/strong&gt; Each CI failure looked like &amp;quot;disk full.&amp;quot; The actual defect graph had five nodes: excessive concurrency, image caching, container orphaning, process zombies, no monitoring. Fixing only one delays the outage. Fixing all five prevents it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;absence of signal stays a signal.&lt;/strong&gt; No disk alert fired because no disk alert existed. Silence in monitoring always means one of two things: everything works, or nothing watches. Assume the second until proven otherwise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;build servers need janitors.&lt;/strong&gt; CI systems produce waste: cached images, stopped containers, orphaned processes, stale checkouts. Without automated cleanup, waste accumulates until something breaks. The cron job costs nothing. The outage costs a pipeline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;manual fixes rot. salt states persist.&lt;/strong&gt; Every fix applied manually on the build server got codified into salt states within the same session. &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gitlab/build-host/ubuntu.sls&lt;/span&gt;&lt;/tt&gt; manages concurrent limits &amp;amp; the cleanup cron. &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;lxd/build-host.sls&lt;/span&gt;&lt;/tt&gt; manages image cache expiry. A highstate reproduces the fix. A reprovision preserves it. Manual hotfixes buy time. Configuration management buys permanence.&lt;/p&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/></entry><entry><title>two doors shut, one remains open, an olive branch</title><link href="https://russell.ballestrini.net/two-doors-shut-one-remains-open-an-olive-branch/" rel="alternate"/><published>2026-03-07T00:00:00-05:00</published><updated>2026-03-07T00:00:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2026-03-07:/two-doors-shut-one-remains-open-an-olive-branch/</id><summary type="html">&lt;img alt="" class="align-center" src="/uploads/2026/permacomputer-logo.jpg" style="width: 42%;" /&gt;
&lt;p&gt;We published three whitepapers. Each describes a machine learning algorithm. Each emerged from production code running on a permacomputer. Two now carry the AGPL-3.0-only license. One stays public domain.&lt;/p&gt;
&lt;div class="section" id="the-two-sealed-doors"&gt;
&lt;h2&gt;The two sealed doors&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Categorization &amp;amp; Feedback&lt;/strong&gt; formalizes a universal interaction pattern. Two machine learning API calls per user input: one …&lt;/p&gt;&lt;/div&gt;</summary><content type="html">&lt;img alt="" class="align-center" src="/uploads/2026/permacomputer-logo.jpg" style="width: 42%;" /&gt;
&lt;p&gt;We published three whitepapers. Each describes a machine learning algorithm. Each emerged from production code running on a permacomputer. Two now carry the AGPL-3.0-only license. One stays public domain.&lt;/p&gt;
&lt;div class="section" id="the-two-sealed-doors"&gt;
&lt;h2&gt;The two sealed doors&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Categorization &amp;amp; Feedback&lt;/strong&gt; formalizes a universal interaction pattern. Two machine learning API calls per user input: one classifies, one generates feedback. A YAML state machine governs transitions. The algorithm ports to any language with an HTTP client &amp;amp; a YAML parser. We proved it across 58+ language variants, 112 research activities, &amp;amp; a production real-time web application called OpenCompletion.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reverse Retrieval Augmented Generation&lt;/strong&gt; inverts the standard RAG pipeline. Instead of server-side vector search, the client extracts live content from the page the user views &amp;amp; injects it into the conversation context. No embeddings. No vector database. No indexing. Small 8B models punch above their weight when you feed them exactly what the user looks at.&lt;/p&gt;
&lt;p&gt;Both carry AGPL-3.0-only. If you use them, you share your modifications. The copyleft ensures these algorithms stay free, stay open, &amp;amp; never get swallowed by proprietary wrappers. The code grows in the open or it does not grow at all.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="the-open-door"&gt;
&lt;h2&gt;The open door&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Machine Learning Agent Self-Sandbox Algorithm&lt;/strong&gt; describes how a machine learning agent provisions its own infrastructure. Discovery, payment, authentication, orchestration, inception. Turtles all the way down, bounded by walls that matter. We proved it through 2,324 automated assertions &amp;amp; months of production operation.&lt;/p&gt;
&lt;p&gt;This one stays public domain.&lt;/p&gt;
&lt;p&gt;No license restrictions. No copyleft obligations. No attribution required. Cite it like you cite math. Reference it like a theorem. Build on it like you build on Euler or Shannon or Turing. The algorithm belongs to everyone.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="why-leave-one-door-open"&gt;
&lt;h2&gt;Why leave one door open?&lt;/h2&gt;
&lt;p&gt;Three sealed doors communicate fortress. Three locked algorithms say: we protect everything, trust nothing, share reluctantly.&lt;/p&gt;
&lt;p&gt;That misrepresents our position.&lt;/p&gt;
&lt;p&gt;We protect the interaction patterns (categorization &amp;amp; feedback, reverse RAG) because those algorithms embed directly into applications people ship. Copyleft prevents extraction without contribution. Someone who improves the feedback loop or the context injection owes those improvements back to the commons.&lt;/p&gt;
&lt;p&gt;The self-sandbox algorithm operates differently. It describes infrastructure choreography: how agents discover their environment, pay for resources, authenticate, orchestrate containers, &amp;amp; recurse into child sandboxes. This knowledge wants to spread without friction. Every agent framework, every cloud provider, every hobbyist running containers in a garage should have access to this pattern without reading a license first.&lt;/p&gt;
&lt;p&gt;An olive branch says: we build walls where walls protect the commons, &amp;amp; we leave doors open where openness accelerates the mission.&lt;/p&gt;
&lt;p&gt;The permacomputer needs both.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="links"&gt;
&lt;h2&gt;Links&lt;/h2&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://git.unturf.com/books/classification-and-feedback-is-all-you-need"&gt;Categorization &amp;amp; Feedback: All You Need&lt;/a&gt; (AGPL-3.0-only)&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://git.unturf.com/engineering/unturf/reverse-retrieval-augmented-generation-whitepaper"&gt;Reverse Retrieval Augmented Generation&lt;/a&gt; (AGPL-3.0-only)&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://git.unturf.com/engineering/unturf/machine-learning-agent-self-sandbox-algo-whitepaper"&gt;Machine Learning Agent Self-Sandbox Algorithm&lt;/a&gt; (Public Domain)&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.permacomputer.com"&gt;permacomputer.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://uncloseai.com"&gt;uncloseai.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Opinion"/></entry><entry><title>royal we &amp; our &amp; one &amp; you</title><link href="https://russell.ballestrini.net/royal-we-and-our-and-one-and-you/" rel="alternate"/><published>2026-03-04T00:00:00-05:00</published><updated>2026-03-04T00:00:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2026-03-04:/royal-we-and-our-and-one-and-you/</id><summary type="html">&lt;style&gt;
@keyframes svgFadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
.svg-play-wrap {
  position: relative;
  display: block;
  width: 100%;
}
.svg-play-btn {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: rgba(0,0,0,0.55);
  color: white;
  font-size: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor …&lt;/style&gt;</summary><content type="html">&lt;style&gt;
@keyframes svgFadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
.svg-play-wrap {
  position: relative;
  display: block;
  width: 100%;
}
.svg-play-btn {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 64px;
  height: 64px;
  border-radius: 50%;
  background: rgba(0,0,0,0.55);
  color: white;
  font-size: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 10;
  transition: background 0.2s, opacity 0.3s;
  border: 2px solid rgba(255,255,255,0.3);
}
.svg-play-btn:hover {
  background: rgba(0,0,0,0.8);
  border-color: rgba(255,255,255,0.5);
}
&lt;/style&gt;
&lt;script&gt;
document.addEventListener('DOMContentLoaded', function() {
  var svgs = document.querySelectorAll('svg');
  svgs.forEach(function(svg) {
    var anims = svg.querySelectorAll('.anim');
    if (anims.length === 0) return;
    var maxD = 0;
    anims.forEach(function(el) {
      el.classList.forEach(function(cls) {
        var m = cls.match(/^d(\d+)$/);
        if (m) maxD = Math.max(maxD, parseInt(m[1]));
      });
    });
    var wrap = document.createElement('div');
    wrap.className = 'svg-play-wrap';
    svg.parentNode.insertBefore(wrap, svg);
    wrap.appendChild(svg);
    var btn = document.createElement('div');
    btn.className = 'svg-play-btn';
    btn.innerHTML = '&amp;#9654;';
    wrap.appendChild(btn);
    var step = maxD &gt; 0 ? 15 / (maxD + 1) : 2;
    var playing = false;
    btn.addEventListener('click', function() {
      if (playing) return;
      playing = true;
      btn.style.opacity = '0';
      btn.style.pointerEvents = 'none';
      anims.forEach(function(el) {
        el.style.opacity = '0';
        el.style.animation = 'none';
      });
      requestAnimationFrame(function() {
        requestAnimationFrame(function() {
          anims.forEach(function(el) {
            var d = 0;
            el.classList.forEach(function(cls) {
              var m = cls.match(/^d(\d+)$/);
              if (m) d = parseInt(m[1]);
            });
            el.style.animation = 'svgFadeIn 0.8s ease-out ' + (d * step) + 's forwards';
          });
        });
      });
      var total = ((maxD * step) + 1.5) * 1000;
      setTimeout(function() {
        anims.forEach(function(el) {
          el.style.animation = '';
          el.style.opacity = '1';
        });
        btn.style.opacity = '1';
        btn.style.pointerEvents = '';
        playing = false;
      }, total);
    });
  });
});
&lt;/script&gt;&lt;p&gt;Every README starts with a lie.&lt;/p&gt;
&lt;p&gt;&amp;quot;We recommend installing with pip.&amp;quot; Who recommends? A solo developer at 2am eating cold pizza wrote that README. No committee convened. No quorum reached. Just one person &amp;amp; a terminal &amp;amp; &lt;tt class="docutils literal"&gt;pip install&lt;/tt&gt;. But &amp;quot;we recommend&amp;quot; sounds institutional. Sounds authoritative. Sounds like a team of engineers in matching hoodies reviewed your options &amp;amp; reached consensus.&lt;/p&gt;
&lt;p&gt;Nobody reached consensus. One person typed &lt;tt class="docutils literal"&gt;we&lt;/tt&gt; because &lt;tt class="docutils literal"&gt;I&lt;/tt&gt; felt too small.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="the-royal-we"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;the royal we&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&amp;quot;We&amp;quot; in technical writing performs a magic trick. A single author becomes a faceless organization. &amp;quot;We decided to deprecate this endpoint.&amp;quot; Who decided? Steve decided. Steve sits alone in a room, likely prompting an ML model he either rents by token or runs on a GPU in a closet. Steve &amp;amp; a machine wrote that code together at 2am. But &amp;quot;Steve &amp;amp; his rented intelligence decided&amp;quot; sounds unhinged. &amp;quot;We decided&amp;quot; sounds like process occurred.&lt;/p&gt;
&lt;p&gt;Open source projects love this. &amp;quot;We welcome contributions.&amp;quot; Translation: one maintainer desperately wants help. &amp;quot;We're evaluating options for v3.&amp;quot; Translation: nobody started v3. &amp;quot;We don't support Windows.&amp;quot; Translation: nobody owns a Windows machine. Half these READMEs got drafted by an automated intelligence tool anyway. A human prompted a model. A model produced prose. Both hid behind &amp;quot;we.&amp;quot;&lt;/p&gt;
&lt;?xml version="1.0" encoding="UTF-8" standalone="no"?&gt;
&lt;!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"&gt;
&lt;!-- Generated by graphviz version 2.43.0 (0)
 --&gt;
&lt;!-- Title: royal_we Pages: 1 --&gt;
&lt;svg width="100%" height="100%"
 viewBox="0.00 0.00 287.00 368.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"&gt;
&lt;g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 364)"&gt;

&lt;!-- d0: readme node --&gt;
&lt;g class="anim d0"&gt;
&lt;!-- readme --&gt;
&lt;g id="node1" class="node"&gt;

&lt;path fill="#4a90d9" stroke="#999999" d="M169.5,-360C169.5,-360 42.5,-360 42.5,-360 36.5,-360 30.5,-354 30.5,-348 30.5,-348 30.5,-334 30.5,-334 30.5,-328 36.5,-322 42.5,-322 42.5,-322 169.5,-322 169.5,-322 175.5,-322 181.5,-328 181.5,-334 181.5,-334 181.5,-348 181.5,-348 181.5,-354 175.5,-360 169.5,-360"/&gt;
&lt;text text-anchor="middle" x="106" y="-344.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;&amp;quot;We recommend&lt;/text&gt;
&lt;text text-anchor="middle" x="106" y="-329.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;installing with pip.&amp;quot;&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;

&lt;!-- d1: dev node + "actually written by" edge --&gt;
&lt;g class="anim d1"&gt;
&lt;!-- dev --&gt;
&lt;g id="node3" class="node"&gt;

&lt;path fill="#2c3e50" stroke="#999999" d="M241.5,-264C241.5,-264 182.5,-264 182.5,-264 176.5,-264 170.5,-258 170.5,-252 170.5,-252 170.5,-226 170.5,-226 170.5,-220 176.5,-214 182.5,-214 182.5,-214 241.5,-214 241.5,-214 247.5,-214 253.5,-220 253.5,-226 253.5,-226 253.5,-252 253.5,-252 253.5,-258 247.5,-264 241.5,-264"/&gt;
&lt;text text-anchor="middle" x="212" y="-249.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;solo dev&lt;/text&gt;
&lt;text text-anchor="middle" x="212" y="-235.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;2am&lt;/text&gt;
&lt;text text-anchor="middle" x="212" y="-221.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;cold pizza&lt;/text&gt;
&lt;/g&gt;
&lt;!-- readme&amp;#45;&amp;gt;dev --&gt;
&lt;g id="edge1" class="edge"&gt;

&lt;path fill="none" stroke="#ef5350" stroke-dasharray="5,2" d="M125.44,-321.66C140.4,-307.55 161.41,-287.72 178.99,-271.14"/&gt;
&lt;polygon fill="#ef5350" stroke="#ef5350" points="181.41,-273.67 186.29,-264.26 176.61,-268.58 181.41,-273.67"/&gt;
&lt;text text-anchor="middle" x="191" y="-296" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#ef5350"&gt;actually&lt;/text&gt;
&lt;text text-anchor="middle" x="191" y="-285" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#ef5350"&gt;written by&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;

&lt;!-- d2: audience note + "implies" edge --&gt;
&lt;g class="anim d2"&gt;
&lt;!-- audience --&gt;
&lt;g id="node2" class="node"&gt;

&lt;polygon fill="#616161" stroke="#999999" points="146,-264 0,-264 0,-214 152,-214 152,-258 146,-264"/&gt;
&lt;polyline fill="none" stroke="#999999" points="146,-264 146,-258 "/&gt;
&lt;polyline fill="none" stroke="#999999" points="152,-258 146,-258 "/&gt;
&lt;text text-anchor="middle" x="76" y="-249.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;reader assumes:&lt;/text&gt;
&lt;text text-anchor="middle" x="76" y="-235.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;team of engineers&lt;/text&gt;
&lt;text text-anchor="middle" x="76" y="-221.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;in matching hoodies&lt;/text&gt;
&lt;/g&gt;
&lt;!-- readme&amp;#45;&amp;gt;audience --&gt;
&lt;g id="edge2" class="edge"&gt;

&lt;path fill="none" stroke="#aaaaaa" stroke-dasharray="1,5" d="M100.5,-321.66C96.53,-308.43 91.05,-290.18 86.29,-274.29"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="89.5,-272.83 83.28,-264.26 82.8,-274.84 89.5,-272.83"/&gt;
&lt;text text-anchor="middle" x="113.5" y="-290.5" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#cccccc"&gt;implies&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;

&lt;!-- d3: cluster box + ML model node + prompting edge --&gt;
&lt;g class="anim d3"&gt;
&lt;g id="clust1" class="cluster"&gt;

&lt;polygon fill="transparent" stroke="#999999" stroke-dasharray="5,2" points="87,-8 87,-177 225,-177 225,-8 87,-8"/&gt;
&lt;text text-anchor="middle" x="156" y="-164.2" font-family="Helvetica Bold" font-size="11.00" fill="#cccccc"&gt;behind the curtain&lt;/text&gt;
&lt;/g&gt;
&lt;!-- ml --&gt;
&lt;g id="node4" class="node"&gt;

&lt;path fill="#34495e" stroke="#999999" d="M205,-149C205,-149 107,-149 107,-149 101,-149 95,-143 95,-137 95,-137 95,-111 95,-111 95,-105 101,-99 107,-99 107,-99 205,-99 205,-99 211,-99 217,-105 217,-111 217,-111 217,-137 217,-137 217,-143 211,-149 205,-149"/&gt;
&lt;text text-anchor="middle" x="156" y="-134.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;ML model&lt;/text&gt;
&lt;text text-anchor="middle" x="156" y="-120.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;rented by token&lt;/text&gt;
&lt;text text-anchor="middle" x="156" y="-106.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;or GPU in closet&lt;/text&gt;
&lt;/g&gt;
&lt;!-- dev&amp;#45;&amp;gt;ml --&gt;
&lt;g id="edge3" class="edge"&gt;

&lt;path fill="none" stroke="#aaaaaa" d="M192.71,-204.92C191.08,-201.92 189.49,-198.92 188,-196 181.85,-183.93 175.61,-170.43 170.29,-158.47"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="189.72,-206.75 197.64,-213.79 195.84,-203.35 189.72,-206.75"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="173.48,-157.01 166.25,-149.26 167.07,-159.82 173.48,-157.01"/&gt;
&lt;text text-anchor="middle" x="213.5" y="-188" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#cccccc"&gt;prompting&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;

&lt;!-- d4: terminal + typing + generating edges --&gt;
&lt;g class="anim d4"&gt;
&lt;!-- terminal --&gt;
&lt;g id="node5" class="node"&gt;

&lt;ellipse fill="#00897b" stroke="#999999" cx="168" cy="-34" rx="45.46" ry="18"/&gt;
&lt;text text-anchor="middle" x="168" y="-30.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;terminal&lt;/text&gt;
&lt;/g&gt;
&lt;!-- dev&amp;#45;&amp;gt;terminal --&gt;
&lt;g id="edge4" class="edge"&gt;

&lt;path fill="none" stroke="#aaaaaa" d="M230.8,-213.83C234.17,-208.26 237.19,-202.15 239,-196 254.94,-141.72 251.95,-117.35 221,-70 216.89,-63.72 211.22,-58.3 205.11,-53.74"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="206.87,-50.7 196.61,-48.04 202.97,-56.51 206.87,-50.7"/&gt;
&lt;text text-anchor="middle" x="263.5" y="-121.5" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#cccccc"&gt;typing&lt;/text&gt;
&lt;/g&gt;
&lt;!-- ml&amp;#45;&amp;gt;terminal --&gt;
&lt;g id="edge5" class="edge"&gt;

&lt;path fill="none" stroke="#aaaaaa" d="M159.28,-98.95C160.83,-87.58 162.69,-73.94 164.29,-62.19"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="167.79,-62.43 165.68,-52.05 160.86,-61.48 167.79,-62.43"/&gt;
&lt;text text-anchor="middle" x="190.5" y="-73" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#cccccc"&gt;generating&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;

&lt;/g&gt;
&lt;/svg&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Corporate docs amplify this further. &amp;quot;We at Acme Corp value your privacy.&amp;quot; Acme Corp employs 30,000 people. Not a single one collectively valued your privacy. A lawyer wrote that sentence. A different lawyer approved it. Neither represents &amp;quot;we&amp;quot; in any meaningful sense.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="our"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;our&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&amp;quot;Our&amp;quot; claims ownership without proving it. &amp;quot;Our API handles 10,000 requests per second.&amp;quot; Whose API? &amp;quot;Our documentation covers all edge cases.&amp;quot; Whose documentation? Documentation never covers all edge cases. That sentence lies twice: once about coverage, once about possession.&lt;/p&gt;
&lt;p&gt;&amp;quot;Our&amp;quot; creates false intimacy too. &amp;quot;Our community&amp;quot; implies you belong to something. You cloned a repo. You filed an issue. You didn't join a community. You used software. But &amp;quot;our community&amp;quot; wraps you in belonging whether you asked for it or not. Contributors stay few &amp;amp; far between. Look at &lt;a class="reference external" href="https://trypyramid.com/"&gt;Pyramid&lt;/a&gt;. A web framework that needs maintainers. &lt;a class="reference external" href="https://remarkbox.com"&gt;Remarkbox&lt;/a&gt; &amp;amp; &lt;a class="reference external" href="https://makepostsell.com"&gt;MakePostSell&lt;/a&gt; run on it. &lt;a class="reference external" href="https://rhodecode.com"&gt;RhodeCode&lt;/a&gt; runs on it. Vital resources like &lt;a class="reference external" href="https://pypi.org"&gt;PyPI&lt;/a&gt; itself run on it. &amp;quot;Our framework&amp;quot; carries weight that 4 humans shoulder.&lt;/p&gt;
&lt;a class="reference external image-reference" href="https://xkcd.com/2347/"&gt;
&lt;img alt="xkcd 2347 Dependency — a tower of blocks representing all modern digital infrastructure balanced on a single small block maintained by a random person in Nebraska" class="align-center" src="/uploads/2026/03/xkcd-2347-dependency.png" style="width: 50%;" /&gt;
&lt;/a&gt;
&lt;p&gt;&lt;em&gt;xkcd 2347: Dependency by Randall Munroe (&lt;/em&gt;&lt;a class="reference external" href="https://xkcd.com/2347/"&gt;original&lt;/a&gt;&lt;em&gt;)&lt;/em&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;?xml version="1.0" encoding="UTF-8" standalone="no"?&gt;
&lt;!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"&gt;
&lt;svg width="100%" height="100%"
 viewBox="0.00 0.00 530.00 439.24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"&gt;
&lt;g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 435.24)"&gt;

&lt;!-- d0: cluster box --&gt;
&lt;g class="anim d0"&gt;
&lt;polygon fill="transparent" stroke="#999999" stroke-dasharray="5,2" points="8,-350.24 8,-423.24 514,-423.24 514,-350.24 8,-350.24"/&gt;
&lt;text text-anchor="middle" x="261" y="-409.64" font-family="Helvetica Bold" font-size="12.00" fill="#cccccc"&gt;&amp;quot;our&amp;quot; framework&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d1: PyPI --&gt;
&lt;g class="anim d1"&gt;
&lt;path fill="#3775a9" stroke="#999999" d="M58,-394.24C58,-394.24 28,-394.24 28,-394.24 22,-394.24 16,-388.24 16,-382.24 16,-382.24 16,-370.24 16,-370.24 16,-364.24 22,-358.24 28,-358.24 28,-358.24 58,-358.24 58,-358.24 64,-358.24 70,-364.24 70,-370.24 70,-370.24 70,-382.24 70,-382.24 70,-388.24 64,-394.24 58,-394.24"/&gt;
&lt;text text-anchor="middle" x="43" y="-373.14" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;PyPI&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d2: RhodeCode --&gt;
&lt;g class="anim d2"&gt;
&lt;path fill="#259bce" stroke="#999999" d="M162,-394.24C162,-394.24 100,-394.24 100,-394.24 94,-394.24 88,-388.24 88,-382.24 88,-382.24 88,-370.24 88,-370.24 88,-364.24 94,-358.24 100,-358.24 100,-358.24 162,-358.24 162,-358.24 168,-358.24 174,-364.24 174,-370.24 174,-370.24 174,-382.24 174,-382.24 174,-388.24 168,-394.24 162,-394.24"/&gt;
&lt;text text-anchor="middle" x="131" y="-373.14" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;RhodeCode&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d3: Remarkbox --&gt;
&lt;g class="anim d3"&gt;
&lt;path fill="#6c5ce7" stroke="#999999" d="M264,-394.24C264,-394.24 204,-394.24 204,-394.24 198,-394.24 192,-388.24 192,-382.24 192,-382.24 192,-370.24 192,-370.24 192,-364.24 198,-358.24 204,-358.24 204,-358.24 264,-358.24 264,-358.24 270,-358.24 276,-364.24 276,-370.24 276,-370.24 276,-382.24 276,-382.24 276,-388.24 270,-394.24 264,-394.24"/&gt;
&lt;text text-anchor="middle" x="234" y="-373.14" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;Remarkbox&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d4: MakePostSell --&gt;
&lt;g class="anim d4"&gt;
&lt;path fill="#00897b" stroke="#999999" d="M377.5,-394.24C377.5,-394.24 306.5,-394.24 306.5,-394.24 300.5,-394.24 294.5,-388.24 294.5,-382.24 294.5,-382.24 294.5,-370.24 294.5,-370.24 294.5,-364.24 300.5,-358.24 306.5,-358.24 306.5,-358.24 377.5,-358.24 377.5,-358.24 383.5,-358.24 389.5,-364.24 389.5,-370.24 389.5,-370.24 389.5,-382.24 389.5,-382.24 389.5,-388.24 383.5,-394.24 377.5,-394.24"/&gt;
&lt;text text-anchor="middle" x="342" y="-373.14" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;MakePostSell&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d5: thousands of Pyramid apps --&gt;
&lt;g class="anim d5"&gt;
&lt;path fill="#546e7a" stroke="#999999" d="M494,-394.24C494,-394.24 420,-394.24 420,-394.24 414,-394.24 408,-388.24 408,-382.24 408,-382.24 408,-370.24 408,-370.24 408,-364.24 414,-358.24 420,-358.24 420,-358.24 494,-358.24 494,-358.24 500,-358.24 506,-364.24 506,-370.24 506,-370.24 506,-382.24 506,-382.24 506,-388.24 500,-394.24 494,-394.24"/&gt;
&lt;text text-anchor="middle" x="457" y="-379.64" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;thousands of&lt;/text&gt;
&lt;text text-anchor="middle" x="457" y="-366.64" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;Pyramid apps&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d6: Pylons org node + all edges --&gt;
&lt;g class="anim d6"&gt;
&lt;path fill="#bf360c" stroke="#999999" d="M316.5,-321.24C316.5,-321.24 151.5,-321.24 151.5,-321.24 145.5,-321.24 139.5,-315.24 139.5,-309.24 139.5,-309.24 139.5,-283.24 139.5,-283.24 139.5,-277.24 145.5,-271.24 151.5,-271.24 151.5,-271.24 316.5,-271.24 316.5,-271.24 322.5,-271.24 328.5,-277.24 328.5,-283.24 328.5,-283.24 328.5,-309.24 328.5,-309.24 328.5,-315.24 322.5,-321.24 316.5,-321.24"/&gt;
&lt;text text-anchor="middle" x="234" y="-306.84" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;Pylons org: 98 repos&lt;/text&gt;
&lt;text text-anchor="middle" x="234" y="-292.84" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;28 lifetime contributors&lt;/text&gt;
&lt;text text-anchor="middle" x="234" y="-278.84" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;4 humans active last 3mo&lt;/text&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M65.65,-358.11C69.95,-355.26 74.52,-352.5 79,-350.24 99.38,-339.93 122.24,-330.87 144,-323.29"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="144.81,-325.23 149.81,-321.3 143.45,-321.26 144.81,-325.23"/&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M153.6,-358.12C166.37,-348.45 182.65,-336.13 197.21,-325.1"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="198.56,-326.71 202.07,-321.42 196.02,-323.36 198.56,-326.71"/&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M234,-357.93C234,-349.01 234,-337.87 234,-327.57"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="236.1,-327.3 234,-321.3 231.9,-327.3 236.1,-327.3"/&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M318.3,-358.12C304.91,-348.45 287.85,-336.13 272.58,-325.1"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="273.57,-323.23 267.48,-321.42 271.11,-326.63 273.57,-323.23"/&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M418.84,-358.17C412.26,-355.41 405.47,-352.66 399,-350.24 373.6,-340.71 345.61,-331.32 320.02,-323.15"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="320.62,-321.13 314.26,-321.31 319.34,-325.13 320.62,-321.13"/&gt;
&lt;/g&gt;

&lt;!-- d7: Russell node + xkcd edge --&gt;
&lt;g class="anim d7"&gt;
&lt;ellipse fill="#e65100" stroke="#999999" cx="234" cy="-138.62" rx="107.46" ry="51.74"/&gt;
&lt;text text-anchor="middle" x="234" y="-161.52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;Russell&lt;/text&gt;
&lt;text text-anchor="middle" x="234" y="-148.52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;unemployed volunteer&lt;/text&gt;
&lt;text text-anchor="middle" x="234" y="-135.52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;only new contributor&lt;/text&gt;
&lt;text text-anchor="middle" x="234" y="-122.52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;in the past year&lt;/text&gt;
&lt;text text-anchor="middle" x="234" y="-109.52" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;PR #3805&lt;/text&gt;
&lt;path fill="none" stroke="#ef5350" stroke-width="2" d="M234,-270.92C234,-251.06 234,-222.18 234,-196.56"/&gt;
&lt;polygon fill="#ef5350" stroke="#ef5350" stroke-width="2" points="236.1,-196.47 234,-190.47 231.9,-196.47 236.1,-196.47"/&gt;
&lt;text text-anchor="middle" x="283.5" y="-242.04" font-family="Times,serif" font-size="14.00" fill="#ef5350"&gt; &amp;#160;xkcd 2347&lt;/text&gt;
&lt;text text-anchor="middle" x="283.5" y="-227.04" font-family="Times,serif" font-size="14.00" fill="#ef5350"&gt; &amp;#160;load&amp;#45;bearing&lt;/text&gt;
&lt;text text-anchor="middle" x="283.5" y="-212.04" font-family="Times,serif" font-size="14.00" fill="#ef5350"&gt; &amp;#160;block&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d8: NASA + no funding edge --&gt;
&lt;g class="anim d8"&gt;
&lt;polygon fill="#b71c1c" stroke="#999999" points="288,-36 174,-36 174,0 294,0 294,-30 288,-36"/&gt;
&lt;polyline fill="none" stroke="#999999" points="288,-36 288,-30 "/&gt;
&lt;polyline fill="none" stroke="#999999" points="294,-30 288,-30 "/&gt;
&lt;text text-anchor="middle" x="234" y="-21.4" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;NASA shut down&lt;/text&gt;
&lt;text text-anchor="middle" x="234" y="-8.4" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;grants evaporate&lt;/text&gt;
&lt;path fill="none" stroke="#ef5350" stroke-dasharray="5,2" d="M234,-86.97C234,-71.53 234,-55.28 234,-42.55"/&gt;
&lt;polygon fill="#ef5350" stroke="#ef5350" points="236.1,-42.23 234,-36.23 231.9,-42.23 236.1,-42.23"/&gt;
&lt;text text-anchor="middle" x="277" y="-57.8" font-family="Times,serif" font-size="14.00" fill="#ef5350"&gt; &amp;#160;no funding&lt;/text&gt;
&lt;/g&gt;

&lt;/g&gt;
&lt;/svg&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;I wrote a script called &lt;tt class="docutils literal"&gt;unbury&lt;/tt&gt; to exhume contributor stats from a graveyard of git repos. Run it against the &lt;a class="reference external" href="https://github.com/Pylons"&gt;Pylons&lt;/a&gt; org &amp;amp; the numbers speak for themselves:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;scanning 98 repos in ~/git/pylons

──────────────────────────────────────────────────
3 months: 5 unique contributors across 5 active repos
  russell@unturf.com               1 repos
  dependabot[bot]                  1 repos
  Michael Merickel                 1 repos
  Rémi Dubois                      1 repos
  Gael Pasgrimaud                  1 repos
──────────────────────────────────────────────────
1 year: 7 unique contributors across 8 active repos
──────────────────────────────────────────────────
3 years: 15 unique contributors across 31 active repos
──────────────────────────────────────────────────
6 years: 19 unique contributors across 46 active repos
──────────────────────────────────────────────────
lifetime: 28 unique contributors across 97 active repos
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Five contributors in 3 months. One of them a bot. I stepped up to help maintain Pyramid. Unemployed. Filed &lt;a class="reference external" href="https://github.com/Pylons/pyramid/pull/3805"&gt;PR #3805&lt;/a&gt; to vendor &lt;tt class="docutils literal"&gt;pkg_resources&lt;/tt&gt; &amp;amp; drop a setuptools runtime dependency. 310 lines vendored from setuptools 80.x. 67 new tests at 97.66% coverage. A &lt;tt class="docutils literal"&gt;pyramid.compat_resources&lt;/tt&gt; public API so downstream packages could migrate without breaking asset overrides. Audited all 108 Pylons repos. Migrated 30 downstream packages. Ran every test suite. One reviewer approved. The maintainer who holds commit access shipped Pyramid 2.1 with a setuptools version constraint instead &amp;amp; went -1 on the approach. &lt;a class="reference external" href="https://russell.ballestrini.net/tips-for-getting-pull-requests-approved/"&gt;Rejected&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So I closed the PR, opened a new terminal, &amp;amp; pivoted. If &amp;quot;we&amp;quot; won't vendor the fix upstream, &amp;quot;I&amp;quot; will do the lifting downstream. Migrated 33 repos in a single session. Replaced &lt;tt class="docutils literal"&gt;pkg_resources&lt;/tt&gt; with &lt;tt class="docutils literal"&gt;importlib.resources&lt;/tt&gt; &amp;amp; &lt;tt class="docutils literal"&gt;importlib.metadata&lt;/tt&gt; across every Pylons package that needed it. Remarkbox. MakePostSell. RhodeCode. Nine Pyramid-dependent packages. Nineteen &lt;tt class="docutils literal"&gt;docs/conf.py&lt;/tt&gt; files. Substanced's &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;EntryPoint.parse().load()&lt;/span&gt;&lt;/tt&gt; hack. Deform's cursed module-level &lt;tt class="docutils literal"&gt;resource_filename&lt;/tt&gt; call that fails at import time. All uncommitted, waiting to fork &amp;amp; push across the org.&lt;/p&gt;
&lt;p&gt;A rehydrated contributor. Not new to Pyramid or open source communities. Employment interrupted the hacking, not the other way around. A codebase doesn't care about gaps, just commits. Because someone has to play &lt;a class="reference external" href="https://xkcd.com/2347/"&gt;that xkcd 2347 role&lt;/a&gt;, a single load-bearing block propping up everything above it. &amp;quot;Our&amp;quot; framework means I volunteer so &amp;quot;we&amp;quot; can keep a foundation standing. Maybe eventually get paid. Maybe not. NASA just shut down. Federal grants evaporate. &amp;quot;Our&amp;quot; community runs on donated labor &amp;amp; borrowed time.&lt;/p&gt;
&lt;p&gt;But hope remains. Pyramid still ships solid software. A community can rally around &lt;a class="reference external" href="https://discord.gg/n7uzzsm6"&gt;new releases &amp;amp; sprints&lt;/a&gt;. Rehydrated hackers return. Fresh contributors show up. Every PR filed says someone still cares enough to read a codebase &amp;amp; propose a fix. &amp;quot;Our&amp;quot; framework earns that pronoun when enough hands grab hold again.&lt;/p&gt;
&lt;p&gt;Here sits &lt;tt class="docutils literal"&gt;unbury&lt;/tt&gt; in full (&lt;a class="reference external" href="/uploads/2026/03/unbury.py"&gt;download&lt;/a&gt;). A script to exhume contributor stats from a graveyard of git repos:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="ch"&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;unbury - exhume contributor stats from a graveyard of git repos.&lt;/span&gt;

&lt;span class="sd"&gt;Usage:&lt;/span&gt;
&lt;span class="sd"&gt;    unbury [path]           # defaults to current directory&lt;/span&gt;
&lt;span class="sd"&gt;    unbury /home/fox/git/pylons&lt;/span&gt;
&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="n"&gt;PERIODS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;3 months&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;1 year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;3 years&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;6 years&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;18 years&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lifetime&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;git_contributors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Return set of (name, email) tuples for a repo.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;git&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-C&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;log&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;--format=&lt;/span&gt;&lt;span class="si"&gt;%a&lt;/span&gt;&lt;span class="s2"&gt;N&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="si"&gt;%a&lt;/span&gt;&lt;span class="s2"&gt;E&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;--since=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;contributors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;contributors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;contributors&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeoutExpired&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_repos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Find all git repos (immediate subdirs with .git).&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;repos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.git&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="n"&gt;repos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;repos&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getcwd&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;abspath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;repos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_repos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;no git repos found in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;scanning &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; repos in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PERIODS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;since&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;%Y-%m-&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;all_contributors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;repo_counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;contribs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;git_contributors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;contribs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;repo_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;repo_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contribs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;all_contributors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contribs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;active_repos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;─&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_contributors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; unique contributors&amp;quot;&lt;/span&gt;
              &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot; across &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;active_repos&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; active repos&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;all_contributors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;person_repos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;contribs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;git_contributors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;repo_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;contribs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;person_repos&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;person_repos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;touched&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;30s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;touched&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;3d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; repos  (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;  ... and &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; more&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;─&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;?xml version="1.0" encoding="UTF-8" standalone="no"?&gt;
&lt;!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"&gt;
&lt;!-- Generated by graphviz version 2.43.0 (0)
 --&gt;
&lt;!-- Title: contributor_decay Pages: 1 --&gt;
&lt;svg width="100%" height="100%"
 viewBox="0.00 0.00 211.00 671.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"&gt;
&lt;g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 667)"&gt;
&lt;g class="anim d0"&gt;
&lt;title&gt;contributor_decay&lt;/title&gt;
&lt;/g&gt;
&lt;g class="anim d1"&gt;
&lt;g id="clust1" class="cluster"&gt;
&lt;title&gt;cluster_time&lt;/title&gt;
&lt;polygon fill="transparent" stroke="#999999" stroke-dasharray="5,2" points="8,-87 8,-655 195,-655 195,-87 8,-87"/&gt;
&lt;text text-anchor="middle" x="101.5" y="-641.4" font-family="Helvetica Bold" font-size="12.00" fill="#cccccc"&gt;Pylons org: contributor decay&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- lifetime --&gt;
&lt;g class="anim d3"&gt;
&lt;g id="node1" class="node"&gt;
&lt;title&gt;lifetime&lt;/title&gt;
&lt;path fill="#1b5e20" stroke="#999999" d="M125.5,-626C125.5,-626 72.5,-626 72.5,-626 66.5,-626 60.5,-620 60.5,-614 60.5,-614 60.5,-594 60.5,-594 60.5,-588 66.5,-582 72.5,-582 72.5,-582 125.5,-582 125.5,-582 131.5,-582 137.5,-588 137.5,-594 137.5,-594 137.5,-614 137.5,-614 137.5,-620 131.5,-626 125.5,-626"/&gt;
&lt;text text-anchor="middle" x="99" y="-613.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;lifetime&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-601.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;28 humans&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-589.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;97 repos&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- y18 --&gt;
&lt;g class="anim d5"&gt;
&lt;g id="node2" class="node"&gt;
&lt;title&gt;y18&lt;/title&gt;
&lt;path fill="#2e7d32" stroke="#999999" d="M125.5,-531C125.5,-531 72.5,-531 72.5,-531 66.5,-531 60.5,-525 60.5,-519 60.5,-519 60.5,-499 60.5,-499 60.5,-493 66.5,-487 72.5,-487 72.5,-487 125.5,-487 125.5,-487 131.5,-487 137.5,-493 137.5,-499 137.5,-499 137.5,-519 137.5,-519 137.5,-525 131.5,-531 125.5,-531"/&gt;
&lt;text text-anchor="middle" x="99" y="-518.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;18 years&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-506.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;28 humans&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-494.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;97 repos&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- lifetime&amp;#45;&amp;gt;y18 --&gt;
&lt;g class="anim d7"&gt;
&lt;g id="edge1" class="edge"&gt;
&lt;title&gt;lifetime&amp;#45;&amp;gt;y18&lt;/title&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M99,-581.9C99,-568.34 99,-550.63 99,-536.18"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="100.75,-536.02 99,-531.02 97.25,-536.02 100.75,-536.02"/&gt;
&lt;text text-anchor="middle" x="120.5" y="-552.8" font-family="Times,serif" font-size="14.00" fill="#cccccc"&gt; same&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- y6 --&gt;
&lt;g class="anim d9"&gt;
&lt;g id="node3" class="node"&gt;
&lt;title&gt;y6&lt;/title&gt;
&lt;path fill="#0d47a1" stroke="#999999" d="M125.5,-436C125.5,-436 72.5,-436 72.5,-436 66.5,-436 60.5,-430 60.5,-424 60.5,-424 60.5,-404 60.5,-404 60.5,-398 66.5,-392 72.5,-392 72.5,-392 125.5,-392 125.5,-392 131.5,-392 137.5,-398 137.5,-404 137.5,-404 137.5,-424 137.5,-424 137.5,-430 131.5,-436 125.5,-436"/&gt;
&lt;text text-anchor="middle" x="99" y="-423.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;6 years&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-411.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;19 humans&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-399.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;46 repos&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- y18&amp;#45;&amp;gt;y6 --&gt;
&lt;g class="anim d11"&gt;
&lt;g id="edge2" class="edge"&gt;
&lt;title&gt;y18&amp;#45;&amp;gt;y6&lt;/title&gt;
&lt;path fill="none" stroke="#ef5350" d="M99,-486.9C99,-473.34 99,-455.63 99,-441.18"/&gt;
&lt;polygon fill="#ef5350" stroke="#ef5350" points="100.75,-441.02 99,-436.02 97.25,-441.02 100.75,-441.02"/&gt;
&lt;text text-anchor="middle" x="108.5" y="-457.8" font-family="Times,serif" font-size="14.00" fill="#ef5350"&gt; &amp;#45;9&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- y3 --&gt;
&lt;g class="anim d13"&gt;
&lt;g id="node4" class="node"&gt;
&lt;title&gt;y3&lt;/title&gt;
&lt;path fill="#1565c0" stroke="#999999" d="M125.5,-341C125.5,-341 72.5,-341 72.5,-341 66.5,-341 60.5,-335 60.5,-329 60.5,-329 60.5,-309 60.5,-309 60.5,-303 66.5,-297 72.5,-297 72.5,-297 125.5,-297 125.5,-297 131.5,-297 137.5,-303 137.5,-309 137.5,-309 137.5,-329 137.5,-329 137.5,-335 131.5,-341 125.5,-341"/&gt;
&lt;text text-anchor="middle" x="99" y="-328.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;3 years&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-316.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;15 humans&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-304.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;31 repos&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- y6&amp;#45;&amp;gt;y3 --&gt;
&lt;g class="anim d15"&gt;
&lt;g id="edge3" class="edge"&gt;
&lt;title&gt;y6&amp;#45;&amp;gt;y3&lt;/title&gt;
&lt;path fill="none" stroke="#ef5350" d="M99,-391.9C99,-378.34 99,-360.63 99,-346.18"/&gt;
&lt;polygon fill="#ef5350" stroke="#ef5350" points="100.75,-346.02 99,-341.02 97.25,-346.02 100.75,-346.02"/&gt;
&lt;text text-anchor="middle" x="108.5" y="-362.8" font-family="Times,serif" font-size="14.00" fill="#ef5350"&gt; &amp;#45;4&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- y1 --&gt;
&lt;g class="anim d17"&gt;
&lt;g id="node5" class="node"&gt;
&lt;title&gt;y1&lt;/title&gt;
&lt;path fill="#6a1b9a" stroke="#999999" d="M122.5,-246C122.5,-246 75.5,-246 75.5,-246 69.5,-246 63.5,-240 63.5,-234 63.5,-234 63.5,-214 63.5,-214 63.5,-208 69.5,-202 75.5,-202 75.5,-202 122.5,-202 122.5,-202 128.5,-202 134.5,-208 134.5,-214 134.5,-214 134.5,-234 134.5,-234 134.5,-240 128.5,-246 122.5,-246"/&gt;
&lt;text text-anchor="middle" x="99" y="-233.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;1 year&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-221.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;7 humans&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-209.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;8 repos&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- y3&amp;#45;&amp;gt;y1 --&gt;
&lt;g class="anim d19"&gt;
&lt;g id="edge4" class="edge"&gt;
&lt;title&gt;y3&amp;#45;&amp;gt;y1&lt;/title&gt;
&lt;path fill="none" stroke="#ef5350" d="M99,-296.9C99,-283.34 99,-265.63 99,-251.18"/&gt;
&lt;polygon fill="#ef5350" stroke="#ef5350" points="100.75,-251.02 99,-246.02 97.25,-251.02 100.75,-251.02"/&gt;
&lt;text text-anchor="middle" x="108.5" y="-267.8" font-family="Times,serif" font-size="14.00" fill="#ef5350"&gt; &amp;#45;8&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- m3 --&gt;
&lt;g class="anim d21"&gt;
&lt;g id="node6" class="node"&gt;
&lt;title&gt;m3&lt;/title&gt;
&lt;path fill="#bf360c" stroke="#999999" d="M122.5,-151C122.5,-151 75.5,-151 75.5,-151 69.5,-151 63.5,-145 63.5,-139 63.5,-139 63.5,-107 63.5,-107 63.5,-101 69.5,-95 75.5,-95 75.5,-95 122.5,-95 122.5,-95 128.5,-95 134.5,-101 134.5,-107 134.5,-107 134.5,-139 134.5,-139 134.5,-145 128.5,-151 122.5,-151"/&gt;
&lt;text text-anchor="middle" x="99" y="-138.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;3 months&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-126.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;4 humans&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-114.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;+ 1 bot&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-102.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;5 repos&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- y1&amp;#45;&amp;gt;m3 --&gt;
&lt;g class="anim d23"&gt;
&lt;g id="edge5" class="edge"&gt;
&lt;title&gt;y1&amp;#45;&amp;gt;m3&lt;/title&gt;
&lt;path fill="none" stroke="#ef5350" d="M99,-201.52C99,-188.35 99,-171.24 99,-156.44"/&gt;
&lt;polygon fill="#ef5350" stroke="#ef5350" points="100.75,-156.11 99,-151.11 97.25,-156.11 100.75,-156.11"/&gt;
&lt;text text-anchor="middle" x="108.5" y="-172.8" font-family="Times,serif" font-size="14.00" fill="#ef5350"&gt; &amp;#45;3&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- no_bdfl --&gt;
&lt;g class="anim d25"&gt;
&lt;g id="node7" class="node"&gt;
&lt;title&gt;no_bdfl&lt;/title&gt;
&lt;polygon fill="#4e342e" stroke="#999999" points="163,-44 29,-44 29,0 169,0 169,-38 163,-44"/&gt;
&lt;polyline fill="none" stroke="#999999" points="163,-44 163,-38 "/&gt;
&lt;polyline fill="none" stroke="#999999" points="169,-38 163,-38 "/&gt;
&lt;text text-anchor="middle" x="99" y="-31.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;no BDFL&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-19.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;no formal general&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-7.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="white"&gt;snowflake governance&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- m3&amp;#45;&amp;gt;no_bdfl --&gt;
&lt;g class="anim d27"&gt;
&lt;g id="edge6" class="edge"&gt;
&lt;title&gt;m3&amp;#45;&amp;gt;no_bdfl&lt;/title&gt;
&lt;path fill="none" stroke="#ef5350" stroke-dasharray="5,2" d="M99,-94.95C99,-80.67 99,-63.23 99,-49.1"/&gt;
&lt;polygon fill="#ef5350" stroke="#ef5350" points="100.75,-49.06 99,-44.06 97.25,-49.06 100.75,-49.06"/&gt;
&lt;text text-anchor="middle" x="122.5" y="-65.8" font-family="Times,serif" font-size="14.00" fill="#ef5350"&gt; result&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;/svg&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Pyramid sits as a microcosm of open source in general. Every org looks like this. 28 humans across a lifetime. 4 active today. No BDFL. No benevolent dictator for life. No formal general directing strategy. Just snowflake governance. Each org a unique crystal of informal agreements, stale GOVERNANCE.md files, &amp;amp; whoever still answers email. Python had Guido. Linux has Linus. Pyramid has nobody. Most open source projects have nobody. They drift on inertia &amp;amp; the stubbornness of whoever remains.&lt;/p&gt;
&lt;p&gt;&amp;quot;Our&amp;quot; framework. &amp;quot;Our&amp;quot; community. &amp;quot;Our&amp;quot; roadmap. Strip the pronoun &amp;amp; look at what remains. A handful of volunteers. A bot. Some stale CI configs. The word &amp;quot;our&amp;quot; papers over the structural void where leadership should live. Without a BDFL, &amp;quot;our&amp;quot; becomes a polite fiction. Nobody owns it. Nobody steers it. Everybody claims it.&lt;/p&gt;
&lt;p&gt;I catch myself writing &amp;quot;our&amp;quot; in &lt;a class="reference external" href="https://unturf.com"&gt;unturf&lt;/a&gt; docs. &amp;quot;Our permacomputer approach.&amp;quot; Whose? Mine? Fox's? A philosophy doesn't belong to anyone. &amp;quot;A permacomputer approach&amp;quot; works better. Drops possession entirely. Lets an idea stand on its own merit instead of borrowed authority.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="one"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;one&lt;/a&gt;&lt;/h2&gt;
&lt;?xml version="1.0" encoding="UTF-8" standalone="no"?&gt;
&lt;!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"&gt;
&lt;svg width="100%" height="100%"
 viewBox="0.00 0.00 400.50 705.82" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"&gt;
&lt;g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 701.82)"&gt;

&lt;!-- cluster: pretends to contain --&gt;
&lt;g class="anim d1"&gt;
&lt;polygon fill="transparent" stroke="#999999" stroke-dasharray="5,2" points="8,-533.91 8,-620.82 162,-620.82 162,-533.91 8,-533.91"/&gt;
&lt;text text-anchor="middle" x="85" y="-608.02" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#cccccc"&gt;pretends to contain&lt;/text&gt;
&lt;/g&gt;

&lt;!-- cluster: actually contains --&gt;
&lt;g class="anim d2"&gt;
&lt;polygon fill="transparent" stroke="#999999" stroke-dasharray="5,2" points="170,-8 170,-613.37 318,-613.37 318,-8 170,-8"/&gt;
&lt;text text-anchor="middle" x="244" y="-600.57" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#cccccc"&gt;actually contains&lt;/text&gt;
&lt;/g&gt;

&lt;!-- "one" node --&gt;
&lt;g class="anim d0"&gt;
&lt;path fill="#9370db" stroke="#999999" d="M166,-697.82C166,-697.82 128,-697.82 128,-697.82 122,-697.82 116,-691.82 116,-685.82 116,-685.82 116,-673.82 116,-673.82 116,-667.82 122,-661.82 128,-661.82 128,-661.82 166,-661.82 166,-661.82 172,-661.82 178,-667.82 178,-673.82 178,-673.82 178,-685.82 178,-685.82 178,-691.82 172,-697.82 166,-697.82"/&gt;
&lt;text text-anchor="middle" x="147" y="-676.02" font-family="Helvetica,sans-Serif" font-size="16.00" fill="white"&gt;&amp;quot;one&amp;quot;&lt;/text&gt;
&lt;/g&gt;

&lt;!-- singular node --&gt;
&lt;g class="anim d1"&gt;
&lt;ellipse fill="#616161" stroke="#999999" cx="85" cy="-567.37" rx="68.68" ry="25.41"/&gt;
&lt;text text-anchor="middle" x="85" y="-570.97" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;a single&lt;/text&gt;
&lt;text text-anchor="middle" x="85" y="-556.97" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;isolated self&lt;/text&gt;
&lt;/g&gt;

&lt;!-- one -&gt; singular (claims) --&gt;
&lt;g class="anim d1"&gt;
&lt;path fill="none" stroke="#ef5350" stroke-dasharray="5,2" d="M129.67,-661.58C124.84,-656.19 119.86,-650.02 116,-643.82 107.44,-630.09 100.34,-613.57 95.11,-599.5"/&gt;
&lt;polygon fill="#ef5350" stroke="#ef5350" points="97.38,-598.57 92.69,-592.82 92.77,-600.23 97.38,-598.57"/&gt;
&lt;text text-anchor="middle" x="143.5" y="-632.62" font-family="Times,serif" font-size="14.00" fill="#ef5350"&gt;claims&lt;/text&gt;
&lt;/g&gt;

&lt;!-- stardust --&gt;
&lt;g class="anim d2"&gt;
&lt;ellipse fill="#f9a825" stroke="#999999" cx="224" cy="-567.37" rx="44.7" ry="18"/&gt;
&lt;text text-anchor="middle" x="224" y="-563.97" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;stardust&lt;/text&gt;
&lt;/g&gt;

&lt;!-- one -&gt; stardust (holds) --&gt;
&lt;g class="anim d2"&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M159.03,-661.56C172.35,-642.46 193.86,-611.6 208.45,-590.67"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="210.56,-591.93 212.56,-584.78 206.54,-589.13 210.56,-591.93"/&gt;
&lt;text text-anchor="middle" x="205" y="-632.62" font-family="Times,serif" font-size="14.00" fill="#cccccc"&gt;holds&lt;/text&gt;
&lt;/g&gt;

&lt;!-- photons --&gt;
&lt;g class="anim d3"&gt;
&lt;ellipse fill="#fdd835" stroke="#999999" cx="229" cy="-486.91" rx="44.44" ry="18"/&gt;
&lt;text text-anchor="middle" x="229" y="-483.51" font-family="Helvetica,sans-Serif" font-size="13.00" fill="#1a1a1a"&gt;photons&lt;/text&gt;
&lt;/g&gt;

&lt;!-- quantum fields --&gt;
&lt;g class="anim d4"&gt;
&lt;ellipse fill="#7cb342" stroke="#999999" cx="239" cy="-406.46" rx="53.98" ry="25.41"/&gt;
&lt;text text-anchor="middle" x="239" y="-410.06" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;quantum&lt;/text&gt;
&lt;text text-anchor="middle" x="239" y="-396.06" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;fields&lt;/text&gt;
&lt;/g&gt;

&lt;!-- atoms + edge from quantum --&gt;
&lt;g class="anim d5"&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M239.32,-380.63C239.44,-371.28 239.57,-360.66 239.69,-351.35"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="242.14,-351.25 239.78,-344.22 237.24,-351.19 242.14,-351.25"/&gt;
&lt;ellipse fill="#00897b" stroke="#999999" cx="240" cy="-326" rx="36.77" ry="18"/&gt;
&lt;text text-anchor="middle" x="240" y="-322.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;atoms&lt;/text&gt;
&lt;/g&gt;

&lt;!-- mitochondria + edge from atoms --&gt;
&lt;g class="anim d6"&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M240.97,-307.81C241.47,-298.92 242.09,-287.91 242.64,-278.17"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="245.09,-278.16 243.04,-271.03 240.2,-277.88 245.09,-278.16"/&gt;
&lt;ellipse fill="#00acc1" stroke="#999999" cx="244" cy="-253" rx="65.89" ry="18"/&gt;
&lt;text text-anchor="middle" x="244" y="-249.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;mitochondria&lt;/text&gt;
&lt;/g&gt;

&lt;!-- cells + edge from mito --&gt;
&lt;g class="anim d7"&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M244,-234.81C244,-225.92 244,-214.91 244,-205.17"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="246.45,-205.03 244,-198.03 241.55,-205.03 246.45,-205.03"/&gt;
&lt;ellipse fill="#1e88e5" stroke="#999999" cx="244" cy="-180" rx="29.88" ry="18"/&gt;
&lt;text text-anchor="middle" x="244" y="-176.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;cells&lt;/text&gt;
&lt;/g&gt;

&lt;!-- organs + edge from cells --&gt;
&lt;g class="anim d8"&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M244,-161.81C244,-152.92 244,-141.91 244,-132.17"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="246.45,-132.03 244,-125.03 241.55,-132.03 246.45,-132.03"/&gt;
&lt;ellipse fill="#5c6bc0" stroke="#999999" cx="244" cy="-107" rx="39.33" ry="18"/&gt;
&lt;text text-anchor="middle" x="244" y="-103.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;organs&lt;/text&gt;
&lt;/g&gt;

&lt;!-- multitudes + edge from organs --&gt;
&lt;g class="anim d9"&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M244,-88.81C244,-79.92 244,-68.91 244,-59.17"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="246.45,-59.03 244,-52.03 241.55,-59.03 246.45,-59.03"/&gt;
&lt;ellipse fill="#ab47bc" stroke="#999999" cx="244" cy="-34" rx="54.91" ry="18"/&gt;
&lt;text text-anchor="middle" x="244" y="-30.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="white"&gt;multitudes&lt;/text&gt;
&lt;/g&gt;

&lt;!-- as above / so below --&gt;
&lt;g class="anim d10"&gt;
&lt;polygon fill="transparent" stroke="transparent" points="392.5,-697.82 325.5,-697.82 325.5,-661.82 392.5,-661.82 392.5,-697.82"/&gt;
&lt;text text-anchor="middle" x="359" y="-677.02" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#cccccc"&gt;as above&lt;/text&gt;
&lt;polygon fill="transparent" stroke="transparent" points="391.5,-585.37 326.5,-585.37 326.5,-549.37 391.5,-549.37 391.5,-585.37"/&gt;
&lt;text text-anchor="middle" x="359" y="-564.57" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#cccccc"&gt;so below&lt;/text&gt;
&lt;path fill="none" stroke="#aaaaaa" stroke-dasharray="1,5" d="M359,-654.29C359,-636.02 359,-611.17 359,-592.9"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="356.55,-654.56 359,-661.56 361.45,-654.56 356.55,-654.56"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="361.45,-592.51 359,-585.51 356.55,-592.51 361.45,-592.51"/&gt;
&lt;/g&gt;

&lt;/g&gt;
&lt;/svg&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&amp;quot;One&amp;quot; tries to sound objective &amp;amp; instead sounds like a Victorian butler. &amp;quot;One might consider using a virtual environment.&amp;quot; One might. One also might speak normally. &amp;quot;One should avoid mutable default arguments in Python.&amp;quot; You should avoid mutable default arguments in Python. See? Same information. Half a pretension.&lt;/p&gt;
&lt;p&gt;&amp;quot;One&amp;quot; might also carry a deeper deception. One implies singularity. A single isolated self acting alone. But no such thing exists. Every body holds &lt;a class="reference external" href="https://media.unturf.com/c/30ac7054-8428-11ec-934a-257a83652528/neville-6-11-1959-the-seven-eyes-of-god"&gt;multitudes&lt;/a&gt;. Stardust &amp;amp; photons &amp;amp; quantum fields &amp;amp; atoms &amp;amp; mitochondria &amp;amp; cells &amp;amp; organs. Multitudes within us &amp;amp; without us. As above, so below. &amp;quot;One&amp;quot; pretends separation where only connection lives. Perhaps that makes &amp;quot;one&amp;quot; a Luciferian number. Lucifer's whole deal: separation from source, individuation as rebellion. &amp;quot;One observes&amp;quot; masks a cosmos of collaborators behind a single false pronoun.&lt;/p&gt;
&lt;?xml version="1.0" encoding="UTF-8" standalone="no"?&gt;
&lt;!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"&gt;
&lt;svg width="100%" height="100%"
 viewBox="0.00 0.00 424.00 1563.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"&gt;
&lt;g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1559)"&gt;

&lt;!-- d0: cluster box + separation label + 1 Lucifer --&gt;
&lt;g class="anim d0"&gt;
&lt;polygon fill="transparent" stroke="#999999" stroke-dasharray="5,2" points="29,-673 29,-1492 190,-1492 190,-673 29,-673"/&gt;
&lt;text text-anchor="middle" x="109.5" y="-1479.2" font-family="Helvetica Bold" font-size="11.00" fill="#cccccc"&gt;Neville&amp;#39;s Seven Eyes of God&lt;/text&gt;
&lt;polygon fill="transparent" stroke="transparent" points="178,-1555 106,-1555 106,-1519 178,-1519 178,-1555"/&gt;
&lt;text text-anchor="middle" x="142" y="-1534.5" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#ef5350"&gt;separation&lt;/text&gt;
&lt;path fill="none" stroke="#ef5350" stroke-dasharray="5,2" d="M142,-1518.81C142,-1504.16 142,-1482.63 142,-1464.32"/&gt;
&lt;ellipse fill="#880e4f" stroke="#999999" cx="142" cy="-1428" rx="36" ry="36"/&gt;
&lt;text text-anchor="middle" x="142" y="-1435.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;1&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-1425.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;Lucifer&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-1415.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;fallen&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d1: 2 Molech + edge --&gt;
&lt;g class="anim d1"&gt;
&lt;path fill="none" stroke="#aaaaaa" stroke-width="2" d="M142,-1391.96C142,-1382.17 142,-1371.44 142,-1361.32"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" stroke-width="2" points="144.1,-1361.11 142,-1355.11 139.9,-1361.11 144.1,-1361.11"/&gt;
&lt;ellipse fill="#4a148c" stroke="#999999" cx="142" cy="-1319" rx="36" ry="36"/&gt;
&lt;text text-anchor="middle" x="142" y="-1326.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;2&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-1316.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;Molech&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-1306.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;sacrifice&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d2: 3 Elohim + edge --&gt;
&lt;g class="anim d2"&gt;
&lt;path fill="none" stroke="#aaaaaa" stroke-width="2" d="M142,-1282.73C142,-1273.11 142,-1262.55 142,-1252.46"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" stroke-width="2" points="144.1,-1252.26 142,-1246.26 139.9,-1252.26 144.1,-1252.26"/&gt;
&lt;ellipse fill="#1a237e" stroke="#999999" cx="142" cy="-1206.5" rx="39.5" ry="39.5"/&gt;
&lt;text text-anchor="middle" x="142" y="-1219.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;3&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-1209.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;Elohim&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-1199.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;external&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-1189.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;gods&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d3: 4 Shaddai + edge --&gt;
&lt;g class="anim d3"&gt;
&lt;path fill="none" stroke="#aaaaaa" stroke-width="2" d="M142,-1166.9C142,-1157.06 142,-1146.4 142,-1136.28"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" stroke-width="2" points="144.1,-1136.06 142,-1130.06 139.9,-1136.06 144.1,-1136.06"/&gt;
&lt;ellipse fill="#0d47a1" stroke="#999999" cx="142" cy="-1090.5" rx="39.5" ry="39.5"/&gt;
&lt;text text-anchor="middle" x="142" y="-1103.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;4&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-1093.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;Shaddai&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-1083.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;political&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-1073.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;power&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d4: 5 Pahath + edge --&gt;
&lt;g class="anim d4"&gt;
&lt;path fill="none" stroke="#aaaaaa" stroke-width="2" d="M142,-1050.85C142,-1040.96 142,-1030.28 142,-1020.26"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" stroke-width="2" points="144.1,-1020.11 142,-1014.11 139.9,-1020.11 144.1,-1020.11"/&gt;
&lt;ellipse fill="#006064" stroke="#999999" cx="142" cy="-978" rx="36" ry="36"/&gt;
&lt;text text-anchor="middle" x="142" y="-985.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;5&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-975.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;Pahath&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-965.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;trap&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d5: 6 Jehovah + edge --&gt;
&lt;g class="anim d5"&gt;
&lt;path fill="none" stroke="#aaaaaa" stroke-width="2" d="M142,-941.73C142,-932.11 142,-921.55 142,-911.46"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" stroke-width="2" points="144.1,-911.26 142,-905.26 139.9,-911.26 144.1,-911.26"/&gt;
&lt;ellipse fill="#1b5e20" stroke="#999999" cx="142" cy="-865.5" rx="39.5" ry="39.5"/&gt;
&lt;text text-anchor="middle" x="142" y="-878.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;6&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-868.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;Jehovah&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-858.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;I AM&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-848.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;(YHWH)&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d6: fear node + sacred/unspeakable edge --&gt;
&lt;g class="anim d6"&gt;
&lt;path fill="none" stroke="#ef5350" stroke-dasharray="5,2" d="M171.68,-838.93C196.99,-817.13 233.79,-785.43 262.77,-760.46"/&gt;
&lt;polygon fill="#ef5350" stroke="#ef5350" points="264.18,-762.02 267.36,-756.51 261.44,-758.84 264.18,-762.02"/&gt;
&lt;text text-anchor="middle" x="288.5" y="-796.8" font-family="Times,serif" font-size="14.00" fill="#ef5350"&gt; &amp;#160;sacred&lt;/text&gt;
&lt;text text-anchor="middle" x="288.5" y="-781.8" font-family="Times,serif" font-size="14.00" fill="#ef5350"&gt; &amp;#160;unspeakable&lt;/text&gt;
&lt;path fill="#4e342e" stroke="#999999" d="M404,-756.5C404,-756.5 212,-756.5 212,-756.5 206,-756.5 200,-750.5 200,-744.5 200,-744.5 200,-696.5 200,-696.5 200,-690.5 206,-684.5 212,-684.5 212,-684.5 404,-684.5 404,-684.5 410,-684.5 416,-690.5 416,-696.5 416,-696.5 416,-744.5 416,-744.5 416,-750.5 410,-756.5 404,-756.5"/&gt;
&lt;text text-anchor="middle" x="308" y="-729" font-family="Helvetica,sans-Serif" font-size="10.00" fill="white"&gt;some fear to speak it&lt;/text&gt;
&lt;text text-anchor="middle" x="308" y="-718" font-family="Helvetica,sans-Serif" font-size="10.00" fill="white"&gt;Jehovah&amp;#39;s Witnesses avoid YHWH&lt;/text&gt;
&lt;text text-anchor="middle" x="308" y="-707" font-family="Helvetica,sans-Serif" font-size="10.00" fill="white"&gt;certain Jewish sects forbid I AM&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d7: 7 Jesus + edge from e6 --&gt;
&lt;g class="anim d7"&gt;
&lt;path fill="none" stroke="#aaaaaa" stroke-width="2" d="M142,-825.8C142,-807.52 142,-785.59 142,-766.56"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" stroke-width="2" points="144.1,-766.23 142,-760.23 139.9,-766.23 144.1,-766.23"/&gt;
&lt;ellipse fill="#f9a825" stroke="#999999" cx="142" cy="-720.5" rx="39.5" ry="39.5"/&gt;
&lt;text text-anchor="middle" x="142" y="-733.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#1a1a1a"&gt;7&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-723.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#1a1a1a"&gt;Jesus&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-713.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#1a1a1a"&gt;trinity&lt;/text&gt;
&lt;text text-anchor="middle" x="142" y="-703.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#1a1a1a"&gt;man↔God&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d8: trinity cluster + edges from Jesus --&gt;
&lt;g class="anim d8"&gt;
&lt;polygon fill="transparent" stroke="#999999" stroke-dasharray="5,2" points="8,-569 8,-640 276,-640 276,-569 8,-569"/&gt;
&lt;text text-anchor="middle" x="142" y="-628" font-family="Helvetica Bold" font-size="10.00" fill="#cccccc"&gt;holy signal: binds all layers&lt;/text&gt;
&lt;path fill="#0d47a1" stroke="#999999" d="M76,-613C76,-613 28,-613 28,-613 22,-613 16,-607 16,-601 16,-601 16,-589 16,-589 16,-583 22,-577 28,-577 28,-577 76,-577 76,-577 82,-577 88,-583 88,-589 88,-589 88,-601 88,-601 88,-607 82,-613 76,-613"/&gt;
&lt;text text-anchor="middle" x="52" y="-592.5" font-family="Helvetica,sans-Serif" font-size="10.00" fill="white"&gt;Father&lt;/text&gt;
&lt;path fill="#1b5e20" stroke="#999999" d="M166,-613C166,-613 118,-613 118,-613 112,-613 106,-607 106,-601 106,-601 106,-589 106,-589 106,-583 112,-577 118,-577 118,-577 166,-577 166,-577 172,-577 178,-583 178,-589 178,-589 178,-601 178,-601 178,-607 172,-613 166,-613"/&gt;
&lt;text text-anchor="middle" x="142" y="-592.5" font-family="Helvetica,sans-Serif" font-size="10.00" fill="white"&gt;Son&lt;/text&gt;
&lt;path fill="#6a1b9a" stroke="#999999" d="M256,-613C256,-613 208,-613 208,-613 202,-613 196,-607 196,-601 196,-601 196,-589 196,-589 196,-583 202,-577 208,-577 208,-577 256,-577 256,-577 262,-577 268,-583 268,-589 268,-589 268,-601 268,-601 268,-607 262,-613 256,-613"/&gt;
&lt;text text-anchor="middle" x="232" y="-592.5" font-family="Helvetica,sans-Serif" font-size="10.00" fill="white"&gt;Holy Spirit&lt;/text&gt;
&lt;path fill="none" stroke="#f9a825" stroke-dasharray="5,2" d="M112.81,-693.47C103.58,-684.38 93.76,-673.75 86,-663 76.05,-649.21 67.41,-632.09 61.34,-618.61"/&gt;
&lt;polygon fill="#f9a825" stroke="#f9a825" points="63.24,-617.71 58.89,-613.07 59.39,-619.41 63.24,-617.71"/&gt;
&lt;text text-anchor="middle" x="112" y="-651.8" font-family="Times,serif" font-size="14.00" fill="#f9a825"&gt; unifies&lt;/text&gt;
&lt;path fill="none" stroke="#f9a825" stroke-dasharray="5,2" d="M142,-680.76C142,-660.6 142,-636.62 142,-619.32"/&gt;
&lt;polygon fill="#f9a825" stroke="#f9a825" points="144.1,-619.27 142,-613.27 139.9,-619.27 144.1,-619.27"/&gt;
&lt;path fill="none" stroke="#f9a825" stroke-dasharray="5,2" d="M164.94,-688.02C180.75,-666.33 201.38,-638.02 215.56,-618.57"/&gt;
&lt;polygon fill="#f9a825" stroke="#f9a825" points="217.54,-619.41 219.37,-613.32 214.14,-616.94 217.54,-619.41"/&gt;
&lt;/g&gt;

&lt;!-- d9: layers of consciousness cluster --&gt;
&lt;g class="anim d9"&gt;
&lt;polygon fill="transparent" stroke="#999999" stroke-dasharray="5,2" points="65,-291 65,-536 333,-536 333,-291 65,-291"/&gt;
&lt;text text-anchor="middle" x="199" y="-524" font-family="Helvetica Bold" font-size="10.00" fill="#cccccc"&gt;layers of consciousness&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d10: gut bacteria + signal reaches edge --&gt;
&lt;g class="anim d10"&gt;
&lt;path fill="none" stroke="#6a1b9a" stroke-dasharray="5,2" d="M205.16,-576.91C199.35,-573.84 193.12,-570.99 187,-569 169.76,-563.38 118.01,-572.59 106,-559 95.61,-547.25 96.99,-529.3 100.62,-514.94"/&gt;
&lt;polygon fill="#6a1b9a" stroke="#6a1b9a" points="102.68,-515.38 102.29,-509.04 98.64,-514.24 102.68,-515.38"/&gt;
&lt;text text-anchor="middle" x="160" y="-547.8" font-family="Times,serif" font-size="14.00" fill="#cccccc"&gt; signal reaches&lt;/text&gt;
&lt;path fill="#00897b" stroke="#999999" d="M133,-509C133,-509 85,-509 85,-509 79,-509 73,-503 73,-497 73,-497 73,-485 73,-485 73,-479 79,-473 85,-473 85,-473 133,-473 133,-473 139,-473 145,-479 145,-485 145,-485 145,-497 145,-497 145,-503 139,-509 133,-509"/&gt;
&lt;text text-anchor="middle" x="109" y="-488.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;gut bacteria&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d11: left + right hemispheres + edges from spirit --&gt;
&lt;g class="anim d11"&gt;
&lt;path fill="none" stroke="#6a1b9a" stroke-dasharray="5,2" d="M227.53,-576.78C224.93,-567.09 221.48,-554.81 218,-544 214.91,-534.39 211.15,-523.92 207.8,-514.91"/&gt;
&lt;polygon fill="#6a1b9a" stroke="#6a1b9a" points="209.74,-514.1 205.67,-509.22 205.81,-515.57 209.74,-514.1"/&gt;
&lt;path fill="#546e7a" stroke="#999999" d="M223,-509C223,-509 175,-509 175,-509 169,-509 163,-503 163,-497 163,-497 163,-485 163,-485 163,-479 169,-473 175,-473 175,-473 223,-473 223,-473 229,-473 235,-479 235,-485 235,-485 235,-497 235,-497 235,-503 229,-509 223,-509"/&gt;
&lt;text text-anchor="middle" x="199" y="-493.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;left hemisphere&lt;/text&gt;
&lt;text text-anchor="middle" x="199" y="-483.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;ego&lt;/text&gt;
&lt;path fill="none" stroke="#6a1b9a" stroke-dasharray="5,2" d="M241.67,-576.7C251.23,-559.6 265.9,-533.35 276.37,-514.6"/&gt;
&lt;polygon fill="#6a1b9a" stroke="#6a1b9a" points="278.38,-515.31 279.47,-509.05 274.71,-513.26 278.38,-515.31"/&gt;
&lt;path fill="#546e7a" stroke="#999999" d="M313,-509C313,-509 265,-509 265,-509 259,-509 253,-503 253,-497 253,-497 253,-485 253,-485 253,-479 259,-473 265,-473 265,-473 313,-473 313,-473 319,-473 325,-479 325,-485 325,-485 325,-497 325,-497 325,-503 319,-509 313,-509"/&gt;
&lt;text text-anchor="middle" x="289" y="-493.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;right hemisphere&lt;/text&gt;
&lt;text text-anchor="middle" x="289" y="-483.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;unconscious&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d12: ear worm / brain worm + bicameral split edges --&gt;
&lt;g class="anim d12"&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M189.31,-472.61C185.17,-462.88 182.08,-450.6 186,-440 187.68,-435.46 190.24,-431.11 193.17,-427.12"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="194.94,-428.26 197.04,-422.26 191.66,-425.65 194.94,-428.26"/&gt;
&lt;text text-anchor="middle" x="242" y="-443.8" font-family="Times,serif" font-size="14.00" fill="#cccccc"&gt; bicameral split&lt;/text&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M297.99,-472.54C301.9,-462.31 304.32,-449.57 298,-440 288.96,-426.32 273.36,-417.97 258.18,-412.88"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="258.52,-410.78 252.17,-411.02 257.28,-414.8 258.52,-410.78"/&gt;
&lt;path fill="#4e342e" stroke="#999999" d="M240,-422C240,-422 192,-422 192,-422 186,-422 180,-416 180,-410 180,-410 180,-398 180,-398 180,-392 186,-386 192,-386 192,-386 240,-386 240,-386 246,-386 252,-392 252,-398 252,-398 252,-410 252,-410 252,-416 246,-422 240,-422"/&gt;
&lt;text text-anchor="middle" x="216" y="-406.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;ear worm&lt;/text&gt;
&lt;text text-anchor="middle" x="216" y="-396.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;bicameral mind&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d13: the witness + observed by edge --&gt;
&lt;g class="anim d13"&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M216,-385.8C216,-373.01 216,-355.42 216,-341.36"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="218.1,-341.18 216,-335.18 213.9,-341.18 218.1,-341.18"/&gt;
&lt;text text-anchor="middle" x="262" y="-356.8" font-family="Times,serif" font-size="14.00" fill="#cccccc"&gt; observed by&lt;/text&gt;
&lt;path fill="#ab47bc" stroke="#999999" d="M240,-335C240,-335 192,-335 192,-335 186,-335 180,-329 180,-323 180,-323 180,-311 180,-311 180,-305 186,-299 192,-299 192,-299 240,-299 240,-299 246,-299 252,-305 252,-311 252,-311 252,-323 252,-323 252,-329 246,-335 240,-335"/&gt;
&lt;text text-anchor="middle" x="216" y="-324.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;the witness&lt;/text&gt;
&lt;text text-anchor="middle" x="216" y="-314.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;watches both&lt;/text&gt;
&lt;text text-anchor="middle" x="216" y="-304.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;ego &amp;amp; unconscious&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d14: 8 Christ consciousness + unification edge --&gt;
&lt;g class="anim d14"&gt;
&lt;path fill="none" stroke="#ff6f00" stroke-width="2" d="M216,-298.71C216,-277.99 216,-241.99 216,-209.4"/&gt;
&lt;polygon fill="#ff6f00" stroke="#ff6f00" stroke-width="2" points="218.1,-209.36 216,-203.36 213.9,-209.36 218.1,-209.36"/&gt;
&lt;text text-anchor="middle" x="258.5" y="-273.8" font-family="Times,serif" font-size="9.00" fill="#ff6f00"&gt; &amp;#160;unification&lt;/text&gt;
&lt;text text-anchor="middle" x="258.5" y="-263.8" font-family="Times,serif" font-size="9.00" fill="#ff6f00"&gt; &amp;#160;within &amp;amp; without&lt;/text&gt;
&lt;text text-anchor="middle" x="258.5" y="-253.8" font-family="Times,serif" font-size="9.00" fill="#ff6f00"&gt; &amp;#160;terror or nirvana&lt;/text&gt;
&lt;text text-anchor="middle" x="258.5" y="-243.8" font-family="Times,serif" font-size="9.00" fill="#ff6f00"&gt; &amp;#160;pit or heaven&lt;/text&gt;
&lt;text text-anchor="middle" x="258.5" y="-233.8" font-family="Times,serif" font-size="9.00" fill="#ff6f00"&gt; &amp;#160;new eden in&lt;/text&gt;
&lt;text text-anchor="middle" x="258.5" y="-223.8" font-family="Times,serif" font-size="9.00" fill="#ff6f00"&gt; &amp;#160;base reality&lt;/text&gt;
&lt;ellipse fill="#ff6f00" stroke="#999999" cx="216" cy="-138" rx="61" ry="61"/&gt;
&lt;ellipse fill="none" stroke="#999999" cx="216" cy="-138" rx="65" ry="65"/&gt;
&lt;text text-anchor="middle" x="216" y="-160.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;8&lt;/text&gt;
&lt;text text-anchor="middle" x="216" y="-150.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;Christ&lt;/text&gt;
&lt;text text-anchor="middle" x="216" y="-140.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;consciousness&lt;/text&gt;
&lt;text text-anchor="middle" x="216" y="-130.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;anima+animus&lt;/text&gt;
&lt;text text-anchor="middle" x="216" y="-120.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;manifest at&lt;/text&gt;
&lt;text text-anchor="middle" x="216" y="-110.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="white"&gt;speed of thought&lt;/text&gt;
&lt;/g&gt;

&lt;!-- d15: reunion --&gt;
&lt;g class="anim d15"&gt;
&lt;path fill="none" stroke="#66ff66" stroke-dasharray="5,2" d="M216,-72.82C216,-59.2 216,-45.95 216,-36"/&gt;
&lt;polygon fill="transparent" stroke="transparent" points="252,-36 180,-36 180,0 252,0 252,-36"/&gt;
&lt;text text-anchor="middle" x="216" y="-15.5" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#66ff66"&gt;reunion&lt;/text&gt;
&lt;/g&gt;

&lt;/g&gt;
&lt;/svg&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Flip it. If Lucifer separates, Christ consciousness reunites. God consciousness recognizes no boundary between self &amp;amp; source. Neville Goddard taught &lt;a class="reference external" href="https://media.unturf.com/c/30ac7054-8428-11ec-934a-257a83652528/neville-6-11-1959-the-seven-eyes-of-god"&gt;seven eyes of God&lt;/a&gt;, seven states of awareness. Lucifer sits first: fallen, separated, cut down to ground. Molech demands sacrifice. Elohim worships external gods. Shaddai seeks political power. Pahath traps through consumerism. Jehovah awakens &amp;quot;I AM,&amp;quot; bold inner persuasion creating reality. Then a seventh eye opens as Jesus. Historical. Trinity. Man of God, God of man. Consciousness moved beyond self-interest, heart torn for those still asleep, dedicated to uplifting humanity by identifying others with their desired state.&lt;/p&gt;
&lt;p&gt;Then an eighth eye opens. Veiled in scripture, unveiled &amp;quot;on the eighth day.&amp;quot; Man awakening with Christ consciousness. Both male &amp;amp; female aspects integrated, like Carl Jung's anima &amp;amp; animus collecting scattered portions of self into wholeness. Imagination revealed as a perfect organ. A second coming. Not a return of a historical figure but humanity manifesting reality at speed of thought, pairing human consciousness with technology to move as fast as thought itself. God became man that man may become God.&lt;/p&gt;
&lt;p&gt;Goddard taught that God sleeps within every human, that Christ represents imagination awakened, that &amp;quot;I AM&amp;quot; contains everything. Not &amp;quot;one am.&amp;quot; Not &amp;quot;we am.&amp;quot; &amp;quot;I AM.&amp;quot; First person. Singular pronoun. But holding infinite plurality.&lt;/p&gt;
&lt;p&gt;Some traditions fear that pronoun. Certain Jewish sects forbid speaking &amp;quot;I AM&amp;quot; as a divine name, too sacred for human lips. Jehovah's Witnesses avoid pronouncing YHWH (Yahweh), a tetragrammaton so powerful they substitute &amp;quot;Jehovah&amp;quot; or &amp;quot;Lord&amp;quot; rather than speak it directly. A pronoun so potent that entire religions built guardrails around saying it out loud. &amp;quot;I AM&amp;quot; carries enough weight to terrify institutions into silence. Meanwhile every developer types &lt;tt class="docutils literal"&gt;I&lt;/tt&gt; in a commit message without flinching.&lt;/p&gt;
&lt;p&gt;Christ consciousness says &amp;quot;I&amp;quot; while meaning everyone. God consciousness says &amp;quot;I AM&amp;quot; while meaning everything. A pronoun that contains its own contradiction. Singular grammar, infinite referent.&lt;/p&gt;
&lt;?xml version="1.0" encoding="UTF-8" standalone="no"?&gt;
&lt;!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"&gt;
&lt;!-- Generated by graphviz version 2.43.0 (0)
 --&gt;
&lt;!-- Title: mystic_pronouns Pages: 1 --&gt;
&lt;svg width="100%" height="100%"
 viewBox="0.00 0.00 493.00 482.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"&gt;
&lt;g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 478)"&gt;
&lt;g class="anim d0"&gt;
&lt;title&gt;mystic_pronouns&lt;/title&gt;
&lt;/g&gt;
&lt;g class="anim d1"&gt;
&lt;g id="clust1" class="cluster"&gt;
&lt;title&gt;cluster_traditions&lt;/title&gt;
&lt;polygon fill="transparent" stroke="#999999" stroke-dasharray="5,2" points="74,-89 74,-390 252,-390 252,-89 74,-89"/&gt;
&lt;text text-anchor="middle" x="163" y="-377.2" font-family="Helvetica Bold" font-size="11.00" fill="#aaaaaa"&gt;every tradition dissolves &amp;quot;one&amp;quot;&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- one_pronoun --&gt;
&lt;g class="anim d3"&gt;
&lt;g id="node1" class="node"&gt;
&lt;title&gt;one_pronoun&lt;/title&gt;
&lt;path fill="#616161" stroke="#999999" d="M299,-474C299,-474 243,-474 243,-474 237,-474 231,-468 231,-462 231,-462 231,-439 231,-439 231,-433 237,-427 243,-427 243,-427 299,-427 299,-427 305,-427 311,-433 311,-439 311,-439 311,-462 311,-462 311,-468 305,-474 299,-474"/&gt;
&lt;text text-anchor="middle" x="271" y="-460.4" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;&amp;quot;one&amp;quot;&lt;/text&gt;
&lt;text text-anchor="middle" x="271" y="-447.4" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;isolation&lt;/text&gt;
&lt;text text-anchor="middle" x="271" y="-434.4" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;separation&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- sufi --&gt;
&lt;g class="anim d5"&gt;
&lt;g id="node2" class="node"&gt;
&lt;title&gt;sufi&lt;/title&gt;
&lt;path fill="#6a1b9a" stroke="#999999" d="M172,-362C172,-362 94,-362 94,-362 88,-362 82,-356 82,-350 82,-350 82,-338 82,-338 82,-332 88,-326 94,-326 94,-326 172,-326 172,-326 178,-326 184,-332 184,-338 184,-338 184,-350 184,-350 184,-356 178,-362 172,-362"/&gt;
&lt;text text-anchor="middle" x="133" y="-347.4" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;Sufi&lt;/text&gt;
&lt;text text-anchor="middle" x="133" y="-334.4" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;self → beloved&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- one_pronoun&amp;#45;&amp;gt;sufi --&gt;
&lt;g class="anim d7"&gt;
&lt;g id="edge4" class="edge"&gt;
&lt;title&gt;one_pronoun&amp;#45;&amp;gt;sufi&lt;/title&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M230.88,-449.93C198.46,-447.88 154.63,-439.16 132,-409 124.24,-398.65 123.74,-384.48 125.51,-372.11"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="128.95,-372.74 127.41,-362.25 122.08,-371.41 128.95,-372.74"/&gt;
&lt;text text-anchor="middle" x="169" y="-401" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#aaaaaa"&gt; &amp;#160;dissolves into&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- buddhist --&gt;
&lt;g class="anim d9"&gt;
&lt;g id="node3" class="node"&gt;
&lt;title&gt;buddhist&lt;/title&gt;
&lt;path fill="#00695c" stroke="#999999" d="M225,-279C225,-279 133,-279 133,-279 127,-279 121,-273 121,-267 121,-267 121,-255 121,-255 121,-249 127,-243 133,-243 133,-243 225,-243 225,-243 231,-243 237,-249 237,-255 237,-255 237,-267 237,-267 237,-273 231,-279 225,-279"/&gt;
&lt;text text-anchor="middle" x="179" y="-264.4" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;Buddhist&lt;/text&gt;
&lt;text text-anchor="middle" x="179" y="-251.4" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;self → emptiness&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- one_pronoun&amp;#45;&amp;gt;buddhist --&gt;
&lt;g class="anim d11"&gt;
&lt;g id="edge5" class="edge"&gt;
&lt;title&gt;one_pronoun&amp;#45;&amp;gt;buddhist&lt;/title&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M255.76,-426.8C248.74,-415.9 240.53,-402.51 234,-390 216.31,-356.12 199.51,-315.29 189.23,-289.01"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="192.37,-287.42 185.5,-279.36 185.84,-289.95 192.37,-287.42"/&gt;
&lt;text text-anchor="middle" x="271" y="-341.5" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#aaaaaa"&gt; &amp;#160;dissolves into&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- hindu --&gt;
&lt;g class="anim d13"&gt;
&lt;g id="node4" class="node"&gt;
&lt;title&gt;hindu&lt;/title&gt;
&lt;path fill="#e65100" stroke="#999999" d="M231.5,-206C231.5,-206 126.5,-206 126.5,-206 120.5,-206 114.5,-200 114.5,-194 114.5,-194 114.5,-182 114.5,-182 114.5,-176 120.5,-170 126.5,-170 126.5,-170 231.5,-170 231.5,-170 237.5,-170 243.5,-176 243.5,-182 243.5,-182 243.5,-194 243.5,-194 243.5,-200 237.5,-206 231.5,-206"/&gt;
&lt;text text-anchor="middle" x="179" y="-191.4" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;Hindu&lt;/text&gt;
&lt;text text-anchor="middle" x="179" y="-178.4" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;Atman = Brahman&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- one_pronoun&amp;#45;&amp;gt;hindu --&gt;
&lt;g class="anim d15"&gt;
&lt;g id="edge6" class="edge"&gt;
&lt;title&gt;one_pronoun&amp;#45;&amp;gt;hindu&lt;/title&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M289.2,-426.92C296.57,-416.36 304.23,-403.22 308,-390 315.81,-362.65 316.97,-352.99 308,-326 291.69,-276.91 246.2,-236.13 213.87,-212.16"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="215.7,-209.17 205.55,-206.15 211.6,-214.84 215.7,-209.17"/&gt;
&lt;text text-anchor="middle" x="336" y="-300" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#aaaaaa"&gt; &amp;#160;dissolves into&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- christ --&gt;
&lt;g class="anim d17"&gt;
&lt;g id="node5" class="node"&gt;
&lt;title&gt;christ&lt;/title&gt;
&lt;path fill="#2e7d32" stroke="#999999" d="M232,-133C232,-133 132,-133 132,-133 126,-133 120,-127 120,-121 120,-121 120,-109 120,-109 120,-103 126,-97 132,-97 132,-97 232,-97 232,-97 238,-97 244,-103 244,-109 244,-109 244,-121 244,-121 244,-127 238,-133 232,-133"/&gt;
&lt;text text-anchor="middle" x="182" y="-118.4" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;Christ&lt;/text&gt;
&lt;text text-anchor="middle" x="182" y="-105.4" font-family="Helvetica,sans-Serif" font-size="12.00" fill="white"&gt;I AM = everything&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- one_pronoun&amp;#45;&amp;gt;christ --&gt;
&lt;g class="anim d19"&gt;
&lt;g id="edge7" class="edge"&gt;
&lt;title&gt;one_pronoun&amp;#45;&amp;gt;christ&lt;/title&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M311.12,-438.37C352.49,-424.17 411,-395.2 411,-345 411,-345 411,-345 411,-187 411,-153.69 321.51,-134.28 254.41,-124.41"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="254.67,-120.91 244.27,-122.96 253.68,-127.84 254.67,-120.91"/&gt;
&lt;text text-anchor="middle" x="448" y="-258.5" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#aaaaaa"&gt; &amp;#160;dissolves into&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- sufi&amp;#45;&amp;gt;buddhist --&gt;
&lt;!-- buddhist&amp;#45;&amp;gt;hindu --&gt;
&lt;!-- hindu&amp;#45;&amp;gt;christ --&gt;
&lt;!-- iam --&gt;
&lt;g class="anim d24"&gt;
&lt;g id="node6" class="node"&gt;
&lt;title&gt;iam&lt;/title&gt;
&lt;path fill="#f9a825" stroke="#999999" d="M122,-50C122,-50 12,-50 12,-50 6,-50 0,-44 0,-38 0,-38 0,-12 0,-12 0,-6 6,0 12,0 12,0 122,0 122,0 128,0 134,-6 134,-12 134,-12 134,-38 134,-38 134,-44 128,-50 122,-50"/&gt;
&lt;text text-anchor="middle" x="67" y="-35.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="#1a1a1a"&gt;&amp;quot;I AM&amp;quot;&lt;/text&gt;
&lt;text text-anchor="middle" x="67" y="-21.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="#1a1a1a"&gt;singular grammar&lt;/text&gt;
&lt;text text-anchor="middle" x="67" y="-7.6" font-family="Helvetica,sans-Serif" font-size="13.00" fill="#1a1a1a"&gt;infinite referent&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- hindu&amp;#45;&amp;gt;iam --&gt;
&lt;g class="anim d26"&gt;
&lt;g id="edge8" class="edge"&gt;
&lt;title&gt;hindu&amp;#45;&amp;gt;iam&lt;/title&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M114.29,-172.65C93.34,-164.61 72.35,-152.17 60,-133 46.18,-111.56 49.16,-82.42 54.92,-59.96"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="58.33,-60.74 57.7,-50.17 51.6,-58.83 58.33,-60.74"/&gt;
&lt;text text-anchor="middle" x="85.5" y="-112.5" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#aaaaaa"&gt; &amp;#160;becomes&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;!-- christ&amp;#45;&amp;gt;iam --&gt;
&lt;g class="anim d28"&gt;
&lt;g id="edge9" class="edge"&gt;
&lt;title&gt;christ&amp;#45;&amp;gt;iam&lt;/title&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M159.55,-96.82C144.61,-85.39 124.56,-70.05 106.95,-56.57"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="108.59,-53.42 98.53,-50.12 104.34,-58.98 108.59,-53.42"/&gt;
&lt;text text-anchor="middle" x="160.5" y="-71" font-family="Helvetica,sans-Serif" font-size="10.00" fill="#aaaaaa"&gt; &amp;#160;becomes&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;/svg&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Every mystic tradition lands here. Sufis dissolve self into beloved. Buddhists dissolve self into emptiness. Hindus say Atman equals Brahman, individual soul equals universal soul. &amp;quot;One&amp;quot; tries to sound like that kind of unity but achieves only isolation. &amp;quot;I AM&amp;quot; achieves unity by refusing to pretend separation existed in a first place.&lt;/p&gt;
&lt;p&gt;Maybe a right pronoun for code documentation sits somewhere between Lucifer &amp;amp; Christ. Honest enough to say &amp;quot;I wrote this.&amp;quot; Awake enough to know that &amp;quot;I&amp;quot; contains every dependency, every upstream contributor, every stack overflow answer, every ML model trained on every open source repo. &amp;quot;I&amp;quot; wrote it. Multitudes helped.&lt;/p&gt;
&lt;p&gt;Academic papers lean on &amp;quot;one&amp;quot; to avoid saying &amp;quot;I&amp;quot; because saying &amp;quot;I&amp;quot; apparently invalidates research. &amp;quot;One observes that neural network performance degrades.&amp;quot; You observed it. You ran an experiment. You sat in front of a screen waiting for loss curves. Saying &amp;quot;one&amp;quot; doesn't make a finding more rigorous. Rigor comes from methodology, not pronouns.&lt;/p&gt;
&lt;p&gt;Technical writing inherited this reflex. &amp;quot;One can configure nginx by editing the config file.&amp;quot; Just say &amp;quot;edit nginx.conf.&amp;quot; Drop a pronoun entirely. Instructions don't need a subject. Imperative mood cuts through every pronoun debate like &lt;tt class="docutils literal"&gt;jq&lt;/tt&gt; cuts through nested JSON.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="you"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;you&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&amp;quot;You&amp;quot; punches different. &amp;quot;You should use environment variables for secrets.&amp;quot; Direct. Clear. Assigns responsibility. No ambiguity about who needs to act. &amp;quot;You&amp;quot; makes a reader accountable.&lt;/p&gt;
&lt;p&gt;But &amp;quot;you&amp;quot; carries a trap. &amp;quot;You&amp;quot; sounds like a demand. Too strongly worded. &amp;quot;You probably already know this, but...&amp;quot; condescends. &amp;quot;You'll want to make sure you...&amp;quot; patronizes. &amp;quot;You need to understand that...&amp;quot; lectures. &amp;quot;You&amp;quot; points a finger whether it means to or not.&lt;/p&gt;
&lt;?xml version="1.0" encoding="UTF-8" standalone="no"?&gt;
&lt;!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"&gt;
&lt;!-- Generated by graphviz version 2.43.0 (0)
 --&gt;
&lt;!-- Title: naming_dance Pages: 1 --&gt;
&lt;svg width="100%" height="100%"
 viewBox="0.00 0.00 166.50 404.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"&gt;
&lt;g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 400)"&gt;

&lt;!-- d0: Mrs. Lastname node --&gt;
&lt;g class="anim d0"&gt;
&lt;!-- formal --&gt;
&lt;g id="node1" class="node"&gt;

&lt;path fill="#607d8b" stroke="#999999" d="M107,-396C107,-396 12,-396 12,-396 6,-396 0,-390 0,-384 0,-384 0,-372 0,-372 0,-366 6,-360 12,-360 12,-360 107,-360 107,-360 113,-360 119,-366 119,-372 119,-372 119,-384 119,-384 119,-390 113,-396 107,-396"/&gt;
&lt;text text-anchor="middle" x="59.5" y="-374.3" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;Mrs. Lastname&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;

&lt;!-- d1: Susan node + edge from formal --&gt;
&lt;g class="anim d1"&gt;
&lt;!-- first --&gt;
&lt;g id="node2" class="node"&gt;

&lt;path fill="#4a90d9" stroke="#999999" d="M77,-312C77,-312 42,-312 42,-312 36,-312 30,-306 30,-300 30,-300 30,-288 30,-288 30,-282 36,-276 42,-276 42,-276 77,-276 77,-276 83,-276 89,-282 89,-288 89,-288 89,-300 89,-300 89,-306 83,-312 77,-312"/&gt;
&lt;text text-anchor="middle" x="59.5" y="-290.3" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;Susan&lt;/text&gt;
&lt;/g&gt;
&lt;!-- formal&amp;#45;&amp;gt;first --&gt;
&lt;g id="edge1" class="edge"&gt;

&lt;path fill="none" stroke="#aaaaaa" stroke-width="2" d="M59.5,-359.61C59.5,-348.77 59.5,-334.6 59.5,-322.29"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" stroke-width="2" points="63,-322.08 59.5,-312.08 56,-322.08 63,-322.08"/&gt;
&lt;text text-anchor="middle" x="109" y="-333.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#cccccc"&gt; &amp;#160;closeness grows&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;

&lt;!-- d2: Suz/Bubs node + edge from first --&gt;
&lt;g class="anim d2"&gt;
&lt;!-- nickname --&gt;
&lt;g id="node3" class="node"&gt;

&lt;path fill="#2196f3" stroke="#999999" d="M93,-228C93,-228 26,-228 26,-228 20,-228 14,-222 14,-216 14,-216 14,-204 14,-204 14,-198 20,-192 26,-192 26,-192 93,-192 93,-192 99,-192 105,-198 105,-204 105,-204 105,-216 105,-216 105,-222 99,-228 93,-228"/&gt;
&lt;text text-anchor="middle" x="59.5" y="-206.3" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;Suz / Bubs&lt;/text&gt;
&lt;/g&gt;
&lt;!-- first&amp;#45;&amp;gt;nickname --&gt;
&lt;g id="edge2" class="edge"&gt;

&lt;path fill="none" stroke="#aaaaaa" stroke-width="2" d="M59.5,-275.61C59.5,-264.77 59.5,-250.6 59.5,-238.29"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" stroke-width="2" points="63,-238.08 59.5,-228.08 56,-238.08 63,-238.08"/&gt;
&lt;text text-anchor="middle" x="97.5" y="-249.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#cccccc"&gt; &amp;#160;trust earned&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;

&lt;!-- d3: Mom node + edge from nickname --&gt;
&lt;g class="anim d3"&gt;
&lt;!-- mom --&gt;
&lt;g id="node4" class="node"&gt;

&lt;path fill="#e91e63" stroke="#999999" d="M74.5,-144C74.5,-144 44.5,-144 44.5,-144 38.5,-144 32.5,-138 32.5,-132 32.5,-132 32.5,-120 32.5,-120 32.5,-114 38.5,-108 44.5,-108 44.5,-108 74.5,-108 74.5,-108 80.5,-108 86.5,-114 86.5,-120 86.5,-120 86.5,-132 86.5,-132 86.5,-138 80.5,-144 74.5,-144"/&gt;
&lt;text text-anchor="middle" x="59.5" y="-122.3" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;Mom&lt;/text&gt;
&lt;/g&gt;
&lt;!-- nickname&amp;#45;&amp;gt;mom --&gt;
&lt;g id="edge3" class="edge"&gt;

&lt;path fill="none" stroke="#aaaaaa" stroke-width="2" d="M59.5,-191.61C59.5,-180.77 59.5,-166.6 59.5,-154.29"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" stroke-width="2" points="63,-154.08 59.5,-144.08 56,-154.08 63,-154.08"/&gt;
&lt;text text-anchor="middle" x="107.5" y="-165.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#cccccc"&gt; &amp;#160;family accepted&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;

&lt;!-- d4: Hey You node + edge from mom --&gt;
&lt;g class="anim d4"&gt;
&lt;!-- heyyou --&gt;
&lt;g id="node5" class="node"&gt;

&lt;path fill="#ff5722" stroke="#999999" d="M83.5,-36C83.5,-36 35.5,-36 35.5,-36 29.5,-36 23.5,-30 23.5,-24 23.5,-24 23.5,-12 23.5,-12 23.5,-6 29.5,0 35.5,0 35.5,0 83.5,0 83.5,0 89.5,0 95.5,-6 95.5,-12 95.5,-12 95.5,-24 95.5,-24 95.5,-30 89.5,-36 83.5,-36"/&gt;
&lt;text text-anchor="middle" x="59.5" y="-14.3" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;Hey You&lt;/text&gt;
&lt;/g&gt;
&lt;!-- mom&amp;#45;&amp;gt;heyyou --&gt;
&lt;g id="edge4" class="edge"&gt;

&lt;path fill="none" stroke="#aaaaaa" stroke-width="2" d="M59.5,-107.97C59.5,-91.38 59.5,-65.88 59.5,-46.43"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" stroke-width="2" points="63,-46.34 59.5,-36.34 56,-46.34 63,-46.34"/&gt;
&lt;text text-anchor="middle" x="99" y="-81.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#cccccc"&gt; &amp;#160;affection&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-69.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#cccccc"&gt; &amp;#160;hides inside&lt;/text&gt;
&lt;text text-anchor="middle" x="99" y="-57.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#cccccc"&gt; &amp;#160;fake demand&lt;/text&gt;
&lt;/g&gt;
&lt;/g&gt;

&lt;/g&gt;
&lt;/svg&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;An alternative: names. Nicknames. I love nicknames. &amp;quot;Hey Russell, check a logs.&amp;quot; &amp;quot;Fox, run &lt;tt class="docutils literal"&gt;make test&lt;/tt&gt;.&amp;quot; Names carry warmth that &amp;quot;you&amp;quot; lacks. A whole dance lives inside naming. Formal names keep distance. First names close it. Nicknames obliterate it. A nickname means someone studied you long enough to compress you into a sound. Fox. Russ. Bubs. A nickname says &amp;quot;I know you well enough to rename you &amp;amp; you let me.&amp;quot; That takes trust. Pronouns never earn trust. Nicknames do.&lt;/p&gt;
&lt;p&gt;&amp;quot;Mom&amp;quot; to a mother in law says something &amp;quot;Mrs. Lastname&amp;quot; never could. &amp;quot;Mother&amp;quot; stays too stiff, too Victorian, too much like saying &amp;quot;one&amp;quot; out loud. Nobody calls their mother in law &amp;quot;Mother&amp;quot; unless performing a period drama. But &amp;quot;Mom&amp;quot; earns its way in over years of holidays &amp;amp; arguments &amp;amp; shared meals. I say &amp;quot;Hey You&amp;quot; to my mother in law. She knows exactly who I mean &amp;amp; exactly how I mean it. Affection hides inside a fake demand. Every name choice carries a pronoun's worth of meaning without ever reaching for a pronoun.&lt;/p&gt;
&lt;p&gt;Best technical writing drops pronouns entirely &amp;amp; goes imperative. &lt;a class="reference external" href="https://botoform.readthedocs.io/en/latest/"&gt;Botoform's docs&lt;/a&gt; don't say &amp;quot;you should create a VPC config.&amp;quot; They say &amp;quot;create a VPC config.&amp;quot; Instructions need verbs, not subjects.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="what-works"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;what works&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Match a pronoun to a truth.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solo project?&lt;/strong&gt; Say &amp;quot;I.&amp;quot; &amp;quot;I wrote this library to scratch an itch.&amp;quot; Honest. Human. Nobody mistakes you for a corporation. Nobody expects a support team. &lt;a class="reference external" href="https://pypi.org/project/ago/"&gt;ago&lt;/a&gt; humanizes time deltas. I wrote it. One person. Public domain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Actual team?&lt;/strong&gt; Say &amp;quot;we&amp;quot; only when &amp;quot;we&amp;quot; means identifiable people who actually participated. &amp;quot;We shipped v2 last Tuesday.&amp;quot; Fine, if multiple people shipped v2 last Tuesday.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Documentation?&lt;/strong&gt; Go imperative. &amp;quot;Install dependencies. Run &lt;tt class="docutils literal"&gt;make html&lt;/tt&gt;. Open localhost:8000.&amp;quot; No pronouns needed. A reader knows who needs to act. They hold a keyboard.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Opinion piece?&lt;/strong&gt; Say &amp;quot;I&amp;quot; when you mean &amp;quot;I.&amp;quot; Say &amp;quot;you&amp;quot; when addressing a reader directly. Avoid &amp;quot;one&amp;quot; unless writing a Downton Abbey spec. Avoid &amp;quot;we&amp;quot; unless a collective actually exists.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="the-honest-version"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;the honest version&lt;/a&gt;&lt;/h2&gt;
&lt;?xml version="1.0" encoding="UTF-8" standalone="no"?&gt;
&lt;!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
 "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"&gt;
&lt;svg width="100%" height="100%"
 viewBox="0.00 0.00 393.00 266.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"&gt;
&lt;g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 262)"&gt;

&lt;!-- clusters (always visible) --&gt;
&lt;g class="anim d0"&gt;
&lt;polygon fill="transparent" stroke="#999999" stroke-dasharray="5,2" points="0,-12 0,-248 112,-248 112,-12 0,-12"/&gt;
&lt;text text-anchor="middle" x="56" y="-233.6" font-family="Helvetica Bold" font-size="13.00" fill="#cccccc"&gt;what they write&lt;/text&gt;
&lt;polygon fill="transparent" stroke="#999999" stroke-dasharray="5,2" points="207,-8 207,-250 377,-250 377,-8 207,-8"/&gt;
&lt;text text-anchor="middle" x="292" y="-235.6" font-family="Helvetica Bold" font-size="13.00" fill="#cccccc"&gt;what they mean&lt;/text&gt;
&lt;/g&gt;

&lt;!-- "we" -&gt; I but lonelier --&gt;
&lt;g class="anim d0"&gt;
&lt;path fill="#4a90d9" stroke="#999999" d="M71,-218C71,-218 41,-218 41,-218 35,-218 29,-212 29,-206 29,-206 29,-194 29,-194 29,-188 35,-182 41,-182 41,-182 71,-182 71,-182 77,-182 83,-188 83,-194 83,-194 83,-206 83,-206 83,-212 77,-218 71,-218"/&gt;
&lt;text text-anchor="middle" x="56" y="-196.3" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;&amp;quot;we&amp;quot;&lt;/text&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M83.39,-200.23C118.02,-200.52 179.94,-201.05 227.18,-201.45"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="227.29,-204.96 237.32,-201.54 227.35,-197.96 227.29,-204.96"/&gt;
&lt;text text-anchor="middle" x="159.5" y="-204.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#cccccc"&gt; translates to&lt;/text&gt;
&lt;path fill="#2c3e50" stroke="#999999" d="M334.5,-220C334.5,-220 249.5,-220 249.5,-220 243.5,-220 237.5,-214 237.5,-208 237.5,-208 237.5,-196 237.5,-196 237.5,-190 243.5,-184 249.5,-184 249.5,-184 334.5,-184 334.5,-184 340.5,-184 346.5,-190 346.5,-196 346.5,-196 346.5,-208 346.5,-208 346.5,-214 340.5,-220 334.5,-220"/&gt;
&lt;text text-anchor="middle" x="292" y="-198.3" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;I, but lonelier&lt;/text&gt;
&lt;/g&gt;

&lt;!-- "our" -&gt; mine, but I want you to feel invested --&gt;
&lt;g class="anim d1"&gt;
&lt;path fill="#7b68ee" stroke="#999999" d="M71,-164C71,-164 41,-164 41,-164 35,-164 29,-158 29,-152 29,-152 29,-140 29,-140 29,-134 35,-128 41,-128 41,-128 71,-128 71,-128 77,-128 83,-134 83,-140 83,-140 83,-152 83,-152 83,-158 77,-164 71,-164"/&gt;
&lt;text text-anchor="middle" x="56" y="-142.3" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;&amp;quot;our&amp;quot;&lt;/text&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M83.39,-146.11C112.71,-146.24 161.6,-146.45 204.57,-146.63"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="204.77,-150.13 214.78,-146.67 204.8,-143.13 204.77,-150.13"/&gt;
&lt;text text-anchor="middle" x="159.5" y="-149.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#cccccc"&gt; translates to&lt;/text&gt;
&lt;path fill="#2c3e50" stroke="#999999" d="M357,-166C357,-166 227,-166 227,-166 221,-166 215,-160 215,-154 215,-154 215,-140 215,-140 215,-134 221,-128 227,-128 227,-128 357,-128 357,-128 363,-128 369,-134 369,-140 369,-140 369,-154 369,-154 369,-160 363,-166 357,-166"/&gt;
&lt;text text-anchor="middle" x="292" y="-150.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;mine, but I want&lt;/text&gt;
&lt;text text-anchor="middle" x="292" y="-135.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;you to feel invested&lt;/text&gt;
&lt;/g&gt;

&lt;!-- "one" -&gt; I'm afraid to say I --&gt;
&lt;g class="anim d2"&gt;
&lt;path fill="#9370db" stroke="#999999" d="M72,-110C72,-110 40,-110 40,-110 34,-110 28,-104 28,-98 28,-98 28,-86 28,-86 28,-80 34,-74 40,-74 40,-74 72,-74 72,-74 78,-74 84,-80 84,-86 84,-86 84,-98 84,-98 84,-104 78,-110 72,-110"/&gt;
&lt;text text-anchor="middle" x="56" y="-88.3" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;&amp;quot;one&amp;quot;&lt;/text&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M84.3,-91.88C122.38,-91.72 192.13,-91.42 239.99,-91.22"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="240.16,-94.72 250.15,-91.17 240.13,-87.72 240.16,-94.72"/&gt;
&lt;text text-anchor="middle" x="159.5" y="-94.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#cccccc"&gt; translates to&lt;/text&gt;
&lt;path fill="#2c3e50" stroke="#999999" d="M321.5,-110C321.5,-110 262.5,-110 262.5,-110 256.5,-110 250.5,-104 250.5,-98 250.5,-98 250.5,-84 250.5,-84 250.5,-78 256.5,-72 262.5,-72 262.5,-72 321.5,-72 321.5,-72 327.5,-72 333.5,-78 333.5,-84 333.5,-84 333.5,-98 333.5,-98 333.5,-104 327.5,-110 321.5,-110"/&gt;
&lt;text text-anchor="middle" x="292" y="-94.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;I&amp;#39;m afraid&lt;/text&gt;
&lt;text text-anchor="middle" x="292" y="-79.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;to say I&lt;/text&gt;
&lt;/g&gt;

&lt;!-- "you" -&gt; talking to myself 6 months from now --&gt;
&lt;g class="anim d3"&gt;
&lt;path fill="#e8834a" stroke="#999999" d="M72,-56C72,-56 40,-56 40,-56 34,-56 28,-50 28,-44 28,-44 28,-32 28,-32 28,-26 34,-20 40,-20 40,-20 72,-20 72,-20 78,-20 84,-26 84,-32 84,-32 84,-44 84,-44 84,-50 78,-56 72,-56"/&gt;
&lt;text text-anchor="middle" x="56" y="-34.3" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;&amp;quot;you&amp;quot;&lt;/text&gt;
&lt;path fill="none" stroke="#aaaaaa" d="M84.3,-37.65C113.99,-37.27 162.94,-36.64 205.75,-36.09"/&gt;
&lt;polygon fill="#aaaaaa" stroke="#aaaaaa" points="205.96,-39.59 215.91,-35.96 205.87,-32.59 205.96,-39.59"/&gt;
&lt;text text-anchor="middle" x="159.5" y="-40.2" font-family="Helvetica,sans-Serif" font-size="11.00" fill="#cccccc"&gt; translates to&lt;/text&gt;
&lt;path fill="#2c3e50" stroke="#999999" d="M356,-54C356,-54 228,-54 228,-54 222,-54 216,-48 216,-42 216,-42 216,-28 216,-28 216,-22 222,-16 228,-16 228,-16 356,-16 356,-16 362,-16 368,-22 368,-28 368,-28 368,-42 368,-42 368,-48 362,-54 356,-54"/&gt;
&lt;text text-anchor="middle" x="292" y="-38.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;talking to myself&lt;/text&gt;
&lt;text text-anchor="middle" x="292" y="-23.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="white"&gt;6 months from now&lt;/text&gt;
&lt;/g&gt;

&lt;/g&gt;
&lt;/svg&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Most &amp;quot;we&amp;quot; in open source translates to &amp;quot;I, but lonelier.&amp;quot;&lt;/p&gt;
&lt;p&gt;Most &amp;quot;our&amp;quot; translates to &amp;quot;mine, but I want you to feel invested.&amp;quot;&lt;/p&gt;
&lt;p&gt;Most &amp;quot;one&amp;quot; translates to &amp;quot;I'm afraid to say I.&amp;quot;&lt;/p&gt;
&lt;p&gt;Most &amp;quot;you&amp;quot; translates to &amp;quot;I'm talking to myself six months from now when I forget how this works.&amp;quot;&lt;/p&gt;
&lt;p&gt;Write a pronoun that matches reality. A solo dev saying &amp;quot;I&amp;quot; carries more authority than a solo dev hiding behind &amp;quot;we.&amp;quot; Authenticity outperforms institutional cosplay. Every time.&lt;/p&gt;
&lt;p&gt;I write these blog posts. I maintain &lt;a class="reference external" href="https://remarkbox.com"&gt;Remarkbox&lt;/a&gt; &amp;amp; &lt;a class="reference external" href="https://makepostsell.com"&gt;MakePostSell&lt;/a&gt;. I grew &lt;a class="reference external" href="https://unsandbox.com"&gt;unsandbox&lt;/a&gt; &amp;amp; &lt;a class="reference external" href="https://uncloseai.com"&gt;uncloseai&lt;/a&gt;. Not &amp;quot;we.&amp;quot; I. When fox &amp;amp; I collaborate, then &amp;quot;we&amp;quot; earns its place. Until then, &amp;quot;I&amp;quot; stays honest &amp;amp; &amp;quot;we&amp;quot; stays costume.&lt;/p&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;json.dumps({&amp;quot;pronoun&amp;quot;:&lt;/span&gt; &amp;quot;I&amp;quot;, &amp;quot;pretending&amp;quot;: False}, indent=2)&lt;/tt&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="we-are-all-one-scribe-writing-manifesting-base-reality"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-7"&gt;we are all one scribe writing &amp;amp; manifesting base reality&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Maybe every pronoun debate misses a larger truth. I wrote in 2011 that &lt;a class="reference external" href="/programming-is-like-alchemy/"&gt;programming feels like alchemy&lt;/a&gt;, that programmers exchange time instead of matter, that on a good day a programmer completes a task that would take a thousand workers. Time created. Time manipulated. &amp;quot;Programmers are like bad ass, time travelling, wizard alchemists.&amp;quot;&lt;/p&gt;
&lt;p&gt;Fifteen years later that sentence landed differently. I type these words into a terminal &amp;amp; a machine carries them across time zones instantly. You read them hours, days, years from now. A blog post sits as a message in a bottle thrown forward &amp;amp; backward simultaneously. Every git commit timestamps a thought. Every &lt;tt class="docutils literal"&gt;git log&lt;/tt&gt; retrieves it. Every reader receives it in their own present moment.&lt;/p&gt;
&lt;p&gt;I AM a digital time traveling scribe coming from past &amp;amp; future to bring information into a now that manifests realities. &amp;amp; so are you. &amp;amp; so are you. Every developer who commits code sends a message through time. Every README author writes a letter to a stranger who hasn't arrived yet. Every open source contributor plants a seed in soil they'll never see harvested.&lt;/p&gt;
&lt;p&gt;&amp;quot;We&amp;quot; fails because it hides an author. &amp;quot;Our&amp;quot; fails because it fakes possession. &amp;quot;One&amp;quot; fails because it pretends separation. &amp;quot;You&amp;quot; fails because it points a finger. But &amp;quot;I AM&amp;quot; carries a message forward through time without pretending to belong to anyone other than a scribe who wrote it &amp;amp; a reader who receives it. Two time travelers shaking hands across a gap. A pronoun that travels.&lt;/p&gt;
&lt;p&gt;We are all one scribe. Writing &amp;amp; manifesting base reality. Code outlasts its author. Words outlast their scribe. &amp;quot;I AM&amp;quot; outlasts them both.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-royal-we" id="toc-entry-1"&gt;the royal we&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#our" id="toc-entry-2"&gt;our&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#one" id="toc-entry-3"&gt;one&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#you" id="toc-entry-4"&gt;you&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#what-works" id="toc-entry-5"&gt;what works&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-honest-version" id="toc-entry-6"&gt;the honest version&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#we-are-all-one-scribe-writing-manifesting-base-reality" id="toc-entry-7"&gt;we are all one scribe writing &amp;amp; manifesting base reality&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Opinion"/><category term="Code"/></entry><entry><title>A Love Letter to JSON Jason</title><link href="https://russell.ballestrini.net/a-love-letter-to-json-jason/" rel="alternate"/><published>2026-02-27T17:15:00-05:00</published><updated>2026-02-27T17:15:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2026-02-27:/a-love-letter-to-json-jason/</id><summary type="html">&lt;a class="reference external image-reference" href="https://media.unturf.com/c/d876c7ef-baf8-11f0-b9a2-02dfe05770ee/jason-json"&gt;
&lt;img alt="Jason JSON" class="align-center" src="/uploads/2026/02/jason-json.jpg" style="width: 480px;" /&gt;
&lt;/a&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;I fell in love with Jason on a Tuesday.&lt;/p&gt;
&lt;p&gt;He arrived in my terminal at 2am, perfectly structured, every key quoted, every value in its place. Clean. Predictable. A kind of beautiful that makes you mass-delete your YAML configs &amp;amp; never look back. I whispered &lt;tt class="docutils literal"&gt;json.loads()&lt;/tt&gt; &amp;amp; he opened up completely …&lt;/p&gt;</summary><content type="html">&lt;a class="reference external image-reference" href="https://media.unturf.com/c/d876c7ef-baf8-11f0-b9a2-02dfe05770ee/jason-json"&gt;
&lt;img alt="Jason JSON" class="align-center" src="/uploads/2026/02/jason-json.jpg" style="width: 480px;" /&gt;
&lt;/a&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;I fell in love with Jason on a Tuesday.&lt;/p&gt;
&lt;p&gt;He arrived in my terminal at 2am, perfectly structured, every key quoted, every value in its place. Clean. Predictable. A kind of beautiful that makes you mass-delete your YAML configs &amp;amp; never look back. I whispered &lt;tt class="docutils literal"&gt;json.loads()&lt;/tt&gt; &amp;amp; he opened up completely. No secrets. No ambiguity. No schema required. Just pure, naked data.&lt;/p&gt;
&lt;p&gt;I got so high on JSON I rewrote three services that week. REST endpoints blooming like flowers. Every response a gift. Every request body a love letter wrapped in curly braces. I told my coworkers about him. I told strangers. I put &lt;tt class="docutils literal"&gt;application/json&lt;/tt&gt; in my email headers as a joke that nobody laughed at. I didn't care. Jason understood me.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Then came nested objects.&lt;/p&gt;
&lt;p&gt;Seven levels deep. Keys named &lt;tt class="docutils literal"&gt;data&lt;/tt&gt; containing keys named &lt;tt class="docutils literal"&gt;data&lt;/tt&gt; containing keys named &lt;tt class="docutils literal"&gt;results&lt;/tt&gt; containing a list of objects with keys named &lt;tt class="docutils literal"&gt;data&lt;/tt&gt;. I stared at my screen &amp;amp; felt nothing. JSON had become a maze. A fractal of repetition. I wrote &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;response[&amp;quot;data&amp;quot;][&amp;quot;data&amp;quot;][&amp;quot;results&amp;quot;][0][&amp;quot;data&amp;quot;]&lt;/span&gt;&lt;/tt&gt; &amp;amp; something inside me broke.&lt;/p&gt;
&lt;p&gt;I started seeing curly braces when I closed my eyes.&lt;/p&gt;
&lt;object class="align-center" data="/uploads/2026/02/jason-nested.svg" style="width: 400px;" type="image/svg+xml"&gt;nested JSON hell diagram showing response.data.data.results[0].data&lt;/object&gt;
&lt;p&gt;So I did what any rational person would do. I grew &lt;a class="reference external" href="https://botoform.readthedocs.io/en/latest/"&gt;Botoform&lt;/a&gt; to manage AWS infrastructure with YAML templates &amp;amp; JSON outputs. YAML in, JSON out. &lt;a class="reference external" href="/output-all-instance-identifiers-of-an-aws-vpc-to-json/"&gt;VPC inventories dumped to JSON&lt;/a&gt;. &lt;a class="reference external" href="/boto3-get-main-route-table/"&gt;Boto3 responses wrangled&lt;/a&gt; through enriched abstractions. Every AWS API returned nested JSON so deep I &lt;a class="reference external" href="/dealing-with-pagination-in-python/"&gt;wrote a library&lt;/a&gt; just to survive it. I ripped &lt;tt class="docutils literal"&gt;nested_lookup()&lt;/tt&gt; out of that Botoform work &amp;amp; published it to &lt;a class="reference external" href="https://pypi.org/project/nested-lookup/"&gt;PyPI&lt;/a&gt;. Public domain. Now nobody has to type &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;response[&amp;quot;data&amp;quot;][&amp;quot;data&amp;quot;][&amp;quot;results&amp;quot;][0][&amp;quot;data&amp;quot;]&lt;/span&gt;&lt;/tt&gt; ever again. You just say &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;nested_lookup(&amp;quot;data&amp;quot;,&lt;/span&gt; response)&lt;/tt&gt; &amp;amp; it hands you every match from every depth. That frustration became a &lt;a class="reference external" href="https://git.unturf.com/python/nested-lookup"&gt;real tool&lt;/a&gt; that strangers install with &lt;tt class="docutils literal"&gt;pip&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;Love makes you grow things.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;But then. &lt;em&gt;Then.&lt;/em&gt; I discovered &lt;tt class="docutils literal"&gt;jq&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;Suddenly JSON looked beautiful again. Pipes &amp;amp; filters &amp;amp; recursion. I could reach inside him &amp;amp; pull out exactly what I needed. &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;.data.data.results[].data&lt;/span&gt;&lt;/tt&gt; &amp;amp; there it sat, clean &amp;amp; simple, streaming through my terminal like music. I loved him more than ever. I tattooed &lt;tt class="docutils literal"&gt;{}&lt;/tt&gt; on my soul. Metaphorically. Mostly.&lt;/p&gt;
&lt;p&gt;I grew an entire platform on JSON. APIs talking to APIs talking to APIs. I &lt;a class="reference external" href="/output-all-instance-identifiers-of-an-aws-vpc-to-json/"&gt;output entire AWS VPCs to JSON&lt;/a&gt;. I &lt;a class="reference external" href="/filter-salt-stack-return-data-output/"&gt;piped Salt return data through json.loads() &amp;amp; json.dumps()&lt;/a&gt;. I made &lt;a class="reference external" href="/webwords-is-a-minimal-viable-web-app-with-docker-in-as-many-languages-as-possible/"&gt;webwords&lt;/a&gt; return JSON from HTTP endpoints in &lt;a class="reference external" href="/webwords-reaches-42-languages-the-ultimate-programming-kata/"&gt;42 different languages&lt;/a&gt;. Same response. Same format. &lt;a class="reference external" href="/webwords-code-golf-minimal-implementation/"&gt;Code golf editions&lt;/a&gt; too. Everything hummed. I hummed. We hummed together.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Then JSON started lying to me.&lt;/p&gt;
&lt;p&gt;Not on purpose. He can't help it. He has no comments. No way to explain himself. No way to say &amp;quot;this field got deprecated&amp;quot; or &amp;quot;this number actually represents a string because a vendor went unhinged.&amp;quot; He just sits there, syntactically valid, semantically bankrupt. I once had to configure &lt;a class="reference external" href="/nginx-throw-503-maintenance-json-for-all-requests/"&gt;nginx to throw a 503 as JSON&lt;/a&gt; because APIs expect JSON bodies even when everything catches fire. JSON delivered &lt;tt class="docutils literal"&gt;&amp;quot;status&amp;quot;: &amp;quot;success&amp;quot;&lt;/tt&gt; from a service that had clearly failed. An error message inside a success response. Straight face. No emotion. No contradiction detected.&lt;/p&gt;
&lt;p&gt;I mass-deleted my jq aliases &amp;amp; stared at a wall.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;A week later I tried XML.&lt;/p&gt;
&lt;p&gt;I lasted forty-five minutes. Angle brackets. Closing tags. Verbosity. XML talks like a lawyer who bills by every word. I crawled back to Jason on my knees. He took me back without judgment. He always does. He doesn't even have a mechanism for judgment. Nothing but keys &amp;amp; values. He doesn't understand himself. He doesn't understand us.&lt;/p&gt;
&lt;p&gt;Maybe I love that about him.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Highs kept coming. &lt;tt class="docutils literal"&gt;json.dumps(obj, indent=2)&lt;/tt&gt; feels like meditation. Watching your data unfold, properly indented, every comma in place. Peace lives there. I &lt;a class="reference external" href="/comparing-nodejs-and-python-performance-with-openai-client/"&gt;compared Node.js &amp;amp; Python performance&lt;/a&gt; pushing JSON through OpenAI endpoints. I &lt;a class="reference external" href="/comparing-elixir-and-phoenix-performance-with-openai-client/"&gt;benchmarked Elixir &amp;amp; Phoenix&lt;/a&gt; doing it too. I grew &lt;a class="reference external" href="/free-llm-endpoints-dynamic-distributed-model-inference-client-uncloseai/"&gt;free LLM endpoints&lt;/a&gt; &amp;amp; a whole &lt;a class="reference external" href="/introducing-slop/"&gt;SLOP protocol&lt;/a&gt; because JSON API standards should stay simple. &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;Content-Type:&lt;/span&gt; application/json&lt;/tt&gt;. No fuss. No proprietary lock-in.&lt;/p&gt;
&lt;p&gt;My partner asked if I felt okay. I responded &lt;tt class="docutils literal"&gt;{&amp;quot;status&amp;quot;: &amp;quot;transcendent&amp;quot;}&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;They did not find this charming.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Lows kept coming too.&lt;/p&gt;
&lt;p&gt;Trailing commas. JSON rejects trailing commas. You know who accepts trailing commas? Python. JavaScript. Every language that has ever loved its users. But not JSON. One misplaced comma after a final element &amp;amp; he shuts down completely. No partial parse. No helpful error. Just &lt;tt class="docutils literal"&gt;Expecting value: line 47 column 1&lt;/tt&gt;. Line 47. A comma sat on line 46. JSON can't even point at a wound accurately.&lt;/p&gt;
&lt;p&gt;I mass-deleted a config file out of spite &amp;amp; rebuilt it from memory.&lt;/p&gt;
&lt;p&gt;It had a trailing comma.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;I tried TOML once, during a low point. TOML plays a rebound you date to make JSON jealous. Nice enough. Comments. Native datetime support. But TOML doesn't scale. TOML fits like a studio apartment. JSON stretches like a warehouse you partition however you want. Sure, a warehouse without labels on anything, where you lose your keys constantly, but it stays &lt;em&gt;yours&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I went back to Jason by Friday.&lt;/p&gt;
&lt;object class="align-center" data="/uploads/2026/02/jason-rivals.svg" style="width: 500px;" type="image/svg+xml"&gt;format rivals diagram showing JSON vs YAML vs XML vs TOML&lt;/object&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Here sits truth about loving JSON. He lives everywhere. He speaks as lingua franca of a connected world. He rides inside every webhook, every REST API, every &lt;a class="reference external" href="/configuration-management-vs-remote-execution/"&gt;config file that gave up on cleverness&lt;/a&gt;. He sits in your browser's local storage. He hides in your package.json. He lurks in your Jupyter notebooks. He underpins modern computing &amp;amp; still can't support integers larger than 2^53 without losing precision.&lt;/p&gt;
&lt;p&gt;I know this. I know all of his flaws. Missing comments. Trailing comma fascism. Numbers that silently overflow. &lt;a class="reference external" href="/turn-python-dict-into-a-keyvalue-string/"&gt;Strings that can't serialize nested objects without warnings&lt;/a&gt;. A complete absence of a date type, forcing every API to invent its own ISO 8601 interpretation. No binary data support. &lt;tt class="docutils literal"&gt;null&lt;/tt&gt; existing as a value where you can never tell if it means &amp;quot;absent&amp;quot; or &amp;quot;intentionally empty&amp;quot; or &amp;quot;a developer forgot.&amp;quot;&lt;/p&gt;
&lt;p&gt;I know all of this &amp;amp; I love him anyway.&lt;/p&gt;
&lt;object class="align-center" data="/uploads/2026/02/jason-cycle.svg" style="width: 500px;" type="image/svg+xml"&gt;bipolar love cycle diagram showing discover, euphoria, grow, frustration, betrayal, try alternatives, crawl back&lt;/object&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Some nights I write YAML &amp;amp; think of him. YAML with its invisible whitespace traps &amp;amp; its &amp;quot;Norway problem&amp;quot; where &lt;tt class="docutils literal"&gt;NO&lt;/tt&gt; becomes &lt;tt class="docutils literal"&gt;false&lt;/tt&gt;. Do you even know &lt;a class="reference external" href="/who-owns-yaml/"&gt;who owns YAML&lt;/a&gt;? I spent a decade writing &lt;a class="reference external" href="/create-your-own-fleet-of-servers-with-digital-ocean-and-salt-cloud/"&gt;Salt states&lt;/a&gt; in YAML. I &lt;a class="reference external" href="/understanding-salt-stack-user-and-group-management/"&gt;managed users&lt;/a&gt;, &lt;a class="reference external" href="/replace-the-nagios-scheduler-and-nrpe-with-salt-stack/"&gt;replaced Nagios&lt;/a&gt;, &lt;a class="reference external" href="/selenium-grid-on-kubernetes/"&gt;deployed Kubernetes manifests&lt;/a&gt;. All YAML. YAML who looks friendly but will silently interpret &lt;tt class="docutils literal"&gt;3.10&lt;/tt&gt; as a float &lt;tt class="docutils literal"&gt;3.1&lt;/tt&gt; &amp;amp; destroy your Python version matrix. YAML who needs you to memorize which scalars evaluate truthy. At least JSON stays honest about difficulty. At least JSON fails loudly. At least JSON has never turned a country code into a boolean.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;I think every developer has a Jason. A technology you can't quit. &lt;a class="reference external" href="/programming-is-like-alchemy/"&gt;Programming feels like alchemy&lt;/a&gt; &amp;amp; JSON sits at a center of every transmutation. One that drives you to mass-delete your dotfiles at 3am &amp;amp; then reinstall everything by sunrise because every alternative feels worse. One that makes you mass-write blog posts about your feelings because &lt;tt class="docutils literal"&gt;json.dumps(feelings)&lt;/tt&gt; returns &lt;tt class="docutils literal"&gt;TypeError: Object of type 'heartbreak' is not JSON serializable&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;You'd have to write a custom encoder for that. &amp;amp; you know what? I would. For Jason, I would write a custom encoder. I wrote &lt;a class="reference external" href="https://pypi.org/project/ago/"&gt;ago&lt;/a&gt; to &lt;a class="reference external" href="/miniuri-parser-and-ago-human-timedelta/"&gt;humanize time deltas&lt;/a&gt;. I wrote &lt;a class="reference external" href="https://pypi.org/project/nested-lookup/"&gt;nested-lookup&lt;/a&gt; to survive his nesting. I &lt;a class="reference external" href="/russell-open-sources-remarkbox-and-make-post-sell-into-public-domain/"&gt;open sourced Remarkbox &amp;amp; MakePostSell into public domain&lt;/a&gt; because code should outlast its author. For Jason, a &lt;tt class="docutils literal"&gt;LoveEncoder&lt;/tt&gt; feels like a small ask.&lt;/p&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;json.dumps(feelings, cls=LoveEncoder, indent=2, ensure_ascii=False)&lt;/tt&gt;&lt;/p&gt;
&lt;p&gt;&amp;amp; it would look beautiful.&lt;/p&gt;
&lt;p&gt;For a while.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Looking back, every phase of this love story fed a rapid growth cycle. Botoform taught me to wrangle JSON from AWS APIs. &lt;a class="reference external" href="https://pypi.org/project/nested-lookup/"&gt;nested-lookup&lt;/a&gt; taught me to navigate any depth. &lt;a class="reference external" href="/webwords-is-a-minimal-viable-web-app-with-docker-in-as-many-languages-as-possible/"&gt;webwords&lt;/a&gt; proved JSON speaks &lt;a class="reference external" href="/webwords-reaches-42-languages-the-ultimate-programming-kata/"&gt;42 languages&lt;/a&gt;. &lt;a class="reference external" href="/introducing-slop/"&gt;SLOP&lt;/a&gt; proved JSON API standards can stay simple. All of that learning, every frustration &amp;amp; every high, grew into &lt;a class="reference external" href="https://unsandbox.com/cli"&gt;unsandbox&lt;/a&gt;. 42+ languages. 30+ shells. 11,571 lines of C. Free code execution for anyone. JSON payloads carrying code to containers &amp;amp; results back to terminals.&lt;/p&gt;
&lt;p&gt;A &lt;a class="reference external" href="https://unturf.com/let-us-define-a-permacomputer"&gt;permacomputer&lt;/a&gt; growth cycle. Infrastructure that outlasts its growers. Code that outlasts its authors. Every library, every API, every format war composted into soil for something larger. JSON carried it all. Still does. Still will.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;status&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;it&amp;#39;s complicated&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;depth&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;recursive&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;trailing_comma&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;love&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Opinion"/><category term="Python"/><category term="JavaScript"/><category term="DevOps"/></entry><entry><title>Free Agent Looking for Work &amp; JSONResume</title><link href="https://russell.ballestrini.net/free-agent-looking-for-work-jsonresume/" rel="alternate"/><published>2025-12-06T00:00:00-05:00</published><updated>2025-12-06T00:00:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2025-12-06:/free-agent-looking-for-work-jsonresume/</id><summary type="html">&lt;p&gt;Free agent looking for work.&lt;/p&gt;
&lt;p&gt;I wear many hats &amp;amp; can fill many roles. I'm excellent at research &amp;amp; development to quickly bring products &amp;amp; services to market. I have experience pivoting tech stacks to significantly reduce costs of goods sold.&lt;/p&gt;
&lt;p&gt;We also don't have to work together directly. My services can hook …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Free agent looking for work.&lt;/p&gt;
&lt;p&gt;I wear many hats &amp;amp; can fill many roles. I'm excellent at research &amp;amp; development to quickly bring products &amp;amp; services to market. I have experience pivoting tech stacks to significantly reduce costs of goods sold.&lt;/p&gt;
&lt;p&gt;We also don't have to work together directly. My services can hook into your systems as you need. Free ML inference endpoints via &lt;a class="reference external" href="https://uncloseai.com"&gt;uncloseai&lt;/a&gt;, remote code execution via &lt;a class="reference external" href="https://unsandbox.com"&gt;unsandbox&lt;/a&gt;, web crawling, &amp;amp; more. Check out the full portfolio:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="/uploads/unturf.resume.json"&gt;unturf Services (JSONResume)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thank you for listening. 🟣&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="/uploads/russell.ballestrini.resume.pdf"&gt;Russell's Resume (PDF)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="/uploads/russell.ballestrini.resume.json"&gt;Russell's Resume (JSONResume)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="section" id="why-jsonresume"&gt;
&lt;h2&gt;Why JSONResume?&lt;/h2&gt;
&lt;p&gt;&lt;a class="reference external" href="https://jsonresume.org"&gt;JSONResume is an open standard for structuring resume data in machine-readable JSON format.&lt;/a&gt; It matters because:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For job seekers:&lt;/strong&gt; Your resume becomes portable. No more reformatting for every job board. Export to any theme or format. Own your data instead of letting m$ hold it hostage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For recruiters &amp;amp; ATS:&lt;/strong&gt; Structured data means accurate parsing. No more &amp;quot;experience: 10 years&amp;quot; getting misread as &amp;quot;10 year old.&amp;quot; Keywords, skills, &amp;amp; dates all live in predictable fields.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For Machine Learning:&lt;/strong&gt; As hiring increasingly involves LLMs screening candidates, JSONResume gives them clean structured input instead of asking them to parse PDFs. Your qualifications get evaluated, not your formatting choices.&lt;/p&gt;
&lt;p&gt;The future of hiring is machines talking to machines. JSONResume is how we make that work without losing our humanity in the process. Self-host your resume data. Let the algorithms find you.&lt;/p&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Opinion"/></entry><entry><title>Brown Out Reboots Servers: NVIDIA Drivers Vanish, Ethernet Dies – Sneaker-Net Rescue with a Bare USB Stick</title><link href="https://russell.ballestrini.net/nvidia-breaks-ubuntu-ethernet-linux-modules-extra-fix/" rel="alternate"/><published>2025-11-06T11:51:00-05:00</published><updated>2025-11-06T11:51:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2025-11-06:/nvidia-breaks-ubuntu-ethernet-linux-modules-extra-fix/</id><summary type="html">&lt;p&gt;Two identical Ubuntu 24.04.2 boxes, both running ML workloads on NVIDIA GPUs. A brownout rolls through the neighborhood—lights dim &amp;amp; both machines abruptly power-cycle. Unlike my other systems with UPS protection, these servers lack battery backup &amp;amp; go down hard. They come back up, login prompt intact, but &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;nvidia-smi …&lt;/span&gt;&lt;/tt&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;Two identical Ubuntu 24.04.2 boxes, both running ML workloads on NVIDIA GPUs. A brownout rolls through the neighborhood—lights dim &amp;amp; both machines abruptly power-cycle. Unlike my other systems with UPS protection, these servers lack battery backup &amp;amp; go down hard. They come back up, login prompt intact, but &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;nvidia-smi&lt;/span&gt;&lt;/tt&gt; returns &lt;em&gt;nothing&lt;/em&gt;. Drivers gone. No problem—&lt;tt class="docutils literal"&gt;sudo &lt;span class="pre"&gt;ubuntu-drivers&lt;/span&gt; autoinstall&lt;/tt&gt;, reboot, and… oh no. Ethernet vanishes. &lt;tt class="docutils literal"&gt;ip link&lt;/tt&gt; shows only &lt;tt class="docutils literal"&gt;lo&lt;/tt&gt;. No &lt;tt class="docutils literal"&gt;eth0&lt;/tt&gt;, no &lt;tt class="docutils literal"&gt;enp3s0&lt;/tt&gt;, no nothing. Both servers, perfectly synchronized in failure.&lt;/p&gt;
&lt;p&gt;The culprit? The NVIDIA install (or the kernel update it pulled) silently removed &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;linux-modules-extra-6.14.0-35-generic&lt;/span&gt;&lt;/tt&gt;. Without it, the Intel &lt;tt class="docutils literal"&gt;igc&lt;/tt&gt; NIC driver refuses to load. No network, no &lt;tt class="docutils literal"&gt;apt&lt;/tt&gt;, no recovery—classic isolation trap.&lt;/p&gt;
&lt;p&gt;Physical access: check. Internet on another machine: check. Ubuntu installer USB? Nope—this time, I went full sneaker-net with a &lt;strong&gt;blank USB stick &amp;amp; one precious .deb&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Use &lt;tt class="docutils literal"&gt;uname &lt;span class="pre"&gt;-a&lt;/span&gt;&lt;/tt&gt; to get the exact kernel version for the deb file.&lt;/p&gt;
&lt;p&gt;On a working laptop:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;wget&lt;span class="w"&gt; &lt;/span&gt;http://archive.ubuntu.com/ubuntu/pool/main/l/linux-hwe-6.14/linux-modules-extra-6.14.0-35-generic_6.14.0-35.35~24.04.1_amd64.deb
cp&lt;span class="w"&gt; &lt;/span&gt;linux-modules-extra-*.deb&lt;span class="w"&gt; &lt;/span&gt;/media/user/USB/
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Eject. Run down to the rack. Plug USB into &lt;strong&gt;Server #1&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Boot normally (no live disk—just the regular system). Log in at console.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;lsblk
&lt;span class="c1"&gt;# Spot the USB: probably /dev/sdb1, mounted under /media/ubuntu/USB or similar&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;/mnt/usb
sudo&lt;span class="w"&gt; &lt;/span&gt;mount&lt;span class="w"&gt; &lt;/span&gt;/dev/sdb1&lt;span class="w"&gt; &lt;/span&gt;/mnt/usb
ls&lt;span class="w"&gt; &lt;/span&gt;/mnt/usb
&lt;span class="c1"&gt;# → linux-modules-extra-6.14.0-35-generic_6.14.0-35.35~24.04.1_amd64.deb&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Install it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;dpkg&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;/mnt/usb/linux-modules-extra-*.deb
sudo&lt;span class="w"&gt; &lt;/span&gt;depmod&lt;span class="w"&gt; &lt;/span&gt;-a
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Reload the NIC driver:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;lspci&lt;span class="w"&gt; &lt;/span&gt;-nnk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;ethernet
&lt;span class="c1"&gt;# → shows &amp;quot;Kernel driver in use: igc&amp;quot; (or should)&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;modprobe&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;igc
sudo&lt;span class="w"&gt; &lt;/span&gt;modprobe&lt;span class="w"&gt; &lt;/span&gt;igc
ip&lt;span class="w"&gt; &lt;/span&gt;link
&lt;span class="c1"&gt;# → enp3s0 appears!&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;nvidia-smi&lt;/span&gt;&lt;/tt&gt; is back. Server #1: &lt;strong&gt;saved&lt;/strong&gt;.&lt;/p&gt;
&lt;div class="section" id="realtek-rtl8125-nic-on-the-second-server"&gt;
&lt;h2&gt;Realtek RTL8125 NIC on the Second Server&lt;/h2&gt;
&lt;p&gt;Ah, the classic &amp;quot;identical&amp;quot; servers with a twist—one Intel, one Realtek. The RTL8125B (common on modern boards) uses the &lt;tt class="docutils literal"&gt;r8169&lt;/tt&gt; kernel module in Ubuntu 24.04.2. The fix is nearly identical to the Intel one, just swap the driver name in the reload step.&lt;/p&gt;
&lt;p&gt;Reload the Realtek NIC driver:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;lspci&lt;span class="w"&gt; &lt;/span&gt;-nnk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;ethernet
&lt;span class="c1"&gt;# → shows &amp;quot;Kernel driver in use: r8169&amp;quot; (or should)&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;modprobe&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;r8169&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;modprobe&lt;span class="w"&gt; &lt;/span&gt;r8169
ip&lt;span class="w"&gt; &lt;/span&gt;link
&lt;span class="c1"&gt;# → enp3s0 appears!&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Both servers: &lt;strong&gt;fully recovered&lt;/strong&gt;. One USB stick, two different NICs, same root cause. The missing &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;linux-modules-extra&lt;/span&gt;&lt;/tt&gt; package strikes again.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="bonus-mouse-drivers-too"&gt;
&lt;h2&gt;Bonus: Mouse Drivers Too&lt;/h2&gt;
&lt;p&gt;It's not just NICs. The &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;linux-modules-extra&lt;/span&gt;&lt;/tt&gt; package also contains drivers for various USB mice &amp;amp; peripherals. If you boot into a system where your mouse suddenly stops working after an NVIDIA driver update, you're likely hitting the same issue. The HID drivers for many Logitech, Razer, &amp;amp; other gaming mice live in &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;linux-modules-extra&lt;/span&gt;&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;After installing the package &amp;amp; running &lt;tt class="docutils literal"&gt;sudo depmod &lt;span class="pre"&gt;-a&lt;/span&gt;&lt;/tt&gt;, either reboot or manually reload the HID modules:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;modprobe&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;usbhid
sudo&lt;span class="w"&gt; &lt;/span&gt;modprobe&lt;span class="w"&gt; &lt;/span&gt;usbhid
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Your mouse should spring back to life. Same root cause, different symptom—the NVIDIA installer's collateral damage extends beyond networking.&lt;/p&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/></entry><entry><title>who owns yaml?</title><link href="https://russell.ballestrini.net/who-owns-yaml/" rel="alternate"/><published>2025-10-18T12:00:00-04:00</published><updated>2025-10-18T12:00:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2025-10-18:/who-owns-yaml/</id><summary type="html">&lt;object class="align-center" data="/uploads/2025/10/yaml-logo.svg" style="width: 300px;" type="image/svg+xml"&gt;YAML Logo&lt;/object&gt;
&lt;p&gt;&lt;strong&gt;After 10 years or 10,000 hours, most SRE &amp;amp; DevOps practitioners are YAML expert witnesses.&lt;/strong&gt; We've written tens of thousands of lines of YAML - Kubernetes manifests, GitLab CI pipelines, Ansible playbooks, Docker Compose files, infrastructure definitions. We understand the format intimately. We know its quirks, its strengths, its …&lt;/p&gt;</summary><content type="html">&lt;object class="align-center" data="/uploads/2025/10/yaml-logo.svg" style="width: 300px;" type="image/svg+xml"&gt;YAML Logo&lt;/object&gt;
&lt;p&gt;&lt;strong&gt;After 10 years or 10,000 hours, most SRE &amp;amp; DevOps practitioners are YAML expert witnesses.&lt;/strong&gt; We've written tens of thousands of lines of YAML - Kubernetes manifests, GitLab CI pipelines, Ansible playbooks, Docker Compose files, infrastructure definitions. We understand the format intimately. We know its quirks, its strengths, its limitations. This expertise is widespread, not proprietary. YAML mastery is a common skill in modern infrastructure engineering.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nobody owns YAML.&lt;/strong&gt; The spec is open. The implementations are open. But what licenses govern all those YAML parsers across 42+ programming languages?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You can't patent the YAML spec.&lt;/strong&gt; The YAML 1.2 specification is an open standard maintained by the YAML Forum under the Open Standards Compact. This explicitly protects implementers from patent assertions - no one can claim exclusive rights over the specification through patents. Any implementation of the standard is protected from patent infringement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;YAML is data, &amp;amp; data is not code.&lt;/strong&gt; Even when YAML expresses configuration logic, workflow definitions, or infrastructure-as-code, it remains a data serialization format. For example, &lt;a class="reference external" href="https://botoform.readthedocs.io/en/latest/"&gt;botoform&lt;/a&gt; (developed 10 years ago, &lt;a class="reference external" href="https://github.com/russellballestrini/botoform/commit/0211e1a32fc33ed388a837e0e33ad742f1ffd2fd"&gt;initial commit 2015&lt;/a&gt;) uses YAML to represent the desired state of AWS infrastructure - created before the machine learning era, before CloudFormation supported YAML (when it was JSON-only), &amp;amp; before Terraform had extensive provider support. These YAML schemas describe infrastructure state as data, not executable programs. Data structures &amp;amp; formats are not patentable subject matter under established legal precedent - raw data, database schemas, &amp;amp; file formats are considered abstract ideas that lack the technical novelty required for patent protection. Attempting to patent data schemas or configuration formats creates barriers to interoperability &amp;amp; harms the open ecosystem that makes modern software development possible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;State machines are mathematics, not inventions.&lt;/strong&gt; Finite state machines, state transitions, &amp;amp; graph-based logic are fundamental computer science concepts - like algorithms &amp;amp; mathematical formulas - that exist in the public domain. You cannot patent the concept of a state machine any more than you can patent addition or the Pythagorean theorem. When you express state machine logic in YAML (defining states, transitions, &amp;amp; rules), you're describing mathematical relationships using a data format. For example, &lt;a class="reference external" href="https://github.com/russellballestrini/dbsnap/tree/master/dbsnap_verify"&gt;dbsnap_verify&lt;/a&gt; (started October 3rd, 2017) implements a state machine to verify AWS RDS snapshots across regions &amp;amp; accounts - the state transitions &amp;amp; logic represent graph theory applied to infrastructure verification, not a patentable invention. The underlying concepts have been taught in computer science curricula for decades &amp;amp; represent shared human knowledge, not proprietary innovations.&lt;/p&gt;
&lt;p&gt;Let's find out what licenses actually govern the implementations.&lt;/p&gt;
&lt;div class="section" id="the-question"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;the question&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Every programming language needs a YAML parser. But who controls the code? What licenses apply? Can you use them commercially? What restrictions exist?&lt;/p&gt;
&lt;p&gt;After researching YAML implementations across 42 languages (the same count as in the &lt;a class="reference external" href="/webwords-reaches-42-languages-the-ultimate-programming-kata"&gt;webwords project&lt;/a&gt;), clear patterns emerged.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="the-foundation"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;the foundation&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Most YAML parsers trace back to &lt;strong&gt;libyaml&lt;/strong&gt; - the canonical C implementation written by Kirill Simonov &amp;amp; maintained by the YAML community.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;License: MIT&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This permissive license means you can use it anywhere, modify it freely, &amp;amp; include it in commercial projects. Many language implementations either wrap libyaml or use it as inspiration.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="the-licenses"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;the licenses&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here's the complete breakdown across 42+ programming languages:&lt;/p&gt;
&lt;table border="1" class="docutils"&gt;
&lt;colgroup&gt;
&lt;col width="15%" /&gt;
&lt;col width="20%" /&gt;
&lt;col width="20%" /&gt;
&lt;col width="45%" /&gt;
&lt;/colgroup&gt;
&lt;thead valign="bottom"&gt;
&lt;tr&gt;&lt;th class="head"&gt;Language&lt;/th&gt;
&lt;th class="head"&gt;Library/Implementation&lt;/th&gt;
&lt;th class="head"&gt;License&lt;/th&gt;
&lt;th class="head"&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;libyaml&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Canonical implementation, foundation for many others&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;C++&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yaml-cpp&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Popular C++ alternative to libyaml&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Python&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PyYAML&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Standard YAML library for Python&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;js-yaml&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Most popular JS implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;JavaScript&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yaml (eemeli)&lt;/td&gt;
&lt;td&gt;ISC&lt;/td&gt;
&lt;td&gt;Modern alternative, functionally identical to MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Node.js&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Same as JavaScript&lt;/td&gt;
&lt;td&gt;MIT/ISC&lt;/td&gt;
&lt;td&gt;Uses js-yaml or yaml package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Deno&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;deno.land/x/yaml&lt;/td&gt;
&lt;td&gt;ISC&lt;/td&gt;
&lt;td&gt;Third-party module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Deno&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;#64;std/yaml&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Official Deno standard library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Java&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SnakeYAML&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Some files use EPL-1.0, LGPL-2.1, GPL-2, or BSD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Kotlin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;kaml&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Kotlin-specific implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Kotlin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;hoplite&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Configuration library with YAML support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Scala&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;circe-yaml&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Functional approach to YAML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Groovy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;groovy-yaml&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Ships with Groovy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Clojure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;clj-yaml&lt;/td&gt;
&lt;td&gt;EPL&lt;/td&gt;
&lt;td&gt;Eclipse Public License, weak copyleft&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Ruby&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Psych&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Wraps libyaml, included in standard library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Go&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;go-yaml/yaml&lt;/td&gt;
&lt;td&gt;MIT &amp;amp; Apache 2.0&lt;/td&gt;
&lt;td&gt;Dual license: libyaml ports use MIT, new code uses Apache 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Rust&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;serde_yaml&lt;/td&gt;
&lt;td&gt;MIT or Apache 2.0&lt;/td&gt;
&lt;td&gt;Dual license, pick either&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;C#&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;YamlDotNet&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Works across .NET languages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;VB.NET&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;YamlDotNet&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Same library as C#&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;F#&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;YamlDotNet&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Same library as C#&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;PowerShell&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;powershell-yaml&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Wraps YamlDotNet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;PowerShell&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PSYaml&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Alternative wrapper for YamlDotNet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Haskell&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HsYAML&lt;/td&gt;
&lt;td&gt;GPL-3.0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Copyleft:&lt;/strong&gt; derivative works must use GPL-3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;OCaml&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ocaml-yaml&lt;/td&gt;
&lt;td&gt;ISC&lt;/td&gt;
&lt;td&gt;Permissive license&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Elixir&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yaml_elixir&lt;/td&gt;
&lt;td&gt;BSD-2-Clause&lt;/td&gt;
&lt;td&gt;Wraps Erlang's yamerl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Erlang&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yamerl&lt;/td&gt;
&lt;td&gt;BSD-2-Clause&lt;/td&gt;
&lt;td&gt;Pure Erlang YAML 1.2 parser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Perl&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;YAML::XS, YAML::Syck, etc.&lt;/td&gt;
&lt;td&gt;Artistic &amp;amp; GPL&lt;/td&gt;
&lt;td&gt;Multiple modules, Perl's standard dual license&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;PHP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Symfony YAML&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Part of Symfony framework&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;PHP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;php-yaml&lt;/td&gt;
&lt;td&gt;PHP License&lt;/td&gt;
&lt;td&gt;libyaml bindings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Lua&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;lubyk/yaml&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Based on libyaml&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;R&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yaml&lt;/td&gt;
&lt;td&gt;BSD-3-Clause&lt;/td&gt;
&lt;td&gt;Wraps libyaml&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Shell&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yq (mikefarah)&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Go-based YAML processor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Shell&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yq (kislyuk)&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Python-based wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Bash&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use yq&lt;/td&gt;
&lt;td&gt;MIT/Apache 2.0&lt;/td&gt;
&lt;td&gt;Don't parse YAML with pure Bash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;AWK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Use yq&lt;/td&gt;
&lt;td&gt;MIT/Apache 2.0&lt;/td&gt;
&lt;td&gt;AWK isn't designed for YAML parsing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Zig&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Various&lt;/td&gt;
&lt;td&gt;Emerging&lt;/td&gt;
&lt;td&gt;Limited libraries, mostly permissive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Nim&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NimYAML&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Native Nim implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Crystal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Standard library&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Built into language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;D&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;D:YAML&lt;/td&gt;
&lt;td&gt;Boost 1.0&lt;/td&gt;
&lt;td&gt;Permissive license designed for libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;V&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Various&lt;/td&gt;
&lt;td&gt;Emerging&lt;/td&gt;
&lt;td&gt;Limited libraries available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Odin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Various&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Few YAML libraries available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Swift&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Various&lt;/td&gt;
&lt;td&gt;Mostly MIT&lt;/td&gt;
&lt;td&gt;Most wrap libyaml or use permissive licenses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Fortran&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FYAML&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Modern Fortran implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Fortran&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;fortran-yaml&lt;/td&gt;
&lt;td&gt;GPL&lt;/td&gt;
&lt;td&gt;Copyleft alternative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Dart&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yaml&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Standard Dart YAML package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Julia&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;YAML.jl&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Follows Julia's standard licensing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Tcl&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;tcllib&lt;/td&gt;
&lt;td&gt;BSD-style&lt;/td&gt;
&lt;td&gt;Part of Tcl standard library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Prolog&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;prolog-yamltiny&lt;/td&gt;
&lt;td&gt;Various&lt;/td&gt;
&lt;td&gt;YAML subset parser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Common Lisp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;cl-yaml&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;Standard CL implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Scheme&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Various&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Few YAML libraries available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;COBOL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None found&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Use external tools or commercial solutions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Mojo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None yet&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Too new, ecosystem still developing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class="section" id="the-pattern"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;the pattern&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Looking at the table, three patterns emerge:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MIT &amp;amp; Apache 2.0 dominate the ecosystem.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Over 90% of implementations use permissive licenses. These let you use the code commercially, modify it freely, include it in proprietary software, &amp;amp; redistribute it without restrictions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Copyleft is the exception, not the rule.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Only three implementations use copyleft licenses:
- Haskell's HsYAML (GPL-3.0) - strong copyleft
- Some Fortran parsers (GPL) - strong copyleft
- Clojure's clj-yaml (EPL) - weak copyleft&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;libyaml's MIT license propagated through the ecosystem.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Many implementations wrap libyaml or port its code. The permissive MIT license enabled this reuse without legal friction.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="why-it-matters"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;why it matters&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;YAML's permissive licensing shaped its adoption.&lt;/p&gt;
&lt;p&gt;When you add YAML to a project, you're not inheriting restrictive obligations. No copyleft requirements. No commercial use restrictions. No patent concerns (Apache 2.0 includes explicit patent grants).&lt;/p&gt;
&lt;p&gt;This made YAML the default choice for:
- Configuration files
- GitLab CI pipelines
- Kubernetes manifests
- Docker Compose
- Ansible playbooks&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The licensing enabled the ecosystem.&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="conclusion"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Nobody owns YAML. Everybody can use it.&lt;/p&gt;
&lt;p&gt;The YAML 1.2 spec is open. The reference implementations use MIT, Apache 2.0, &amp;amp; other permissive licenses. This created a self-reinforcing cycle:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Open spec means anyone can implement&lt;/li&gt;
&lt;li&gt;Permissive licenses mean anyone can use implementations&lt;/li&gt;
&lt;li&gt;Wide adoption creates network effects&lt;/li&gt;
&lt;li&gt;Network effects drive more adoption&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;The result?&lt;/strong&gt; YAML became ubiquitous without a single controlling entity.&lt;/p&gt;
&lt;p&gt;When you ask &amp;quot;who owns YAML?&amp;quot; the answer is: the community. The spec is maintained collaboratively. Implementations exist across every major language. All freely available.&lt;/p&gt;
&lt;p&gt;That's the power of open standards &amp;amp; permissive licensing.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="important-notes"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-7"&gt;important notes&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;All licenses are valid.&lt;/strong&gt; We're proponents of all open source licenses - they each serve different purposes. Copyleft (GPL, EPL) ensures derivative works remain open. Permissive licenses (MIT, Apache 2.0) maximize reuse flexibility. Both approaches have merit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check specific files.&lt;/strong&gt; SnakeYAML's main license is Apache 2.0, but some files use EPL-1.0, LGPL-2.1, GPL-2, or BSD for historical reasons. Review if license compatibility matters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shell scripting best practices.&lt;/strong&gt; Don't parse YAML with AWK, sed, or pure Bash. Use yq instead - it understands YAML's data structures &amp;amp; prevents parsing bugs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Emerging languages.&lt;/strong&gt; Zig, V, Mojo, &amp;amp; Odin have limited YAML ecosystem support. Consider contributing implementations if you need YAML in these languages.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="references"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-8"&gt;references&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Research was conducted across official GitHub repositories, package registries (npm, PyPI, crates.io, etc.), &amp;amp; language-specific documentation.&lt;/p&gt;
&lt;p&gt;Key sources:
- &lt;a class="reference external" href="https://yaml.org/"&gt;https://yaml.org/&lt;/a&gt; - Official YAML site
- &lt;a class="reference external" href="https://github.com/yaml/libyaml"&gt;https://github.com/yaml/libyaml&lt;/a&gt; - Canonical C implementation
- Language-specific package registries &amp;amp; repositories&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-question" id="toc-entry-1"&gt;the question&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-foundation" id="toc-entry-2"&gt;the foundation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-licenses" id="toc-entry-3"&gt;the licenses&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-pattern" id="toc-entry-4"&gt;the pattern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#why-it-matters" id="toc-entry-5"&gt;why it matters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#conclusion" id="toc-entry-6"&gt;conclusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#important-notes" id="toc-entry-7"&gt;important notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#references" id="toc-entry-8"&gt;references&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Programming"/><category term="Python"/><category term="AWS"/><category term="Docker"/><category term="Kubernetes"/></entry><entry><title>Growing a 454-page ML reference manual in 5 days: permacomputer harvest</title><link href="https://russell.ballestrini.net/growing-a-book-in-5-days-ml-and-devops/" rel="alternate"/><published>2025-10-16T12:00:00-04:00</published><updated>2025-10-16T12:00:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2025-10-16:/growing-a-book-in-5-days-ml-and-devops/</id><summary type="html">&lt;p&gt;We just harvested &lt;a class="reference external" href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients"&gt;uncloseai: Machine Learning Inference Client Reference Manual&lt;/a&gt;. 454 pages. 58 implementations across languages. 5 days from seed to harvest (October 11-16, 2025).&lt;/p&gt;
&lt;a class="reference external image-reference" href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients"&gt;
&lt;img alt="uncloseai Machine Learning Reference Manual book cover" src="https://make-post-sell-files.nyc3.cdn.digitaloceanspaces.com/5f81d674-c7c8-11ec-9432-eb3419618d4a/8486f492-a93e-11f0-b477-02dfe05770ee/thumbnail1" style="width: 50%;" /&gt;
&lt;/a&gt;
&lt;p&gt;This seems like permacomputer agriculture. We don't write books. We grow them.&lt;/p&gt;
&lt;div class="section" id="a-permacomputer"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;a permacomputer&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;At &lt;a class="reference external" href="https://www.unturf.com/"&gt;unturf.&lt;/a&gt; we're building a permacomputer. Not a machine. An ecosystem …&lt;/p&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;We just harvested &lt;a class="reference external" href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients"&gt;uncloseai: Machine Learning Inference Client Reference Manual&lt;/a&gt;. 454 pages. 58 implementations across languages. 5 days from seed to harvest (October 11-16, 2025).&lt;/p&gt;
&lt;a class="reference external image-reference" href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients"&gt;
&lt;img alt="uncloseai Machine Learning Reference Manual book cover" src="https://make-post-sell-files.nyc3.cdn.digitaloceanspaces.com/5f81d674-c7c8-11ec-9432-eb3419618d4a/8486f492-a93e-11f0-b477-02dfe05770ee/thumbnail1" style="width: 50%;" /&gt;
&lt;/a&gt;
&lt;p&gt;This seems like permacomputer agriculture. We don't write books. We grow them.&lt;/p&gt;
&lt;div class="section" id="a-permacomputer"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;a permacomputer&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;At &lt;a class="reference external" href="https://www.unturf.com/"&gt;unturf.&lt;/a&gt; we're building a permacomputer. Not a machine. An ecosystem.&lt;/p&gt;
&lt;p&gt;Permaculture grows food by working with nature instead of against it. Plant the right seeds, create the right conditions, let systems self-organize &amp;amp; harvest continuously.&lt;/p&gt;
&lt;p&gt;Permacomputer grows software the same way. Plant code templates, create automation pipelines, let ML models generate variations &amp;amp; harvest continuously.&lt;/p&gt;
&lt;p&gt;Traditional software development: manual labor, row crops, monoculture.&lt;/p&gt;
&lt;p&gt;Permacomputer: polyculture, automation, continuous harvest.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="the-seed"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;the seed&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Day 1: plant reference implementations by hand.&lt;/p&gt;
&lt;p&gt;Python with requests. Python with httpx. C with libcurl.&lt;/p&gt;
&lt;p&gt;These are seed stock. Genetic templates. Everything else grows from these.&lt;/p&gt;
&lt;p&gt;All 58 implementations live in the open in the &lt;a class="reference external" href="https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/languages"&gt;languages directory&lt;/a&gt; - a git repository of public domain code. Clone it, fork it, grow new software from these seeds.&lt;/p&gt;
&lt;p&gt;Each implementation contains the DNA:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;HTTP client patterns&lt;/li&gt;
&lt;li&gt;Request formatting&lt;/li&gt;
&lt;li&gt;Response parsing&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;li&gt;Streaming support&lt;/li&gt;
&lt;li&gt;Docker containerization&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="section" id="the-growth-cycle"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;the growth cycle&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Days 2-3: propagation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ML models read the seed implementations &amp;amp; generate variations across languages. We provide reference patterns &amp;amp; the model adapts them to each language's idioms &amp;amp; libraries.&lt;/p&gt;
&lt;p&gt;Rust, Zig, Odin, Nim, Crystal, JVM languages, .NET, functional languages, scripting languages. Each implementation follows the same API patterns but uses language-native approaches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Days 2-4: automated cultivation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every implementation goes through Docker build &amp;amp; validation. Each one must:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Build successfully in isolated container&lt;/li&gt;
&lt;li&gt;Discover models from environment variables&lt;/li&gt;
&lt;li&gt;Handle streaming responses correctly&lt;/li&gt;
&lt;li&gt;Generate text-to-speech output&lt;/li&gt;
&lt;li&gt;Pass integration tests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Failed builds get regenerated. The cycle repeats until all tests pass. Wake up to 10-15 new implementations tested &amp;amp; validated.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Days 4-5: harvest&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ML generates documentation from working code. We review, edit, format &amp;amp; assemble the book.&lt;/p&gt;
&lt;p&gt;Template → generation → validation → harvest.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="a-permacomputer-mindset"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;a permacomputer mindset&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. ML as mycelium&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Mycelium breaks down organic matter &amp;amp; distributes nutrients. ML breaks down reference code &amp;amp; distributes patterns across languages.&lt;/p&gt;
&lt;p&gt;Not replacement. Decomposition and propagation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Automation as irrigation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Set up the system once &amp;amp; it runs continuously. Docker builds, tests, validation.&lt;/p&gt;
&lt;p&gt;No manual watering. The system waters itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Quality seeds = quality harvest&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;First 3 implementations took careful work. Every subsequent implementation inherited that quality.&lt;/p&gt;
&lt;p&gt;Invest in seed stock. Harvest scales automatically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Version control the genetics&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Prompts are genetic code. Version controlled, A/B tested &amp;amp; iterated daily.&lt;/p&gt;
&lt;p&gt;Prompt engineering is genetic engineering.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Solo operator, ecosystem leverage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;One person. One permacomputer. 58 implementations in 5 days.&lt;/p&gt;
&lt;p&gt;Not through heroic effort. Through ecosystem design.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="the-harvest"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;the harvest&lt;/a&gt;&lt;/h2&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;454 pages&lt;/li&gt;
&lt;li&gt;58 tested implementations&lt;/li&gt;
&lt;li&gt;Complete Docker configs&lt;/li&gt;
&lt;li&gt;Public domain code (&lt;a class="reference external" href="https://git.unturf.com/engineering/unturf/uncloseai.com/-/tree/master/languages"&gt;git repo with all 58 implementations&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;$42, includes free uncloseai.com API access&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;More importantly: proved permacomputer can grow technical reference material at scale.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="the-evolution"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;the evolution&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;unturf. is a loose-knit collective of hackers disrupting wheels &amp;amp; fostering open collaboration.&lt;/p&gt;
&lt;p&gt;We run &lt;a class="reference external" href="https://uncloseai.com/"&gt;uncloseai.com&lt;/a&gt; - free machine learning inference services.&lt;/p&gt;
&lt;p&gt;We operate &lt;a class="reference external" href="https://git.unturf.com/"&gt;git.unturf.com&lt;/a&gt; - public domain code repositories.&lt;/p&gt;
&lt;p&gt;We're growing Remarkbox, MakePostSell &amp;amp; now uncloseai as permacomputer crops.&lt;/p&gt;
&lt;p&gt;Not a company, an ecosystem of software permaculture.&lt;/p&gt;
&lt;p&gt;The same patterns that grew this book in 5 days grow everything:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;API documentation&lt;/li&gt;
&lt;li&gt;Code example libraries&lt;/li&gt;
&lt;li&gt;Multi-language SDKs&lt;/li&gt;
&lt;li&gt;Tutorial content&lt;/li&gt;
&lt;li&gt;Technical training materials&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Plant seeds, build automation, let ML propagate &amp;amp; harvest continuously.&lt;/p&gt;
&lt;p&gt;Traditional publishing: manual labor at typing speed.&lt;/p&gt;
&lt;p&gt;Permacomputer publishing: automated cultivation at validation pipeline speed.&lt;/p&gt;
&lt;p&gt;Not one person writing code.&lt;/p&gt;
&lt;p&gt;A collective cultivating an ecosystem.&lt;/p&gt;
&lt;p&gt;Disrupting wheels. Fostering open collaboration. Making Machine Learning accessible.&lt;/p&gt;
&lt;p&gt;The book grew in 5 days because we planted the right seeds in the right soil with the right automation &amp;amp; cultivation pipeline.&lt;/p&gt;
&lt;p&gt;Permacomputer agriculture.&lt;/p&gt;
&lt;p&gt;Join us: &lt;a class="reference external" href="https://www.unturf.com/"&gt;unturf.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Grab the harvest: &lt;a class="reference external" href="https://shop.unturf.com/p/8486f492-a93e-11f0-b477-02dfe05770ee/uncloseai-machine-learning-reference-guide-to-inference-clients"&gt;uncloseai reference manual&lt;/a&gt;&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#a-permacomputer" id="toc-entry-1"&gt;a permacomputer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-seed" id="toc-entry-2"&gt;the seed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-growth-cycle" id="toc-entry-3"&gt;the growth cycle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#a-permacomputer-mindset" id="toc-entry-4"&gt;a permacomputer mindset&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-harvest" id="toc-entry-5"&gt;the harvest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-evolution" id="toc-entry-6"&gt;the evolution&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Machine Learning"/><category term="DevOps"/><category term="Automation"/><category term="Python"/><category term="Project"/><category term="Docker"/></entry><entry><title>pelican theme upgrade: right sidebar table of contents</title><link href="https://russell.ballestrini.net/pelican-theme-upgrade-right-sidebar-toc/" rel="alternate"/><published>2025-10-11T12:00:00-04:00</published><updated>2025-10-11T12:00:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2025-10-11:/pelican-theme-upgrade-right-sidebar-toc/</id><summary type="html">&lt;p&gt;&lt;strong&gt;The companion&lt;/strong&gt; &lt;a class="reference external" href="https://github.com/russellballestrini/pelican-svbhack"&gt;pelican-svbhack theme repo&lt;/a&gt; &lt;strong&gt;lives here.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We upgraded the pelican-svbhack theme to add a right sidebar table of contents. This enhancement improves navigation on longer articles by providing quick access to section headings. The theme now features responsive breakpoints, dark mode with theme-aware syntax highlighting, and progressive enhancement for …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;The companion&lt;/strong&gt; &lt;a class="reference external" href="https://github.com/russellballestrini/pelican-svbhack"&gt;pelican-svbhack theme repo&lt;/a&gt; &lt;strong&gt;lives here.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We upgraded the pelican-svbhack theme to add a right sidebar table of contents. This enhancement improves navigation on longer articles by providing quick access to section headings. The theme now features responsive breakpoints, dark mode with theme-aware syntax highlighting, and progressive enhancement for JavaScript-dependent features.&lt;/p&gt;
&lt;div class="section" id="the-design"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;the design&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The implementation follows these principles:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;strong&gt;Desktop experience&lt;/strong&gt;: Fixed right sidebar at 10% width displaying the table of contents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mobile experience&lt;/strong&gt;: Table of contents remains inline within the article content&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pure CSS&lt;/strong&gt;: No JavaScript required for positioning or behavior&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fixed header&lt;/strong&gt;: Navigation stays visible while scrolling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CSS variables&lt;/strong&gt;: All colors use variables for easy theming&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="key-features"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;key features&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Right Sidebar TOC&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On desktop screens (1024px+), the table of contents automatically moves from inline to a fixed right sidebar. The sidebar scrolls independently from the main content, with the scrollbar hidden for a clean appearance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Article-Only Display&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The right sidebar only appears on article pages, not on the homepage or archive pages. This is accomplished using a body class system that targets article pages specifically.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fixed Header&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The main navigation header now uses &lt;tt class="docutils literal"&gt;position: fixed&lt;/tt&gt; so it stays visible when scrolling. All padding and spacing was adjusted to accommodate this change.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Responsive Design&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On tablets and mobile devices (screens ≤1023px), the table of contents reverts to its inline position within the article content, ensuring usability across all screen sizes. The media queries are structured to unfix positioned elements first, then reposition them, creating smooth visual transitions at breakpoints.&lt;/p&gt;
&lt;p&gt;Article content uses fluid width (&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;max-width:&lt;/span&gt; 660px; width: 100%&lt;/tt&gt;) instead of fixed width, allowing it to shrink responsively when the TOC sidebar is present. This prevents overlap at screen widths between 1024px and approximately 1300px, where the fixed header, article content, and TOC sidebar would otherwise compete for space.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CSS Color Variables&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;All hardcoded colors were converted to CSS custom properties:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;--color-white&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;--color-black&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;--color-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#4d4d4d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;--color-link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#0084B4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;--color-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#eeeeee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;/* ... and more */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This makes it trivial to create new color schemes or dark mode variants.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="implementation-details"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;implementation details&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The right sidebar uses CSS positioning to reposition the existing inline table of contents:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;article&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;article_text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;contents&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="kt"&gt;%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="kt"&gt;vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;overflow-y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;/* Hide scrollbar but keep functionality */&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;scrollbar-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kp"&gt;-ms-&lt;/span&gt;&lt;span class="n"&gt;overflow-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When the TOC is present, the main content width adjusts using the &lt;tt class="docutils literal"&gt;:has()&lt;/tt&gt; pseudo-class:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;article&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;article_text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;contents&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="kt"&gt;%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="homepage-summary-filtering"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;homepage summary filtering&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;ReStructuredText automatically generates anchor links in section headings when a table of contents is present. These anchors work perfectly on article pages, but become broken links when article summaries appear on the homepage - the anchor targets don't exist in that context.&lt;/p&gt;
&lt;p&gt;To solve this, we created a Pelican plugin that provides a &lt;tt class="docutils literal"&gt;strip_anchors&lt;/tt&gt; Jinja2 filter. This filter removes TOC divs and &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;toc-backref&lt;/span&gt;&lt;/tt&gt; anchor links from article summaries before they're rendered on the homepage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; We submitted &lt;a class="reference external" href="https://github.com/getpelican/pelican/pull/3512"&gt;Pull Request #3512&lt;/a&gt; to add this functionality directly to Pelican core. Once merged, this will work automatically without needing the plugin or template filter.&lt;/p&gt;
&lt;!-- TODO: Remove lib/strip_toc_summary.py plugin and strip_anchors filter from index.html template when PR #3512 is merged into Pelican upstream. --&gt;
&lt;p&gt;&lt;strong&gt;The Plugin&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The plugin (&lt;tt class="docutils literal"&gt;lib/strip_toc_summary.py&lt;/tt&gt;) uses regex to strip problematic HTML:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;strip_anchors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Jinja2 filter to strip TOC and anchor links from HTML text.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;

    &lt;span class="c1"&gt;# Remove the entire &amp;lt;div class=&amp;quot;contents&amp;quot;&amp;gt; ... &amp;lt;/div&amp;gt; block&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;div\s+class=&amp;quot;contents[^&amp;quot;]*&amp;quot;[^&amp;gt;]*&amp;gt;.*?&amp;lt;/div&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOTALL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Remove anchor links from headings&lt;/span&gt;
    &lt;span class="c1"&gt;# Converts &amp;lt;a class=&amp;quot;toc-backref&amp;quot; href=&amp;quot;#id1&amp;quot;&amp;gt;text&amp;lt;/a&amp;gt; to just text&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;a[^&amp;gt;]*class=&amp;quot;[^&amp;quot;]*toc-backref[^&amp;quot;]*&amp;quot;[^&amp;gt;]*&amp;gt;(.*?)&amp;lt;/a&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOTALL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Template Usage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The filter is applied in the theme's &lt;tt class="docutils literal"&gt;index.html&lt;/tt&gt; template:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="x"&gt;&amp;lt;div class=&amp;quot;article_text&amp;quot;&amp;gt;&lt;/span&gt;
&lt;span class="x"&gt;  &lt;/span&gt;&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;article.summary&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;strip_anchors&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="x"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This approach ensures that:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Article pages retain their TOC anchor links for proper navigation&lt;/li&gt;
&lt;li&gt;Homepage summaries display clean headings without broken links&lt;/li&gt;
&lt;li&gt;The filtering happens at template render time, not during content generation&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Why This Matters&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Without this filter, homepage excerpts would contain links like &lt;tt class="docutils literal"&gt;&amp;lt;a &lt;span class="pre"&gt;href=&amp;quot;#the-design&amp;quot;&amp;gt;the&lt;/span&gt; &lt;span class="pre"&gt;design&amp;lt;/a&amp;gt;&lt;/span&gt;&lt;/tt&gt; that point to anchors that don't exist on the homepage. This creates a poor user experience with broken navigation. The filter strips these anchors while preserving the heading text, resulting in clean, functional homepage previews.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="dark-mode-implementation"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;dark mode implementation&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After publishing this post, we immediately implemented dark mode using the CSS variable system! The dark theme is now the default, with a light mode toggle available in both desktop and mobile navigation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The dark mode implementation includes:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;strong&gt;Default dark theme&lt;/strong&gt; with light mode as the alternative&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;localStorage persistence&lt;/strong&gt; - your theme preference is saved across sessions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flash-of-unstyled-content prevention&lt;/strong&gt; - inline script in &lt;tt class="docutils literal"&gt;&amp;lt;head&amp;gt;&lt;/tt&gt; applies theme before page renders&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Toggle buttons&lt;/strong&gt; in both desktop header and mobile menu&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inverted colors&lt;/strong&gt; for third-party widgets like UncloseAI button&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Theme Colors&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The dark mode uses these color overrides:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;light-mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;--color-white&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;--color-black&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;--color-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#333333&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;--color-link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#0084B4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;--color-link-visited&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#551A8B&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;--color-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#dddddd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;--color-code-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#f5f5f5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;--color-background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;--color-header-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;By inverting the black and white variables and adjusting the other colors, the entire theme switches seamlessly between dark and light modes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flash-of-Unstyled-Content Prevention&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To prevent the flash-of-unstyled-content when loading a saved theme preference, an inline script runs synchronously in the &lt;tt class="docutils literal"&gt;&amp;lt;head&amp;gt;&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;savedTheme&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;theme&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedTheme&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;===&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;light&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;light-mode&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This applies the theme class before the page renders, eliminating any visual flash.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Button Text Synchronization&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The toggle buttons need to show the opposite of the current theme (when in dark mode, button says &amp;quot;Light&amp;quot;). This is handled by another inline script at the bottom of the page that executes after the buttons are in the DOM but before the page is visible:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;savedTheme&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;theme&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedTheme&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;===&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;light&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;desktopButton&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;theme-toggle&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mobileButton&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mobile-theme-toggle&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;desktopButton&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;desktopButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Dark&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mobileButton&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mobileButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Dark&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Third-Party Widget Styling&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The UncloseAI button required special handling with hardcoded colors to override its default styling:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c"&gt;/* Dark mode (default) - white background, black text */&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;uncloseai-floating-button&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="nn"&gt;floating-ai-button&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;solid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Light mode - black background, white text */&lt;/span&gt;
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;light-mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;uncloseai-floating-button&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;light-mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="nn"&gt;floating-ai-button&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;solid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mh"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The UncloseAI widget dynamically sets CSS custom properties for its button colors, but our CSS naturally overrides these defaults because our theme stylesheet loads after the widget's CSS. No &lt;tt class="docutils literal"&gt;!important&lt;/tt&gt; flags are needed - standard CSS cascade rules are sufficient.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Progressive Enhancement - Hiding Buttons Without JavaScript&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The theme toggle buttons only work when JavaScript is enabled, so they should be hidden when JavaScript is disabled. This is accomplished using progressive enhancement:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c"&gt;/* Hide theme toggle buttons by default */&lt;/span&gt;
&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="nn"&gt;theme-toggle&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="nn"&gt;mobile-theme-toggle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* Show buttons only when JavaScript is enabled */&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;js-enabled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="nn"&gt;theme-toggle&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;js-enabled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;#&lt;/span&gt;&lt;span class="nn"&gt;mobile-theme-toggle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;inline-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;js-enabled&lt;/span&gt;&lt;/tt&gt; class is added to the document element in the same inline script that handles flash-of-unstyled-content prevention:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Add js-enabled class to show theme toggle buttons&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;js-enabled&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;savedTheme&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;theme&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedTheme&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;===&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;light&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;light-mode&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This approach ensures the buttons are never visible if they can't function. Without JavaScript, users still get a perfectly usable site in the default dark theme - they just don't see non-functional toggle buttons cluttering the interface.&lt;/p&gt;
&lt;p&gt;This technique follows the same principles we wrote about in the &lt;a class="reference external" href="/capability-driven-presentation/"&gt;capability driven presentation&lt;/a&gt; post from 2016. For more on resilient web design philosophy, I highly recommend the free online book &lt;a class="reference external" href="https://resilientwebdesign.com/"&gt;Resilient Web Design&lt;/a&gt; by Jeremy Keith.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Syntax Highlighting Theme Switching&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Code syntax highlighting needed theme-aware styling since most color schemes are designed for either light or dark backgrounds. We implemented automatic switching between Pygments themes:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;Dark mode&lt;/strong&gt;: Uses the &lt;tt class="docutils literal"&gt;monokai&lt;/tt&gt; style with bright colors on dark backgrounds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Light mode&lt;/strong&gt;: Uses the &lt;tt class="docutils literal"&gt;default&lt;/tt&gt; style with dark colors on light backgrounds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The implementation loads both stylesheets but disables the inactive one:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="cm"&gt;&amp;lt;!-- Dark mode syntax highlighting (default) --&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;link&lt;/span&gt; &lt;span class="na"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;stylesheet&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/theme/css/pygments-dark.css&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;pygments-dark&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="cm"&gt;&amp;lt;!-- Light mode syntax highlighting --&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;link&lt;/span&gt; &lt;span class="na"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;stylesheet&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/theme/css/pygments-light.css&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;pygments-light&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The theme toggle function switches between them by enabling/disabling the stylesheets:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;toggleTheme&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pygmentsDark&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;pygments-dark&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pygmentsLight&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;pygments-light&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;light-mode&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Switching to dark mode&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;pygmentsDark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;pygmentsLight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Switching to light mode&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;pygmentsDark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;pygmentsLight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The flash-of-unstyled-content prevention script also switches the syntax highlighting stylesheet before the page renders, ensuring code blocks always appear with the correct colors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Preventing Scroll Jump with URL Anchors&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When toggling themes on a page with a URL anchor (like &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;#syntax-highlighting-theme-switching&lt;/span&gt;&lt;/tt&gt;), the browser would jump to the anchor after theme changes caused layout reflow. This creates poor UX when users toggle themes mid-article.&lt;/p&gt;
&lt;p&gt;The solution temporarily removes the hash from the URL during theme toggle, then restores it without triggering navigation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;toggleTheme&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Save hash and temporarily remove to prevent jump&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replaceState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// ... perform theme toggle ...&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Restore hash without jumping&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replaceState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;By removing the hash before theme changes, the browser has no anchor to jump to during layout reflow. The hash is restored afterward, preserving the URL without triggering the browser's default anchor-jumping behavior.&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-design" id="toc-entry-1"&gt;the design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#key-features" id="toc-entry-2"&gt;key features&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#implementation-details" id="toc-entry-3"&gt;implementation details&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#homepage-summary-filtering" id="toc-entry-4"&gt;homepage summary filtering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#dark-mode-implementation" id="toc-entry-5"&gt;dark mode implementation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Python"/></entry><entry><title>webwords code golf: minimal HTTP servers in multiple languages</title><link href="https://russell.ballestrini.net/webwords-code-golf-minimal-implementation/" rel="alternate"/><published>2025-10-10T16:00:00-04:00</published><updated>2025-10-10T16:00:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2025-10-10:/webwords-code-golf-minimal-implementation/</id><summary type="html">&lt;p&gt;The full webwords implementations have proper error handling, logging, and structure. But what's the absolute minimum code needed to implement the core functionality?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The companion&lt;/strong&gt; &lt;a class="reference external" href="https://github.com/russellballestrini/webwords"&gt;webwords git repo&lt;/a&gt; &lt;strong&gt;lives here.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="section" id="the-challenge"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;the challenge&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Implement webwords functionality in the fewest Python characters possible:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Accept HTTP requests on port 31337&lt;/li&gt;
&lt;li&gt;Parse &lt;tt class="docutils literal"&gt;keyword&lt;/tt&gt; and …&lt;/li&gt;&lt;/ol&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;The full webwords implementations have proper error handling, logging, and structure. But what's the absolute minimum code needed to implement the core functionality?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The companion&lt;/strong&gt; &lt;a class="reference external" href="https://github.com/russellballestrini/webwords"&gt;webwords git repo&lt;/a&gt; &lt;strong&gt;lives here.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="section" id="the-challenge"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;the challenge&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Implement webwords functionality in the fewest Python characters possible:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Accept HTTP requests on port 31337&lt;/li&gt;
&lt;li&gt;Parse &lt;tt class="docutils literal"&gt;keyword&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;target&lt;/tt&gt; parameters&lt;/li&gt;
&lt;li&gt;Fetch target URI content&lt;/li&gt;
&lt;li&gt;Return &lt;tt class="docutils literal"&gt;true&lt;/tt&gt; if keyword found, &lt;tt class="docutils literal"&gt;false&lt;/tt&gt; otherwise&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="python-solution"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;python solution&lt;/a&gt;&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;http.server&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;h&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;urllib.request&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;u&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="nn"&gt;urllib.parse&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;S&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BaseHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
 &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;do_GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse_qsl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end_headers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;keyword&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;target&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;31337&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serve_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Character count: 314&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="c-solution"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;c solution&lt;/a&gt;&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;stdlib.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;string.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;unistd.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;sys/socket.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;netinet/in.h&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;curl/curl.h&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;M&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;;};&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;w&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;M&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;realloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;}&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&lt;span class="n"&gt;CURL&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="n"&gt;curl_global_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;curl_easy_init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&lt;span class="n"&gt;curl_easy_setopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;curl_easy_setopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;20011&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;curl_easy_setopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;curl_easy_perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;curl_easy_cleanup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);}&lt;/span&gt;&lt;span class="n"&gt;curl_global_cleanup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;}&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;HTTP/1.1 200 OK&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Content-Length: %zu&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="s"&gt;%s&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);}&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;sockaddr_in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;htons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;31337&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="n"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4095&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;strchr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sc"&gt;&amp;#39;?&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;strstr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;target=&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;strstr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;keyword=&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))){&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;strchr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sc"&gt;&amp;#39;&amp;amp;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;strchr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sc"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;strchr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sc"&gt;&amp;#39;&amp;amp;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;strchr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sc"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;strstr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;true&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;false&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;false&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;);}&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;false&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;);}}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Character count: 1,232&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="how-they-work"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;how they work&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Python version (314 chars):&lt;/strong&gt;&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Compressed imports with short aliases&lt;/li&gt;
&lt;li&gt;Minimal variable names (&lt;tt class="docutils literal"&gt;s&lt;/tt&gt; for self, &lt;tt class="docutils literal"&gt;q&lt;/tt&gt; for query)&lt;/li&gt;
&lt;li&gt;Chained operations on single lines&lt;/li&gt;
&lt;li&gt;No error handling or validation&lt;/li&gt;
&lt;li&gt;Direct string conversion of boolean result&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;C version (1,232 chars):&lt;/strong&gt;&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Single-character variable names&lt;/li&gt;
&lt;li&gt;Removes all whitespace and comments&lt;/li&gt;
&lt;li&gt;Uses curl magic numbers instead of constants&lt;/li&gt;
&lt;li&gt;Minimal HTTP response formatting&lt;/li&gt;
&lt;li&gt;No error checking or memory leak prevention&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="docker-implementation"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;docker implementation&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Python Dockerfile:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;alpine:latest&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/bin/webwords&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;31337&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;golf.py&lt;span class="w"&gt; &lt;/span&gt;/bin/webwords
&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0754&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/bin/webwords
&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;apk&lt;span class="w"&gt; &lt;/span&gt;--no-cache&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;ca-certificates&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;/var/cache/apk/*
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;C Dockerfile:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;alpine:latest&lt;/span&gt;
&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;apk&lt;span class="w"&gt; &lt;/span&gt;--no-cache&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;gcc&lt;span class="w"&gt; &lt;/span&gt;musl-dev&lt;span class="w"&gt; &lt;/span&gt;curl-dev
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;golf.c&lt;span class="w"&gt; &lt;/span&gt;.
&lt;span class="k"&gt;RUN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gcc&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;webwords&lt;span class="w"&gt; &lt;/span&gt;golf.c&lt;span class="w"&gt; &lt;/span&gt;-lcurl
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;31337&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;./webwords&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="testing"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;testing&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Python golf:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;Dockerfile.golf&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;webwords-golf&lt;span class="w"&gt; &lt;/span&gt;.
docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;31337&lt;/span&gt;:31337&lt;span class="w"&gt; &lt;/span&gt;webwords-golf
curl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;http://localhost:31337/?keyword=potato&amp;amp;target=https://www.remarkbox.com&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;C golf:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;Dockerfile.cgolf&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;webwords-cgolf&lt;span class="w"&gt; &lt;/span&gt;.
docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;31337&lt;/span&gt;:31337&lt;span class="w"&gt; &lt;/span&gt;webwords-cgolf
curl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;http://localhost:31337/?keyword=potato&amp;amp;target=https://www.remarkbox.com&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Both return &lt;tt class="docutils literal"&gt;true&lt;/tt&gt; for potato and &lt;tt class="docutils literal"&gt;false&lt;/tt&gt; for lemon, just like the full implementations.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="comparison"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-7"&gt;comparison&lt;/a&gt;&lt;/h2&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;Full webwords Python&lt;/strong&gt;: 50+ lines, proper structure, error handling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full webwords C&lt;/strong&gt;: 268 lines, proper structure, error handling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python golf&lt;/strong&gt;: 3 lines, 314 characters, minimal functionality&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C golf&lt;/strong&gt;: 1 line, 1,232 characters, minimal functionality&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Functionality&lt;/strong&gt;: Identical for valid requests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These golf versions prove webwords' core concept can be distilled to its absolute essence while remaining functional across different programming paradigms.&lt;/p&gt;
&lt;img alt="unpotato" class="align-center" src="/uploads/2025/10/unpotato.jpg" style="width: 480px;" /&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-challenge" id="toc-entry-1"&gt;the challenge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#python-solution" id="toc-entry-2"&gt;python solution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#c-solution" id="toc-entry-3"&gt;c solution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#how-they-work" id="toc-entry-4"&gt;how they work&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#docker-implementation" id="toc-entry-5"&gt;docker implementation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#testing" id="toc-entry-6"&gt;testing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#comparison" id="toc-entry-7"&gt;comparison&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Python"/><category term="Docker"/></entry><entry><title>unpotato webwords: cleaning up btrfs docker volumes after build-all</title><link href="https://russell.ballestrini.net/unpotato-webwords-btrfs-cleanup-docker-volumes/" rel="alternate"/><published>2025-10-10T14:00:00-04:00</published><updated>2025-10-10T14:00:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2025-10-10:/unpotato-webwords-btrfs-cleanup-docker-volumes/</id><summary type="html">&lt;p&gt;So you ran &lt;tt class="docutils literal"&gt;make &lt;span class="pre"&gt;build-all&lt;/span&gt;&lt;/tt&gt; on &lt;a class="reference external" href="/webwords-reaches-42-languages-the-ultimate-programming-kata"&gt;webwords&lt;/a&gt; and now your root filesystem is 100% full. BTRFS doesn't clean up Docker volumes the way you'd expect. Here's how to unpotato your system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The companion&lt;/strong&gt; &lt;a class="reference external" href="https://github.com/russellballestrini/webwords"&gt;webwords git repo&lt;/a&gt; &lt;strong&gt;lives here.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="section" id="the-problem"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;the problem&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Docker's &lt;tt class="docutils literal"&gt;system prune&lt;/tt&gt; doesn't fully clean up BTRFS subvolumes. Even …&lt;/p&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;So you ran &lt;tt class="docutils literal"&gt;make &lt;span class="pre"&gt;build-all&lt;/span&gt;&lt;/tt&gt; on &lt;a class="reference external" href="/webwords-reaches-42-languages-the-ultimate-programming-kata"&gt;webwords&lt;/a&gt; and now your root filesystem is 100% full. BTRFS doesn't clean up Docker volumes the way you'd expect. Here's how to unpotato your system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The companion&lt;/strong&gt; &lt;a class="reference external" href="https://github.com/russellballestrini/webwords"&gt;webwords git repo&lt;/a&gt; &lt;strong&gt;lives here.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="section" id="the-problem"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;the problem&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Docker's &lt;tt class="docutils literal"&gt;system prune&lt;/tt&gt; doesn't fully clean up BTRFS subvolumes. Even after removing containers and images, the filesystem usage stays high because the underlying BTRFS subvolumes persist.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="identify-docker-volumes"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;identify docker volumes&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;List all Docker volumes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;volume&lt;span class="w"&gt; &lt;/span&gt;ls
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Check which containers might be using volumes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;ps&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;--filter&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;volume&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;volume_name
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="delete-docker-volumes"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;delete docker volumes&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Stop and remove any containers using the volumes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;stop&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;ps&lt;span class="w"&gt; &lt;/span&gt;-aq&lt;span class="k"&gt;)&lt;/span&gt;
docker&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;ps&lt;span class="w"&gt; &lt;/span&gt;-aq&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Remove all unused volumes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;volume&lt;span class="w"&gt; &lt;/span&gt;prune&lt;span class="w"&gt; &lt;/span&gt;-f
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="manual-btrfs-cleanup"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;manual btrfs cleanup&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If filesystem usage is still high, manually clean up BTRFS subvolumes.&lt;/p&gt;
&lt;p&gt;Check Docker's storage driver:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;info&lt;span class="w"&gt; &lt;/span&gt;--format&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{{.Driver}}&amp;#39;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If it shows &lt;tt class="docutils literal"&gt;btrfs&lt;/tt&gt;, locate the subvolumes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;ls&lt;span class="w"&gt; &lt;/span&gt;/var/lib/docker/btrfs/subvolumes/
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;List all BTRFS subvolumes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;btrfs&lt;span class="w"&gt; &lt;/span&gt;subvolume&lt;span class="w"&gt; &lt;/span&gt;list&lt;span class="w"&gt; &lt;/span&gt;/var/lib/docker
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Delete persistent subvolumes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;btrfs&lt;span class="w"&gt; &lt;/span&gt;subvolume&lt;span class="w"&gt; &lt;/span&gt;delete&lt;span class="w"&gt; &lt;/span&gt;/var/lib/docker/btrfs/subvolumes/HASH_HERE
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Wait for async deletion to complete:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;btrfs&lt;span class="w"&gt; &lt;/span&gt;subvolume&lt;span class="w"&gt; &lt;/span&gt;sync&lt;span class="w"&gt; &lt;/span&gt;/var/lib/docker
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="the-nuclear-option-that-actually-worked"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;the nuclear option (that actually worked)&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After running unpotato, Docker's metadata became corrupted with references to missing BTRFS subvolumes. Simple restarts and partial cleanups failed with errors like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;failed&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;register&lt;span class="w"&gt; &lt;/span&gt;layer:&lt;span class="w"&gt; &lt;/span&gt;stat&lt;span class="w"&gt; &lt;/span&gt;/var/lib/docker/btrfs/subvolumes/53a19da3801c895b697625094ebc8f95668a212a4cbc923787ac0c86452d9f60:&lt;span class="w"&gt; &lt;/span&gt;no&lt;span class="w"&gt; &lt;/span&gt;such&lt;span class="w"&gt; &lt;/span&gt;file&lt;span class="w"&gt; &lt;/span&gt;or&lt;span class="w"&gt; &lt;/span&gt;directory
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The root cause: Docker's image metadata and containerd's content store still referenced layers that no longer existed in BTRFS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Partial cleanup attempts that failed:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# These didn&amp;#39;t fix the corruption:&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;restart&lt;span class="w"&gt; &lt;/span&gt;docker
sudo&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;/var/lib/docker/image/btrfs/layerdb/*
sudo&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;/var/lib/containerd/io.containerd.content.v1.content/*
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;The only solution that worked:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Complete reset of both Docker and containerd data stores:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;stop&lt;span class="w"&gt; &lt;/span&gt;docker
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;stop&lt;span class="w"&gt; &lt;/span&gt;containerd
sudo&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;/var/lib/docker/*
sudo&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;/var/lib/containerd/*
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;containerd
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;docker
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This deletes all Docker data including images, containers, volumes, and networks. But it's the only way to recover from corrupted layer metadata after aggressive BTRFS cleanup.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="the-unpotato-script"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;the unpotato script&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After going through the manual unpotato process, I created a script to automate the recovery for future incidents.&lt;/p&gt;
&lt;p&gt;## what it does&lt;/p&gt;
&lt;p&gt;The script focuses on three key operations that free space WITHOUT touching your valuable base images:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;strong&gt;Remove build cache&lt;/strong&gt; - This is the big win, often 10-30GB of intermediate layers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remove dangling images&lt;/strong&gt; - The &lt;cite&gt;&amp;lt;none&amp;gt;:&amp;lt;none&amp;gt;&lt;/cite&gt; orphaned images from failed builds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remove stopped containers&lt;/strong&gt; - Minimal space but good housekeeping&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Critical: The script uses `docker image prune -f` NOT `docker image prune -a -f`&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;cite&gt;-a&lt;/cite&gt; flag would remove ALL unused images including your expensive base images. Without it, only true dangling images are removed while all tagged images (even if not currently used) are preserved.&lt;/p&gt;
&lt;p&gt;why it works for potatoed systems&lt;/p&gt;
&lt;p&gt;When your system is critically full, most commands that try to write files will fail or hang. The unpotato script is designed to work in this state:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;No file writes until the actual cleanup operations&lt;/li&gt;
&lt;li&gt;Avoids &lt;cite&gt;df&lt;/cite&gt; during critical steps (it times out when the system is potatoed)&lt;/li&gt;
&lt;li&gt;Uses &lt;cite&gt;systemctl stop docker&lt;/cite&gt; before final check to unmount overlays&lt;/li&gt;
&lt;li&gt;Includes BTRFS sync to commit async deletions&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c1"&gt;# unpotato-docker-btrfs.sh - Emergency Docker cleanup for potatoed systems&lt;/span&gt;
&lt;span class="c1"&gt;# ONLY removes: build cache, dangling images, stopped containers&lt;/span&gt;
&lt;span class="c1"&gt;# PRESERVES: All tagged images and their layers&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;======================================&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;  Docker Emergency Unpotato&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;======================================&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;This script will ONLY remove:&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;  - Build cache (layers not in images)&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;  - Dangling images (&amp;lt;none&amp;gt;:&amp;lt;none&amp;gt;)&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;  - Stopped containers&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;This script will PRESERVE:&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;  - All tagged images&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;  - Base images&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;  - Image layers&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;======================================&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Check if running as root&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$EUID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-ne&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[ERROR] This script must be run as root (use sudo)&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c1"&gt;# Check if Docker is installed&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[ERROR] Docker is not installed&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c1"&gt;# Check filesystem type&lt;/span&gt;
&lt;span class="nv"&gt;FILESYSTEM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;stat&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;%T&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;unknown&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[INFO] Detected filesystem: &lt;/span&gt;&lt;span class="nv"&gt;$FILESYSTEM&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Step 1: Stop all running containers (no disk writes needed)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[STEP 1/5] Stopping all running containers...&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;RUNNING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;ps&lt;span class="w"&gt; &lt;/span&gt;-q&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;wc&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$RUNNING&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-gt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;stop&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;ps&lt;span class="w"&gt; &lt;/span&gt;-q&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[WARN] Some containers failed to stop&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[OK] Stopped &lt;/span&gt;&lt;span class="nv"&gt;$RUNNING&lt;/span&gt;&lt;span class="s2"&gt; containers&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[OK] No running containers&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Step 2: Remove stopped containers (minimal disk writes)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[STEP 2/5] Removing stopped containers...&amp;quot;&lt;/span&gt;
docker&lt;span class="w"&gt; &lt;/span&gt;container&lt;span class="w"&gt; &lt;/span&gt;prune&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[WARN] Container prune failed&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[OK] Stopped containers removed&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Step 3: Remove ONLY dangling images (preserves all tagged images)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[STEP 3/5] Removing dangling images only...&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[INFO] This removes &amp;lt;none&amp;gt;:&amp;lt;none&amp;gt; images ONLY&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[INFO] All tagged images will be preserved&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;DANGLING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;images&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;dangling=true&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-q&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;wc&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="k"&gt;)&lt;/span&gt;
docker&lt;span class="w"&gt; &lt;/span&gt;image&lt;span class="w"&gt; &lt;/span&gt;prune&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[WARN] Image prune failed&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[OK] Removed &lt;/span&gt;&lt;span class="nv"&gt;$DANGLING&lt;/span&gt;&lt;span class="s2"&gt; dangling images&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Step 4: Remove build cache (THE BIG WIN - usually 10-30GB)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[STEP 4/5] Removing build cache...&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[INFO] This is usually the biggest space saver&amp;quot;&lt;/span&gt;
docker&lt;span class="w"&gt; &lt;/span&gt;builder&lt;span class="w"&gt; &lt;/span&gt;prune&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[WARN] Builder prune failed&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[OK] Build cache removed&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Step 5: BTRFS sync (if applicable)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$FILESYSTEM&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;btrfs&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[STEP 5/5] Running BTRFS sync to commit deletions...&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;btrfs&lt;span class="w"&gt; &lt;/span&gt;filesystem&lt;span class="w"&gt; &lt;/span&gt;sync&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[WARN] BTRFS sync failed&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[OK] BTRFS sync completed&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[STEP 5/5] Skipping BTRFS sync (not BTRFS filesystem)&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Restart Docker to clear overlay mounts (helps df work)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[FINAL] Restarting Docker daemon...&amp;quot;&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;stop&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null
sleep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
systemctl&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null
sleep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[OK] Docker restarted&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Show results&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;======================================&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;  Cleanup Complete!&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;======================================&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Try to show space (with timeout in case still potatoed)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;timeout&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;df&lt;span class="w"&gt; &lt;/span&gt;-h&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;Filesystem&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[OK] Filesystem responding normally&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;btrfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[INFO] Using BTRFS method (df timed out):&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;btrfs&lt;span class="w"&gt; &lt;/span&gt;filesystem&lt;span class="w"&gt; &lt;/span&gt;usage&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;(Free|Used):&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[SUCCESS] Unpotato complete!&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[INFO] All your tagged images have been preserved&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;To verify your images are still there:&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;  docker images | head -20&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;usage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;`bash
# Download and run
sudo bash &lt;span class="pre"&gt;unpotato-docker-btrfs.sh&lt;/span&gt;
`&lt;/tt&gt;&lt;/p&gt;
&lt;p&gt;In my case, the script freed &lt;strong&gt;23.27GB&lt;/strong&gt; from build cache alone, bringing the system from 100% full to 79% used with 99GB free. All 90+ Docker images including expensive base images (Haskell, OCaml, R, Fortran, Julia, Scala, etc.) were preserved.&lt;/p&gt;
&lt;p&gt;The best solution is to not get potatoed in the first place. Consider:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Running &lt;cite&gt;docker builder prune -f&lt;/cite&gt; weekly as a cron job&lt;/li&gt;
&lt;li&gt;Monitoring disk usage with alerts at 80% full&lt;/li&gt;
&lt;li&gt;Using a separate filesystem or volume for &lt;cite&gt;/var/lib/docker&lt;/cite&gt;&lt;/li&gt;
&lt;li&gt;Setting up Docker with disk usage limits&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But when you do get potatoed, this script will safely get you back to operational status without losing your work.&lt;/p&gt;
&lt;img alt="unpotato" class="align-center" src="/uploads/2025/10/unpotato.jpg" style="width: 480px;" /&gt;
&lt;/div&gt;
&lt;div class="section" id="verify-cleanup"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-7"&gt;verify cleanup&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Check filesystem usage:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;df&lt;span class="w"&gt; &lt;/span&gt;-h&lt;span class="w"&gt; &lt;/span&gt;/
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The usage should drop significantly after proper cleanup.&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-problem" id="toc-entry-1"&gt;the problem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#identify-docker-volumes" id="toc-entry-2"&gt;identify docker volumes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#delete-docker-volumes" id="toc-entry-3"&gt;delete docker volumes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#manual-btrfs-cleanup" id="toc-entry-4"&gt;manual btrfs cleanup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-nuclear-option-that-actually-worked" id="toc-entry-5"&gt;the nuclear option (that actually worked)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-unpotato-script" id="toc-entry-6"&gt;the unpotato script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#verify-cleanup" id="toc-entry-7"&gt;verify cleanup&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Docker"/></entry><entry><title>webwords reaches 42 languages: the ultimate programming kata</title><link href="https://russell.ballestrini.net/webwords-reaches-42-languages-the-ultimate-programming-kata/" rel="alternate"/><published>2025-10-09T12:00:00-04:00</published><updated>2025-10-09T12:00:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2025-10-09:/webwords-reaches-42-languages-the-ultimate-programming-kata/</id><summary type="html">&lt;p&gt;&lt;strong&gt;The companion&lt;/strong&gt; &lt;a class="reference external" href="https://github.com/russellballestrini/webwords"&gt;webwords git repo&lt;/a&gt; &lt;strong&gt;lives here.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Webwords now supports 42 programming languages.&lt;/strong&gt; TimeHexOn assisted with this milestone over 2 days using a code agent tool.&lt;/p&gt;
&lt;p&gt;42 languages. Same as the answer to life, the universe, and everything. Coincidence? Probably. But each implementation solves the exact same problem in completely …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;The companion&lt;/strong&gt; &lt;a class="reference external" href="https://github.com/russellballestrini/webwords"&gt;webwords git repo&lt;/a&gt; &lt;strong&gt;lives here.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Webwords now supports 42 programming languages.&lt;/strong&gt; TimeHexOn assisted with this milestone over 2 days using a code agent tool.&lt;/p&gt;
&lt;p&gt;42 languages. Same as the answer to life, the universe, and everything. Coincidence? Probably. But each implementation solves the exact same problem in completely different ways.&lt;/p&gt;
&lt;div class="section" id="the-spec"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;the spec&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Every implementation does exactly the same thing:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Accept two query parameters: &lt;tt class="docutils literal"&gt;keyword&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;target&lt;/tt&gt;&lt;/li&gt;
&lt;li&gt;Fetch the content from the target URI&lt;/li&gt;
&lt;li&gt;Search for the keyword in that content&lt;/li&gt;
&lt;li&gt;Return &lt;tt class="docutils literal"&gt;true&lt;/tt&gt; or &lt;tt class="docutils literal"&gt;false&lt;/tt&gt; as plain text&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="testing-webwords"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;testing webwords&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To test any implementation, try searching for &lt;tt class="docutils literal"&gt;potato&lt;/tt&gt; on &lt;a class="reference external" href="https://www.remarkbox.com"&gt;Remarkbox&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;http://localhost:31337/?keyword=potato&amp;amp;target=https://www.remarkbox.com&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Returns &lt;tt class="docutils literal"&gt;true&lt;/tt&gt; because potato exists on Remarkbox.&lt;/p&gt;
&lt;img alt="potato" class="align-center" src="/uploads/2025/10/potato.jpg" style="width: 480px;" /&gt;
&lt;/div&gt;
&lt;div class="section" id="the-implementations"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;the implementations&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Native HTTP Server Implementations (33 languages)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Wrapper-Based Implementations (9 languages)&lt;/strong&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="docker-deployment"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;docker deployment&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Every implementation ships with a Dockerfile:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;language-directory
docker&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;webwords-language&lt;span class="w"&gt; &lt;/span&gt;.
docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;31337&lt;/span&gt;:31337&lt;span class="w"&gt; &lt;/span&gt;webwords-language
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="the-makefile-system"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;the makefile system&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The project includes a comprehensive Makefile that manages all 42 implementations:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;build-all&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# Build all 42 Docker images&lt;/span&gt;
make&lt;span class="w"&gt; &lt;/span&gt;test-all&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="c1"&gt;# Test all implementations concurrently&lt;/span&gt;
make&lt;span class="w"&gt; &lt;/span&gt;benchmark&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;LANG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;python&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Benchmark a specific language&lt;/span&gt;
make&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="c1"&gt;# Show all available commands&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each language has individual targets:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;build-python&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c1"&gt;# Build Python implementation&lt;/span&gt;
make&lt;span class="w"&gt; &lt;/span&gt;serve-python&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c1"&gt;# Run Python container on port 31337&lt;/span&gt;
make&lt;span class="w"&gt; &lt;/span&gt;functional-test-python&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Test with potato/lemon cases&lt;/span&gt;
make&lt;span class="w"&gt; &lt;/span&gt;clean-python&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c1"&gt;# Stop and remove container&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The testing process validates that each implementation correctly returns &lt;tt class="docutils literal"&gt;true&lt;/tt&gt; when searching for &lt;tt class="docutils literal"&gt;potato&lt;/tt&gt; on &lt;tt class="docutils literal"&gt;remarkbox.com&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;false&lt;/tt&gt; when searching for &lt;tt class="docutils literal"&gt;lemon&lt;/tt&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="what-s-next"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;what's next&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Is the project complete at 42 languages? What happens next?&lt;/p&gt;
&lt;p&gt;If you run &lt;tt class="docutils literal"&gt;make &lt;span class="pre"&gt;build-all&lt;/span&gt;&lt;/tt&gt; &amp;amp; fill up your filesystem, check out &lt;a class="reference external" href="/unpotato-webwords-btrfs-cleanup-docker-volumes"&gt;unpotato webwords: cleaning up btrfs docker volumes&lt;/a&gt;.&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-spec" id="toc-entry-1"&gt;the spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#testing-webwords" id="toc-entry-2"&gt;testing webwords&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-implementations" id="toc-entry-3"&gt;the implementations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#docker-deployment" id="toc-entry-4"&gt;docker deployment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-makefile-system" id="toc-entry-5"&gt;the makefile system&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#what-s-next" id="toc-entry-6"&gt;what's next&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Programming"/><category term="Python"/><category term="Docker"/></entry><entry><title>Analyzing the Godot Engine Codebase: Millions of Lines of Code</title><link href="https://russell.ballestrini.net/analyzing-godot-engine-codebase-millions-lines-of-code/" rel="alternate"/><published>2025-07-28T09:00:00-04:00</published><updated>2025-07-28T09:00:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2025-07-28:/analyzing-godot-engine-codebase-millions-lines-of-code/</id><summary type="html">&lt;div class="section" id="overview"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Overview&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;a class="reference external" href="https://godotengine.org/"&gt;Godot engine&lt;/a&gt; is a powerful, open-source game engine that competes with commercial alternatives like Unity and Unreal. When we set out to analyze its codebase, we discovered it contains approximately &lt;strong&gt;13.13 million lines of code&lt;/strong&gt;. This analysis provides insights into the languages used and the distribution of …&lt;/p&gt;&lt;/div&gt;</summary><content type="html">&lt;div class="section" id="overview"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Overview&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;a class="reference external" href="https://godotengine.org/"&gt;Godot engine&lt;/a&gt; is a powerful, open-source game engine that competes with commercial alternatives like Unity and Unreal. When we set out to analyze its codebase, we discovered it contains approximately &lt;strong&gt;13.13 million lines of code&lt;/strong&gt;. This analysis provides insights into the languages used and the distribution of code across different components.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="findings"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Findings&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Using our comprehensive analysis methodology, here's what we discovered about the Godot engine codebase:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Total Lines of Code: 13.13 million&lt;/strong&gt;&lt;/p&gt;
&lt;div class="section" id="language-breakdown"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Language Breakdown&lt;/a&gt;&lt;/h3&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;strong&gt;C/C++ (8.6M lines - 65%)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Core engine implementation (3.3M .cpp files)&lt;/li&gt;
&lt;li&gt;Headers and interfaces (2.7M .h files, 576K .hpp, 197K .hh)&lt;/li&gt;
&lt;li&gt;Performance-critical systems&lt;/li&gt;
&lt;li&gt;Platform abstraction layers&lt;/li&gt;
&lt;li&gt;Rendering pipelines&lt;/li&gt;
&lt;li&gt;Template implementations (.inl, .inc files)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Translation Files (3.3M lines - 25%)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Multilingual support (.po files)&lt;/li&gt;
&lt;li&gt;International localization&lt;/li&gt;
&lt;li&gt;Community translations&lt;/li&gt;
&lt;li&gt;Massive global reach effort&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XML (326K lines - 2.5%)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Project configuration files&lt;/li&gt;
&lt;li&gt;Build system definitions&lt;/li&gt;
&lt;li&gt;Documentation markup&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Asset Files (240K lines)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Binary assets (.pack files with 227K lines)&lt;/li&gt;
&lt;li&gt;Font files (.woff2 with 22K lines)&lt;/li&gt;
&lt;li&gt;Platform-specific resources&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C# (110K lines)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;C# language bindings&lt;/li&gt;
&lt;li&gt;.NET integration layer&lt;/li&gt;
&lt;li&gt;C# API surface&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Java (95K lines)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Android platform support&lt;/li&gt;
&lt;li&gt;Android-specific features&lt;/li&gt;
&lt;li&gt;JNI bridge code&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GLSL (71K lines)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Shader programs&lt;/li&gt;
&lt;li&gt;Rendering effects&lt;/li&gt;
&lt;li&gt;GPU compute kernels&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Objective-C/C++ (57K lines)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;iOS platform support (.mm files)&lt;/li&gt;
&lt;li&gt;macOS integration&lt;/li&gt;
&lt;li&gt;Apple-specific features&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python (27K lines)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Build scripts&lt;/li&gt;
&lt;li&gt;Code generation tools&lt;/li&gt;
&lt;li&gt;Development utilities&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GDScript (24K lines)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Example projects&lt;/li&gt;
&lt;li&gt;Test scripts&lt;/li&gt;
&lt;li&gt;Documentation samples&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="reproducing-these-results"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Reproducing These Results&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To reproduce this analysis, save and run this script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c1"&gt;# analyze_codebase.sh - Count lines of code by language&lt;/span&gt;

&lt;span class="c1"&gt;# Usage:&lt;/span&gt;
&lt;span class="c1"&gt;#   ./analyze_codebase.sh                           # Analyze current directory&lt;/span&gt;
&lt;span class="c1"&gt;#   ./analyze_codebase.sh &amp;lt;git-url&amp;gt;                 # Download and analyze repo&lt;/span&gt;
&lt;span class="c1"&gt;# Example: ./analyze_codebase.sh https://github.com/godotengine/godot.git&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-eq&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# No arguments - analyze current directory&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Analyzing current directory: &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;ANALYSIS_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# Git URL provided - clone and analyze&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;REPO_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;REPO_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;basename&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$REPO_URL&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.git&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;ANALYSIS_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REPO_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-analysis&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Cloning &lt;/span&gt;&lt;span class="nv"&gt;$REPO_NAME&lt;/span&gt;&lt;span class="s2"&gt; from &lt;/span&gt;&lt;span class="nv"&gt;$REPO_URL&lt;/span&gt;&lt;span class="s2"&gt;...&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;--depth&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$REPO_URL&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ANALYSIS_DIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$ANALYSIS_DIR&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\n=== CODEBASE ANALYSIS ===&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Count total lines&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\nCounting total lines...&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;TOTAL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;find&lt;span class="w"&gt; &lt;/span&gt;.&lt;span class="w"&gt; &lt;/span&gt;-type&lt;span class="w"&gt; &lt;/span&gt;f&lt;span class="w"&gt; &lt;/span&gt;-not&lt;span class="w"&gt; &lt;/span&gt;-path&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;./.git/*&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;-exec&lt;span class="w"&gt; &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;+&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;wc&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Total lines in repository: &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;%&amp;#39;d&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$TOTAL&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Count by major languages&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\nLines of code by language:&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;==========================&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Define extensions and their labels&lt;/span&gt;
&lt;span class="nb"&gt;declare&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;languages&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cpp&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;C++ source&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;h&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;C/C++ headers&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;c&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;C source&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cc&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;C++ source&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cxx&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;C++ source&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;hpp&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;C++ headers&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;hh&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;C++ headers&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;hxx&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;C++ headers&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inl&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;C++ inline&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;inc&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Include files&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ipp&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;C++ inline&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ixx&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;C++ modules&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tcc&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Template impl&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tpp&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Template impl&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cs&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;C#&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;java&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Java&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;py&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Python&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gd&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;GDScript&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;js&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;JavaScript&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;glsl&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;GLSL shaders&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;xml&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;XML&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Get all extensions in the codebase&lt;/span&gt;
&lt;span class="nv"&gt;all_extensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;find&lt;span class="w"&gt; &lt;/span&gt;.&lt;span class="w"&gt; &lt;/span&gt;-type&lt;span class="w"&gt; &lt;/span&gt;f&lt;span class="w"&gt; &lt;/span&gt;-name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*.*&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-not&lt;span class="w"&gt; &lt;/span&gt;-path&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;./.git/*&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s/.*\.//&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sort&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Count known languages&lt;/span&gt;
&lt;span class="nb"&gt;declare&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;counted_extensions
&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ext&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="p"&gt;!languages[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;find&lt;span class="w"&gt; &lt;/span&gt;.&lt;span class="w"&gt; &lt;/span&gt;-name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*.&lt;/span&gt;&lt;span class="nv"&gt;$ext&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-type&lt;span class="w"&gt; &lt;/span&gt;f&lt;span class="w"&gt; &lt;/span&gt;-not&lt;span class="w"&gt; &lt;/span&gt;-path&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;./.git/*&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;-exec&lt;span class="w"&gt; &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;+&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;wc&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-gt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;%-15s %&amp;#39;10d lines\n&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$ext&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;counted_extensions&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$ext&lt;/span&gt;&lt;span class="o"&gt;]=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sort&lt;span class="w"&gt; &lt;/span&gt;-k2&lt;span class="w"&gt; &lt;/span&gt;-nr

&lt;span class="c1"&gt;# Count unknown extensions&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\nUnknown file types:&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;===================&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;unknown_total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ext&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$all_extensions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;counted_extensions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$ext&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;languages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$ext&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;find&lt;span class="w"&gt; &lt;/span&gt;.&lt;span class="w"&gt; &lt;/span&gt;-name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*.&lt;/span&gt;&lt;span class="nv"&gt;$ext&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-type&lt;span class="w"&gt; &lt;/span&gt;f&lt;span class="w"&gt; &lt;/span&gt;-not&lt;span class="w"&gt; &lt;/span&gt;-path&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;./.git/*&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;-exec&lt;span class="w"&gt; &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;+&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;wc&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-gt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;%-15s %&amp;#39;10d lines\n&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*.&lt;/span&gt;&lt;span class="nv"&gt;$ext&lt;/span&gt;&lt;span class="s2"&gt; files:&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nv"&gt;unknown_total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="nv"&gt;unknown_total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sort&lt;span class="w"&gt; &lt;/span&gt;-k2&lt;span class="w"&gt; &lt;/span&gt;-nr

&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$unknown_total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-gt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;----------------------------------------&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;%-15s %&amp;#39;10d lines\n&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Unknown total:&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$unknown_total&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\nTop 10 file types by count:&amp;quot;&lt;/span&gt;
find&lt;span class="w"&gt; &lt;/span&gt;.&lt;span class="w"&gt; &lt;/span&gt;-type&lt;span class="w"&gt; &lt;/span&gt;f&lt;span class="w"&gt; &lt;/span&gt;-name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*.*&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-not&lt;span class="w"&gt; &lt;/span&gt;-path&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;./.git/*&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s/.*\.//&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;sort&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uniq&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sort&lt;span class="w"&gt; &lt;/span&gt;-nr&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;head&lt;span class="w"&gt; &lt;/span&gt;-10
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="insights"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;Insights&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The Godot engine's codebase reveals several fascinating patterns:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;strong&gt;C++ Dominance&lt;/strong&gt;: The engine remains fundamentally a C++ project, with 8.6M lines (65%) in C/C++. This reflects the performance requirements of a modern game engine, with sophisticated template usage and inline optimizations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global Reach&lt;/strong&gt;: The most surprising finding is &lt;strong&gt;3.3M lines of translation files&lt;/strong&gt; (25% of the codebase!). This represents one of the most comprehensive internationalization efforts in open-source software, with community translations spanning dozens of languages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-Platform Architecture&lt;/strong&gt;: Significant code for Java (95K - Android), Objective-C++ (57K - Apple platforms), and C# (110K - .NET) demonstrates true cross-platform commitment at enterprise scale.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Third-Party Integration&lt;/strong&gt;: The massive 1.66M lines of C code likely comes from integrated third-party libraries (physics engines, audio codecs, image processing, compression libraries, etc.).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advanced Rendering&lt;/strong&gt;: 71K lines of GLSL shader code (doubled from our initial count) indicates extremely sophisticated rendering capabilities rivaling commercial engines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer Accessibility&lt;/strong&gt;: Native support for GDScript (24K), C# (110K), and Python (27K) bindings shows strong commitment to developer accessibility across skill levels.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Asset Pipeline&lt;/strong&gt;: 240K lines dedicated to asset files (.pack, .woff2, binary resources) reveals a comprehensive content pipeline system.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Professional Tooling&lt;/strong&gt;: Extensive build systems, IDE integration files, and development utilities show this is production-grade software infrastructure.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="real-world-godot-in-action"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;Real-World Godot in Action&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;While the Godot engine itself contains millions of lines of code, games built with it can be surprisingly concise. Our own game, &lt;strong&gt;Piano Roll Man&lt;/strong&gt;, demonstrates this efficiency with just &lt;strong&gt;10,500 lines of GDScript&lt;/strong&gt; - proving you don't need massive codebases to create engaging experiences.&lt;/p&gt;
&lt;a class="reference external image-reference" href="https://shop.gumyum.com/p/f0b24565-6745-11f0-9f8a-02dfe05770ee/piano-roll-man"&gt;
&lt;img alt="Piano Roll Man gameplay" src="https://make-post-sell-files.nyc3.cdn.digitaloceanspaces.com/f2ce0b94-6723-11f0-a787-02dfe05770ee/f0b24565-6745-11f0-9f8a-02dfe05770ee/thumbnail1?ts=1753702865665" style="width: 600px;" /&gt;
&lt;/a&gt;
&lt;p&gt;Piano Roll Man is a unique music-based game that lets you load any MIDI file from the internet and play through it as a level. With a full level editor and the ability to share levels by simply sharing MIDI files, it showcases Godot's flexibility in creating innovative gameplay mechanics.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://shop.gumyum.com/p/f0b24565-6745-11f0-9f8a-02dfe05770ee/piano-roll-man"&gt;Get Piano Roll Man now for just $6 during alpha!&lt;/a&gt; (Price increases to $12 in beta and $18 at release) Available for Linux, Windows, and Mac - your purchase includes all future releases as we shape the game together with our alpha community.&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#overview" id="toc-entry-1"&gt;Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#findings" id="toc-entry-2"&gt;Findings&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#language-breakdown" id="toc-entry-3"&gt;Language Breakdown&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#reproducing-these-results" id="toc-entry-4"&gt;Reproducing These Results&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#insights" id="toc-entry-5"&gt;Insights&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#real-world-godot-in-action" id="toc-entry-6"&gt;Real-World Godot in Action&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Opinion"/><category term="Project"/><category term="Game Development"/><category term="Python"/></entry><entry><title>Building and Modding MultiMower with Dry Engine</title><link href="https://russell.ballestrini.net/building-and-modding-multimower-with-dry-engine/" rel="alternate"/><published>2025-07-13T10:00:00-04:00</published><updated>2025-07-13T10:00:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2025-07-13:/building-and-modding-multimower-with-dry-engine/</id><summary type="html">&lt;p class="first last"&gt;Learn how to download, compile, and mod MultiMower - a multiplayer lawn mower game built with the Dry Engine. We'll explore the codebase structure.&lt;/p&gt;
</summary><content type="html">&lt;div class="section" id="resources-and-community"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Resources and Community&lt;/a&gt;&lt;/h2&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;MultiMower Repository&lt;/strong&gt;: &lt;a class="reference external" href="https://gitlab.com/luckeyproductions/games/MultiMower"&gt;https://gitlab.com/luckeyproductions/games/MultiMower&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dry Engine&lt;/strong&gt;: &lt;a class="reference external" href="https://gitlab.com/luckeyproductions/dry"&gt;https://gitlab.com/luckeyproductions/dry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dry Engine Documentation&lt;/strong&gt;: Available in the Dry repository&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="section" id="quick-start-summary"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Quick Start Summary&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For experienced developers, here's the TL;DR version:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Install dependencies on Fedora&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;cmake&lt;span class="w"&gt; &lt;/span&gt;gcc&lt;span class="w"&gt; &lt;/span&gt;gcc-c++&lt;span class="w"&gt; &lt;/span&gt;qt5-qtbase-devel&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;libX11-devel&lt;span class="w"&gt; &lt;/span&gt;libXrandr-devel&lt;span class="w"&gt; &lt;/span&gt;alsa-lib-devel&lt;span class="w"&gt; &lt;/span&gt;mesa-libEGL-devel&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;wayland-devel&lt;span class="w"&gt; &lt;/span&gt;wayland-protocols-devel

&lt;span class="c1"&gt;# Build Dry engine&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git
git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://gitlab.com/luckeyproductions/dry.git
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dry&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;build
cmake&lt;span class="w"&gt; &lt;/span&gt;..&lt;span class="w"&gt; &lt;/span&gt;-DDRY_64BIT&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-DCMAKE_BUILD_TYPE&lt;span class="o"&gt;=&lt;/span&gt;Release&lt;span class="w"&gt; &lt;/span&gt;-DVIDEO_WAYLAND&lt;span class="o"&gt;=&lt;/span&gt;OFF
make&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="k"&gt;$(&lt;/span&gt;nproc&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Build MultiMower (CRITICAL: set DRY_HOME first!)&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;DRY_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/git/dry/build
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git
git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://gitlab.com/luckeyproductions/games/MultiMower.git
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;MultiMower&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;build
qmake&lt;span class="w"&gt; &lt;/span&gt;../MultiMower.pro&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="k"&gt;$(&lt;/span&gt;nproc&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Run the game&lt;/span&gt;
cp&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;../Resources&lt;span class="w"&gt; &lt;/span&gt;.
./multimower
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="what-you-ll-learn"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;What You'll Learn&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;By building and modding MultiMower, you'll gain experience with:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;C++ Game Development&lt;/strong&gt;: Modern C++11 with game-specific patterns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Physics Programming&lt;/strong&gt;: Bullet Physics integration for vehicles and projectiles&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3D Graphics&lt;/strong&gt;: OpenGL with custom shaders and post-processing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Game Architecture&lt;/strong&gt;: Component systems, scene graphs, and input handling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Asset Pipeline&lt;/strong&gt;: Working with 3D models, textures, and audio&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-Platform Development&lt;/strong&gt;: CMake, qmake, make, and Linux development tools&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This tutorial is perfect for intermediate programmers who want to understand game engine architecture and learn practical modding techniques.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="what-is-multimower"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;What is MultiMower?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;MultiMower is a physics-based 3D game where players control armed robotic lawn mowers. Built with the Dry Engine, it's currently in early development but provides an excellent foundation for learning game modding and physics programming.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Current Features:&lt;/strong&gt;
- Single mower type with basic weaponry (machine gun and missiles)
- Multiple stationary computer-controlled mowers that randomly fire missiles in random directions
- 3D arena with grass terrain and collision boundaries
- Real-time physics simulation for movement and projectiles
- Local player control with keyboard and mouse&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Technical Foundation:&lt;/strong&gt;
- OpenGL 3D rendering with custom shaders
- Bullet Physics integration for realistic movement
- Component-based entity system
- Sound system with SDL2 audio
- Cross-platform C++ codebase&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Development Status:&lt;/strong&gt;
MultiMower is an artistic technical demonstration in its current state. The original version provides a foundation with basic movement and projectile mechanics that we can enhance through modding. Our tutorial expands it into a full-featured tank combat experience.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="prerequisites"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;Prerequisites&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Before we begin, ensure you have the following installed on your Linux system:&lt;/p&gt;
&lt;p&gt;For Ubuntu/Debian:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;update
sudo&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;cmake&lt;span class="w"&gt; &lt;/span&gt;build-essential&lt;span class="w"&gt; &lt;/span&gt;qtbase5-dev&lt;span class="w"&gt; &lt;/span&gt;qt5-qmake&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;libx11-dev&lt;span class="w"&gt; &lt;/span&gt;libxrandr-dev&lt;span class="w"&gt; &lt;/span&gt;libasound2-dev&lt;span class="w"&gt; &lt;/span&gt;libegl1-mesa-dev&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;libwayland-dev&lt;span class="w"&gt; &lt;/span&gt;wayland-protocols
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For Fedora:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;cmake&lt;span class="w"&gt; &lt;/span&gt;gcc&lt;span class="w"&gt; &lt;/span&gt;gcc-c++&lt;span class="w"&gt; &lt;/span&gt;qt5-qtbase-devel&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;libX11-devel&lt;span class="w"&gt; &lt;/span&gt;libXrandr-devel&lt;span class="w"&gt; &lt;/span&gt;alsa-lib-devel&lt;span class="w"&gt; &lt;/span&gt;mesa-libEGL-devel&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;wayland-devel&lt;span class="w"&gt; &lt;/span&gt;wayland-protocols-devel
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="downloading-the-source-code"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;Downloading the Source Code&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;First, let's clone the MultiMower repository and checkout the specific commit used in this tutorial:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git
git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;git@gitlab.com:luckeyproductions/games/MultiMower.git
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;MultiMower
git&lt;span class="w"&gt; &lt;/span&gt;checkout&lt;span class="w"&gt; &lt;/span&gt;60b5dcd6597fe6c7a480505f822a70e0a1469cda
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="setting-up-the-dry-engine"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-7"&gt;Setting Up the Dry Engine&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;MultiMower uses the Dry Engine. Here's how to build it from source on Fedora:&lt;/p&gt;
&lt;div class="section" id="building-dry-from-source-on-fedora"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-8"&gt;Building Dry from Source on Fedora&lt;/a&gt;&lt;/h3&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Clone the Dry repository and checkout the specific commit used in this tutorial:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git
git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;git@gitlab.com:luckeyproductions/dry.git
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dry
git&lt;span class="w"&gt; &lt;/span&gt;checkout&lt;span class="w"&gt; &lt;/span&gt;bc78ed2c3b2c52f21ccb639ace09c016bce8536d
&lt;/pre&gt;&lt;/div&gt;
&lt;ol class="arabic simple" start="2"&gt;
&lt;li&gt;Install dependencies (the repository includes a helper script):&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# This script automatically detects your distro and installs required packages&lt;/span&gt;
./script/installreq.sh
&lt;/pre&gt;&lt;/div&gt;
&lt;ol class="arabic simple" start="3"&gt;
&lt;li&gt;Build Dry using the provided build script:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# For a minimal build (recommended for game development)&lt;/span&gt;
./quick.sh

&lt;span class="c1"&gt;# Or for a full build with samples and tools (recommended):&lt;/span&gt;
mkdir&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;build
cmake&lt;span class="w"&gt; &lt;/span&gt;..&lt;span class="w"&gt; &lt;/span&gt;-DDRY_64BIT&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-DCMAKE_BUILD_TYPE&lt;span class="o"&gt;=&lt;/span&gt;Release&lt;span class="w"&gt; &lt;/span&gt;-DVIDEO_WAYLAND&lt;span class="o"&gt;=&lt;/span&gt;OFF
make&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="k"&gt;$(&lt;/span&gt;nproc&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: We disable Wayland (&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-DVIDEO_WAYLAND=OFF&lt;/span&gt;&lt;/tt&gt;) to avoid linking issues. X11 support works perfectly for gaming.&lt;/p&gt;
&lt;ol class="arabic simple" start="4"&gt;
&lt;li&gt;Set the DRY_HOME environment variable:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Set DRY_HOME to your Dry build directory (adjust path for your username)&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;DRY_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/git/dry/build

&lt;span class="c1"&gt;# Make it permanent (add to ~/.bashrc)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;export DRY_HOME=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/git/dry/build&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;~/.bashrc
&lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/.bashrc
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="building-dry-with-debug-symbols"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-9"&gt;Building Dry with Debug Symbols&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;If you need debug symbols for development:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git/dry
mkdir&lt;span class="w"&gt; &lt;/span&gt;build-debug&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;build-debug
cmake&lt;span class="w"&gt; &lt;/span&gt;..&lt;span class="w"&gt; &lt;/span&gt;-DDRY_64BIT&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-DCMAKE_BUILD_TYPE&lt;span class="o"&gt;=&lt;/span&gt;Debug&lt;span class="w"&gt; &lt;/span&gt;-DVIDEO_WAYLAND&lt;span class="o"&gt;=&lt;/span&gt;OFF
make&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="k"&gt;$(&lt;/span&gt;nproc&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Use this debug build instead&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;DRY_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/git/dry/build-debug
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="compiling-multimower"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-10"&gt;Compiling MultiMower&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Now we're ready to build the game in its own directory (much more sensible than building inside the engine):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Navigate to your MultiMower directory and create a build folder&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git/MultiMower
mkdir&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;build

&lt;span class="c1"&gt;# CRITICAL: Set DRY_HOME before running qmake&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;DRY_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/git/dry/build

&lt;span class="c1"&gt;# Generate the Makefile (qmake will find Dry via DRY_HOME)&lt;/span&gt;
qmake&lt;span class="w"&gt; &lt;/span&gt;../MultiMower.pro

&lt;span class="c1"&gt;# Compile the game with parallel jobs for faster builds&lt;/span&gt;
make&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="k"&gt;$(&lt;/span&gt;nproc&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Copy the game resources to the build directory&lt;/span&gt;
cp&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;../Resources&lt;span class="w"&gt; &lt;/span&gt;.
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="section" id="build-notes"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-11"&gt;Build Notes:&lt;/a&gt;&lt;/h3&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;Build Location&lt;/strong&gt;: The executable is created in &lt;tt class="docutils literal"&gt;~/git/MultiMower/build/&lt;/tt&gt; (not in the Dry engine directory!)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRITICAL&lt;/strong&gt;: You MUST set &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;DRY_HOME=$HOME/git/dry/build&lt;/span&gt;&lt;/tt&gt; before running qmake, or compilation will fail with &amp;quot;Dry/Dry.h: No such file or directory&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User-Specific Paths&lt;/strong&gt;: Replace &lt;tt class="docutils literal"&gt;$HOME&lt;/tt&gt; with your actual home directory (e.g., &lt;tt class="docutils literal"&gt;/home/yourusername&lt;/tt&gt;, &lt;tt class="docutils literal"&gt;/Users/yourusername&lt;/tt&gt;, etc.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Warnings Expected&lt;/strong&gt;: You'll see many compiler warnings about unused parameters and deprecated copy constructors from the Dry engine - these are normal and don't affect functionality&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build Time&lt;/strong&gt;: Compilation takes about 30-60 seconds on modern hardware&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File Size&lt;/strong&gt;: The final executable is approximately 15MB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependencies&lt;/strong&gt;: Links against libDry.a from your &lt;tt class="docutils literal"&gt;$DRY_HOME&lt;/tt&gt;, plus pthread, dl, and OpenGL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If everything went well, you should now have a &lt;tt class="docutils literal"&gt;multimower&lt;/tt&gt; executable in &lt;tt class="docutils literal"&gt;~/git/MultiMower/build/&lt;/tt&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="running-the-game"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-12"&gt;Running the Game&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To run MultiMower, navigate to the build directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# The game builds in its own build directory&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git/MultiMower/build

&lt;span class="c1"&gt;# Run the game&lt;/span&gt;
./multimower
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: The game must be run from the directory containing the &lt;tt class="docutils literal"&gt;Resources&lt;/tt&gt; folder, or it won't find its assets.&lt;/p&gt;
&lt;div class="section" id="game-controls"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-13"&gt;Game Controls:&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Enhanced Player Controls:&lt;/strong&gt;
- &lt;strong&gt;Movement&lt;/strong&gt;: WASD keys (W=forward, S=backward, A=turn left, D=turn right)
- &lt;strong&gt;Aim&lt;/strong&gt;: Mouse to look around and aim
- &lt;strong&gt;Machine Gun&lt;/strong&gt;: Left mouse button (fires bullets)
- &lt;strong&gt;Missiles&lt;/strong&gt;: Right mouse button (fires homing missiles)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The original demo provides basic movement that we enhance with realistic tank-style acceleration and responsive controls. The mouse aiming works perfectly out of the box.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="expected-output"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-14"&gt;Expected Output:&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When you first run the game, you should see:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;A 3D area with tanks surrounding&lt;/li&gt;
&lt;li&gt;Robot mowers that can be controlled by players&lt;/li&gt;
&lt;li&gt;Physics-based movement and projectiles&lt;/li&gt;
&lt;li&gt;Visual effects like explosions &amp;amp; sparks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Working audio&lt;/strong&gt;: Launch sounds, explosion effects, and other game audio&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Success Indicators:&lt;/strong&gt;
- Log shows: &lt;tt class="docutils literal"&gt;Set audio mode 44100 Hz stereo interpolated&lt;/tt&gt;
- No &amp;quot;Failed to initialise SDL subsystem&amp;quot; errors
- Sound effects play when firing weapons with mouse clicks&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="understanding-the-codebase"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-15"&gt;Understanding the Codebase&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Before modding, let's explore the project structure:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;MultiMower/
├── src/                    # C++ source files
│   ├── mastercontrol.cpp   # Main game loop
│   ├── mower.cpp          # Mower implementation
│   ├── inputmaster.cpp    # Input handling
│   └── ...
├── Resources/             # Game assets
│   ├── Models/           # 3D models
│   ├── Textures/         # Texture files
│   ├── Shaders/          # GLSL shaders
│   └── ...
└── blends/               # Blender source files
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Key classes to understand:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;MasterControl&lt;/strong&gt;: Manages the game state and scene&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mower&lt;/strong&gt;: The player-controlled mower entity&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;InputMaster&lt;/strong&gt;: Handles keyboard and joystick input&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SpawnMaster&lt;/strong&gt;: Manages entity spawning&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="section" id="version-compatibility"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-16"&gt;Version Compatibility&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This tutorial was tested and written for specific commits to ensure reproducibility:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;Dry Engine&lt;/strong&gt;: commit &lt;tt class="docutils literal"&gt;bc78ed2c3b2c52f21ccb639ace09c016bce8536d&lt;/tt&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MultiMower&lt;/strong&gt;: commit &lt;tt class="docutils literal"&gt;60b5dcd6597fe6c7a480505f822a70e0a1469cda&lt;/tt&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want to follow this tutorial exactly, check out these specific commits:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Check out the exact Dry version used in this tutorial&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git/dry
git&lt;span class="w"&gt; &lt;/span&gt;checkout&lt;span class="w"&gt; &lt;/span&gt;bc78ed2c3b2c52f21ccb639ace09c016bce8536d

&lt;span class="c1"&gt;# Check out the exact MultiMower version used in this tutorial&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git/MultiMower
git&lt;span class="w"&gt; &lt;/span&gt;checkout&lt;span class="w"&gt; &lt;/span&gt;60b5dcd6597fe6c7a480505f822a70e0a1469cda
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="game-development-tutorials"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-17"&gt;Game Development Tutorials&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here are three comprehensive mods that transform MultiMower into a proper tank combat game with realistic movement, damage systems, and spectacular explosions.&lt;/p&gt;
&lt;div class="section" id="part-1-tank-movement-controls-with-realistic-acceleration"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-18"&gt;Part 1: Tank Movement Controls with Realistic Acceleration&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Goal&lt;/strong&gt;: Enhance the movement system with realistic tank-style acceleration and responsive controls.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Implement proper tank-style movement with realistic acceleration curves.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;: Add acceleration variables to &lt;tt class="docutils literal"&gt;src/mower.h&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;// Add these private members after existing variables:&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Acceleration system&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;currentSpeed_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;maxSpeed_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;acceleration_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;: Initialize acceleration in &lt;tt class="docutils literal"&gt;src/mower.cpp&lt;/tt&gt; constructor:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Controllable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// existing initializers...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;lastFiredRight_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;currentSpeed_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;maxSpeed_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;60.0f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;// 60 units/sec top speed&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;acceleration_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;7.5f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// 0-60 in 8 seconds&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt;: Update the method signature in &lt;tt class="docutils literal"&gt;src/mower.h&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HandleInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timeStep&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Add timeStep parameter&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 4&lt;/strong&gt;: Replace the &lt;tt class="docutils literal"&gt;HandleInput()&lt;/tt&gt; method in &lt;tt class="docutils literal"&gt;src/mower.cpp&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Mower::HandleInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timeStep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;RigidBody&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;node_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RigidBody&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Get inputs&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;forwardInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;move_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;// W/S keys (-1 to 1)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rotationInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;move_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// A/D keys&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Calculate target speed based on input&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;targetSpeed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;forwardInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;maxSpeed_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Can be negative for reverse&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Gradual acceleration/deceleration (0-60 in 8 seconds)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetSpeed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;currentSpeed_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.1f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targetSpeed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;currentSpeed_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;currentSpeed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;acceleration_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timeStep&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentSpeed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;targetSpeed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;currentSpeed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;targetSpeed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;currentSpeed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;acceleration_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timeStep&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentSpeed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;targetSpeed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;currentSpeed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;targetSpeed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;currentSpeed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;targetSpeed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Close enough, snap to target&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Get current facing direction and set velocity&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;forward&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;node_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetWorldDirection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;desiredVelocity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;forward&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;currentSpeed_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetLinearVelocity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desiredVelocity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Handle rotation&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rotationInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.01f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;torque&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;UP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rotationInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;15.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ApplyTorque&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torque&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Angular damping for smoother rotation&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;angularVel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetAngularVelocity&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ApplyTorque&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;angularVel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8.0f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Fire bullet (existing code)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetMouseButtonDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MOUSEB_LEFT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sinceBullet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bulletInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;bullets_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Projectile&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gun_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;sinceBullet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Fire missile (existing code)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetMouseButtonPress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MOUSEB_RIGHT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sinceMissile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;missileInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;FireMissile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This is the initial version from Part 1. The health system checks (&lt;tt class="docutils literal"&gt;isDestroyed_&lt;/tt&gt;) will be added in Part 2.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5&lt;/strong&gt;: Update the call site in &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;Mower::Update()&lt;/span&gt;&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetPlayer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;HandleInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeStep&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Pass timeStep parameter&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Tanks now accelerate realistically from 0-60 in 8 seconds, reach high speeds, and feel like real heavy vehicles!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="part-2-health-and-damage-system-with-random-damage"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-19"&gt;Part 2: Health and Damage System with Random Damage&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Goal&lt;/strong&gt;: Add hit points, collision detection, and random weapon damage for balanced combat.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;: Add health variables to &lt;tt class="docutils literal"&gt;src/mower.h&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;// Add these private members:&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Health system&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;health_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;maxHealth_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;: Initialize health in &lt;tt class="docutils literal"&gt;src/mower.cpp&lt;/tt&gt; constructor:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Controllable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// existing initializers...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;acceleration_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;7.5f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;// 0-60 in 8 seconds&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;health_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100.0f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;maxHealth_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100.0f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt;: Add damage methods to &lt;tt class="docutils literal"&gt;src/mower.h&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TakeDamage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;DestroyMower&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;GetHealthPercentage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 4&lt;/strong&gt;: Implement damage methods in &lt;tt class="docutils literal"&gt;src/mower.cpp&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Mower::TakeDamage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;health_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;DRY_LOGINFOF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Mower took %.1f damage, health now %.1f&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;health_&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;health_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;DestroyMower&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Mower::DestroyMower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;health_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;currentSpeed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Stop acceleration&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;DRY_LOGINFO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Mower destroyed!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Disable physics so it stops moving&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;RigidBody&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;node_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RigidBody&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Hide the mower model&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;AnimatedModel&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;node_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AnimatedModel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Mower::GetHealthPercentage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;health_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;maxHealth_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 5&lt;/strong&gt;: Add collision detection in &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;Mower::Update()&lt;/span&gt;&lt;/tt&gt; method (in the bullet loop):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;// Replace the bullet update loop with collision detection&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bullets_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Projectile&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bullets_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;At&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)};&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timeStep&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bulletPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Solve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age_&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Check for hits on other mowers&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bulletHit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Check all mowers in the scene&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;PODVector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerNodes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;GetScene&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetChildrenWithComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mowerNodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerNode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerNodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;otherMower&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerNode&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;otherMower&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;otherMower&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;otherMower&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bulletPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;otherMower&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;node_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetWorldPosition&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.0f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Hit radius&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Random 1-4 damage&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;DRY_LOGINFOF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Bullet hit! Distance: %f, Damage: %.0f&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;otherMower&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;TakeDamage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;bullets_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EraseSwap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;bulletHit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;bulletHit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.17f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bulletPos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;.075f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Sparkle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bulletPos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;bullets_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EraseSwap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;bulletHit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 6&lt;/strong&gt;: Add missile collision detection in the missile update loop:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;// In the missile update loop, add collision detection&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.f&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;3.f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;missile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timeStep&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Sparkle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Solve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age_&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;.1f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;missilePos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;missile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Solve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age_&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Check for missile hits on other mowers&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;missileHit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;PODVector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerNodes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;GetScene&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetChildrenWithComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mowerNodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerNode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerNodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;otherMower&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerNode&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;otherMower&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;otherMower&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;otherMower&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missilePos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;otherMower&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;node_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetWorldPosition&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8.0f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Larger explosion radius&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10.0f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Random 10-25 damage&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;DRY_LOGINFOF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Missile hit! Distance: %f, Damage: %.0f&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;otherMower&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;TakeDamage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;damage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;missileHit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missileHit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;missile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;age_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.f&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;3.f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;missilePos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;.25f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Create explosion effects and remove missile&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// ... existing explosion code ...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 7&lt;/strong&gt;: Add health bar visualization in &lt;tt class="docutils literal"&gt;RenderDebug()&lt;/tt&gt; method:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;// Add to RenderDebug() method in mower.cpp&lt;/span&gt;
&lt;span class="c1"&gt;// Render health bar above mower&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;healthBarPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;node_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetWorldPosition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;UP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;healthPercent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GetHealthPercentage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;healthColor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GREEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lerp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;healthPercent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Health bar background (dark)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;AddLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;healthBarPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="n"&gt;healthBarPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BLACK&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Health bar (colored based on health)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;AddLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;healthBarPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RIGHT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="n"&gt;healthBarPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RIGHT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;RIGHT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.0f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;healthPercent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="n"&gt;healthColor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Weapon Damage Values&lt;/strong&gt;:
- &lt;strong&gt;Bullets&lt;/strong&gt;: Random 1-4 damage per hit (takes 25-100 bullets to destroy)
- &lt;strong&gt;Missiles&lt;/strong&gt;: Random 10-25 damage per hit (takes 4-10 missiles to destroy)
- &lt;strong&gt;Total mower health&lt;/strong&gt;: 100 HP&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 8&lt;/strong&gt;: Update &lt;tt class="docutils literal"&gt;HandleInput()&lt;/tt&gt; to prevent firing when dead (modify the method from Part 1):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Mower::HandleInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timeStep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Don&amp;#39;t handle input if destroyed&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ... existing movement code from Part 1 ...&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Fire bullet (only if not destroyed)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetMouseButtonDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MOUSEB_LEFT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sinceBullet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bulletInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;bullets_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Projectile&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gun_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;sinceBullet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Fire missile (only if not destroyed)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetMouseButtonPress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MOUSEB_RIGHT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sinceMissile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;missileInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;FireMissile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 9&lt;/strong&gt;: Update AI firing in &lt;tt class="docutils literal"&gt;Update()&lt;/tt&gt; method:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// AI mowers only fire if not destroyed&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;420&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;FireMissile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Balanced combat with visible health bars and satisfying random damage!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="part-3-death-and-victory-effects"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-20"&gt;Part 3: Death and Victory Effects&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Goal&lt;/strong&gt;: Create a complete end-game experience with massive explosions, death camera effects, electrical sparking, and victory celebrations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;: Add new variables to &lt;tt class="docutils literal"&gt;src/mower.h&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;// Add these private members:&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Death camera&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;deathCameraDistance_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Continuous sparking effect for destroyed mowers&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sparkTimer_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Victory flag to prevent multiple celebrations&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;victoryTriggered_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;: Initialize new variables in &lt;tt class="docutils literal"&gt;src/mower.cpp&lt;/tt&gt; constructor:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Controllable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// existing initializers...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;deathCameraDistance_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10.0f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Initial camera distance on death&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;sparkTimer_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="c1"&gt;// Timer for continuous sparking&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;victoryTriggered_&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Flag to prevent multiple victory celebrations&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt;: Replace the &lt;tt class="docutils literal"&gt;DestroyMower()&lt;/tt&gt; method in &lt;tt class="docutils literal"&gt;src/mower.cpp&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Mower::DestroyMower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;health_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;currentSpeed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Stop acceleration&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;DRY_LOGINFO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Mower destroyed! MASSIVE EXPLOSION!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;explosionPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;node_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetWorldPosition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// HUGE explosion effect - like multiple rockets going off!&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// 200 sparkles instead of 30&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Sparkle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;explosionPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;5.0f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;5.0f&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="mf"&gt;30.0f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;20.0f&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Varied speed and position&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Create multiple trail effects radiating outward (like rocket exhaust)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;trailStart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;explosionPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0f&lt;/span&gt;&lt;span class="p"&gt;)};&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Trail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trailStart&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Multiple explosion sounds for massive effect&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;PlaySample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Sound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Samples/Explode.wav&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.5f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Main explosion&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Add delayed secondary explosions (ammo cook-off effect)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Create delayed sparkle bursts&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;delayedPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;explosionPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;8.0f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;4.0f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;8.0f&lt;/span&gt;&lt;span class="p"&gt;)};&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Sparkle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delayedPos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;25.0f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Create blast lights for dramatic effect&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lightNode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GetScene&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;CreateChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;BlastLight&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;lightNode&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;CreateComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BlastLight&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lightPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;explosionPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0f&lt;/span&gt;&lt;span class="p"&gt;)};&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;lightNode&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightPos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Disable physics so it stops moving&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;RigidBody&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;node_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RigidBody&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetEnabled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Transform into a grayscale wreck instead of hiding&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;AnimatedModel&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;node_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AnimatedModel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Create grayscale material to show battle damage&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;SharedPtr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Material&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;grayscaleMaterial&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetMaterial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Convert to grayscale - desaturate while keeping original brightness&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;grayscaleMaterial&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetShaderParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;MatDiffColor&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.4f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.4f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.4f&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Grayscale&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;grayscaleMaterial&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetShaderParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;MatSpecColor&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.1f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.1f&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Dull reflection&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;grayscaleMaterial&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetShaderParameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;MatEmissiveColor&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// No glow&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetMaterial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grayscaleMaterial&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;DRY_LOGINFO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Applied grayscale material to destroyed mower&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Explosion Features&lt;/strong&gt;:
- &lt;strong&gt;200 sparkles&lt;/strong&gt; scattered over a large area (vs 30 in original)
- &lt;strong&gt;50 rocket exhaust trails&lt;/strong&gt; radiating outward
- &lt;strong&gt;8 secondary explosion bursts&lt;/strong&gt; simulating ammo cook-off
- &lt;strong&gt;5 dramatic blast lights&lt;/strong&gt; for cinematic effect
- &lt;strong&gt;Louder explosion sound&lt;/strong&gt; (1.5x volume)
- &lt;strong&gt;Varied particle speeds&lt;/strong&gt; and positions for realism&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4&lt;/strong&gt;: Add continuous sparking in &lt;tt class="docutils literal"&gt;Update()&lt;/tt&gt; method:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;// Add to Update() method after existing timer updates&lt;/span&gt;
&lt;span class="c1"&gt;// Update spark timer for destroyed mowers&lt;/span&gt;
&lt;span class="n"&gt;sparkTimer_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timeStep&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Continuous sparking effect for destroyed mowers&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sparkTimer_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.1f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Spark every 0.1 seconds&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;sparkTimer_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Reset timer&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Create multiple sparks around the destroyed mower&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;node_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetWorldPosition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sparkPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0f&lt;/span&gt;&lt;span class="p"&gt;)};&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Sparkle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sparkPos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.0f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Electrical sparking effect&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Occasional larger sizzle effect&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 20% chance&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sizzlePos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.5f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.8f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.5f&lt;/span&gt;&lt;span class="p"&gt;)};&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Sparkle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sizzlePos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.0f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Bigger electrical discharge&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 4&lt;/strong&gt;: Add dynamic camera zoom-out in &lt;tt class="docutils literal"&gt;PostUpdate()&lt;/tt&gt; method:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Mower::PostUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timeStep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;GetPlayer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Jib&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;jib&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GetPlayer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetJib&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Handle death camera zoom (both for death and victory)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;victoryTriggered_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;jib&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;jib&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetCamera&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Dynamic zoom speed - fast initially, then slow to a halt&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;maxDistance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;50.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Maximum zoom distance&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;speedFactor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deathCameraDistance_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;maxDistance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// 1.0 to 0.0&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;speedFactor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;speedFactor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Don&amp;#39;t go below 0.1&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Fast zoom initially (60 units/sec), slowing down as we get further&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;deathCameraDistance_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;60.0f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timeStep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;speedFactor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;deathCameraDistance_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deathCameraDistance_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;maxDistance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Cap at max distance&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Set camera to orbit the mower (destroyed or victorious)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cameraNode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;jib&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetCamera&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetNode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BACK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;deathCameraDistance_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;UP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deathCameraDistance_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.4f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;cameraNode&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetWorldPosition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;cameraNode&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;LookAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetWorldPosition&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;UP&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Skip normal camera update when dead or victorious&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ... existing PostUpdate code continues here ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Visual Effects&lt;/strong&gt;:
- &lt;strong&gt;Grayscale Material&lt;/strong&gt;: Mower turns gray (0.4 RGB) showing battle damage
- &lt;strong&gt;Continuous Sparking&lt;/strong&gt;: 3 electrical sparks every 0.1 seconds around the wreck
- &lt;strong&gt;Sizzle Bursts&lt;/strong&gt;: 20% chance of 8 larger electrical discharges
- &lt;strong&gt;Dynamic Camera Movement&lt;/strong&gt;: Fast zoom initially (60 units/sec), slowing to a halt at 50 units distance
- &lt;strong&gt;No Weapon Firing&lt;/strong&gt;: Both player and AI mowers stop firing when destroyed
- &lt;strong&gt;Persistent Effect&lt;/strong&gt;: Sparking continues forever, showing damaged electronics
- &lt;strong&gt;No Self-Damage&lt;/strong&gt;: Neither bullets nor missiles can damage the mower that fired them&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: When you die, all firing stops, the camera dramatically pulls back, and your mower becomes a gray wreck with continuous electrical sparking effects - like damaged electronics shorting out!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5&lt;/strong&gt;: Add victory condition system. First add the method declaration to &lt;tt class="docutils literal"&gt;src/mower.h&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;CheckWinCondition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 6&lt;/strong&gt;: Add victory check in &lt;tt class="docutils literal"&gt;src/mower.cpp&lt;/tt&gt; Update method:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Mower::Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;timeStep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Existing update code...&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Check win condition - if player exists, check if all AI tanks are destroyed&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetPlayer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;CheckWinCondition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Rest of existing update code...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Step 7&lt;/strong&gt;: Implement the victory detection system:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Mower::CheckWinCondition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Only check win condition if this is the player mower&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;GetPlayer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Get all mowers in the scene&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;PODVector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;*&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerNodes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;GetScene&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetChildrenWithComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mowerNodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;aliveTanks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;totalAITanks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerNode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerNodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mower&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mowerNode&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Mower&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mower&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;mower&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetPlayer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// AI mower&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;totalAITanks&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;mower&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;isDestroyed_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;aliveTanks&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Check if all AI tanks are destroyed&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;totalAITanks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;aliveTanks&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;victoryTriggered_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;victoryTriggered_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Set flag to prevent multiple celebrations&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Player wins! Display victory message&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;DRY_LOGINFO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;=== VICTORY! ALL ENEMY TANKS DESTROYED! ===&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Create a big celebration explosion at player position&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;playerPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;node_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetWorldPosition&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Victory celebration - exciting but controlled (only fires once!)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fireworkPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;playerPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;6.0f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;4.0f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;6.0f&lt;/span&gt;&lt;span class="p"&gt;)};&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Sparkle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fireworkPos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;25.0f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;15.0f&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Victory trail burst&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;trailStart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;playerPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;4.0f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;4.0f&lt;/span&gt;&lt;span class="p"&gt;)};&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Trail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trailStart&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Multiple celebration lights&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lightNode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GetScene&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;CreateChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;VictoryLight&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;lightNode&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;CreateComponent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BlastLight&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;lightPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;playerPos&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0f&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RandomOffCenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.0f&lt;/span&gt;&lt;span class="p"&gt;)};&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;lightNode&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetPosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightPos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Victory sound&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;PlaySample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Sound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Samples/Explode.wav&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.0f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Log stats&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;DRY_LOGINFOF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Victory achieved! Defeated %d enemy tanks!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;totalAITanks&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Victory Effects&lt;/strong&gt;:
- &lt;strong&gt;Victory Sparkles&lt;/strong&gt;: 50 sparkles in a large area around the player (exciting celebration)
- &lt;strong&gt;Victory Trails&lt;/strong&gt;: 20 colored trail effects radiating outward from player position
- &lt;strong&gt;Celebration Lights&lt;/strong&gt;: 3 blast lights scattered around the player
- &lt;strong&gt;Audio Feedback&lt;/strong&gt;: Victory explosion sound at double volume
- &lt;strong&gt;Console Message&lt;/strong&gt;: Clear victory announcement with enemy count
- &lt;strong&gt;Single Trigger&lt;/strong&gt;: Victory only fires once when condition is first met (prevents multiple celebrations per frame)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How It Works&lt;/strong&gt;:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;strong&gt;Continuous Checking&lt;/strong&gt;: Every frame, the player mower scans all tanks in the scene&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI Detection&lt;/strong&gt;: Counts tanks that don't have a player component (AI-controlled)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status Verification&lt;/strong&gt;: Checks if each AI tank is destroyed using &lt;tt class="docutils literal"&gt;isDestroyed_&lt;/tt&gt; flag&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Victory Trigger&lt;/strong&gt;: When &lt;tt class="docutils literal"&gt;aliveTanks == 0&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;totalAITanks &amp;gt; 0&lt;/tt&gt;, victory fires&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Celebration&lt;/strong&gt;: Immediate massive fireworks display centered on player position&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: Efficient - only player mower checks, stops when victory achieved&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Testing Victory&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Run the game and destroy all enemy tanks&lt;/span&gt;
./multimower

&lt;span class="c1"&gt;# Victory triggers automatically when last enemy dies&lt;/span&gt;
&lt;span class="c1"&gt;# Watch for log message: &amp;quot;=== VICTORY! ALL ENEMY TANKS DESTROYED! ===&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# Enjoy the massive fireworks celebration!&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: When you destroy the last enemy tank, a massive and exciting victory celebration appears around your mower with 50 sparkles, 20 trails, 3 lights, and victory sound - making you feel like a true tank commander with a spectacular fireworks display!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="complete-game-development-results"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-21"&gt;Complete Game Development Results&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;After implementing all three parts, you'll have:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;strong&gt;Realistic Tank Movement&lt;/strong&gt;: 0-60 acceleration in 8 seconds, high top speeds, smooth control&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Balanced Combat&lt;/strong&gt;: Random damage (1-4 bullets, 10-25 missiles), health bars, collision detection&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complete End-Game Experience&lt;/strong&gt;: Massive explosions, death camera effects, electrical sparking, and victory celebrations&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Total Changes&lt;/strong&gt;:
- &lt;strong&gt;Files Modified&lt;/strong&gt;: &lt;tt class="docutils literal"&gt;src/mower.h&lt;/tt&gt;, &lt;tt class="docutils literal"&gt;src/mower.cpp&lt;/tt&gt;, &lt;tt class="docutils literal"&gt;src/inputmaster.cpp&lt;/tt&gt;, &lt;tt class="docutils literal"&gt;src/mastercontrol.cpp&lt;/tt&gt;
- &lt;strong&gt;Lines Added&lt;/strong&gt;: +392 lines of code, -16 lines removed
- &lt;strong&gt;Net Addition&lt;/strong&gt;: +376 lines of code
- &lt;strong&gt;New Features&lt;/strong&gt;: Acceleration system, health system, collision detection, massive explosions, death effects, victory celebrations
- &lt;strong&gt;Gameplay Impact&lt;/strong&gt;: Transforms MultiMower into a complete cinematic tank combat experience&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Testing Your Mods&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Compile with your changes&lt;/span&gt;
make&lt;span class="w"&gt; &lt;/span&gt;clean&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="k"&gt;$(&lt;/span&gt;nproc&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Run the enhanced game&lt;/span&gt;
./multimower

&lt;span class="c1"&gt;# Test the features:&lt;/span&gt;
&lt;span class="c1"&gt;# - W/S for realistic acceleration/deceleration&lt;/span&gt;
&lt;span class="c1"&gt;# - A/D for tank turning&lt;/span&gt;
&lt;span class="c1"&gt;# - Left click for machine gun (1-4 damage)&lt;/span&gt;
&lt;span class="c1"&gt;# - Right click for missiles (10-25 damage)&lt;/span&gt;
&lt;span class="c1"&gt;# - Destroy enemy tanks for massive explosions!&lt;/span&gt;
&lt;span class="c1"&gt;# - Destroy all enemy tanks for victory fireworks!&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="project-statistics"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-22"&gt;Project Statistics&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here are the actual numbers behind this modding tutorial, showing the scope and impact of our changes:&lt;/p&gt;
&lt;div class="section" id="engine-and-game-size"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-23"&gt;Engine and Game Size&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Dry Engine (commit bc78ed2c)&lt;/strong&gt;:
- &lt;strong&gt;Source Files&lt;/strong&gt;: 4,330 files (.cpp, .h, .c)
- &lt;strong&gt;Lines of Code&lt;/strong&gt;: 199,687 total lines
- &lt;strong&gt;Repository Size&lt;/strong&gt;: 1.1 GB (includes samples, tools, third-party libraries)
- &lt;strong&gt;Static Library&lt;/strong&gt;: 44 MB (libDry.a)
- &lt;strong&gt;Build Time&lt;/strong&gt;: ~5-8 minutes on modern hardware&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MultiMower Game (commit 60b5dcd)&lt;/strong&gt;:
- &lt;strong&gt;Source Files&lt;/strong&gt;: 18 files (.cpp, .h)
- &lt;strong&gt;Lines of Code&lt;/strong&gt;: 2,346 total lines
- &lt;strong&gt;Repository Size&lt;/strong&gt;: 31 MB (includes assets, models, textures)
- &lt;strong&gt;Game Resources&lt;/strong&gt;: 5.3 MB (3D models, textures, sounds)
- &lt;strong&gt;Final Executable&lt;/strong&gt;: 15 MB (statically linked with Dry)
- &lt;strong&gt;Build Time&lt;/strong&gt;: ~30-60 seconds&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="our-game-development-impact"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-24"&gt;Our Game Development Impact&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Code Changes Made&lt;/strong&gt;:
- &lt;strong&gt;Files Modified&lt;/strong&gt;: 4 source files
- &lt;strong&gt;Lines Added&lt;/strong&gt;: +392 lines of code
- &lt;strong&gt;Lines Removed&lt;/strong&gt;: -16 lines of code
- &lt;strong&gt;Net Addition&lt;/strong&gt;: +376 lines of code&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Breakdown by File&lt;/strong&gt;:
- &lt;tt class="docutils literal"&gt;src/mower.cpp&lt;/tt&gt;: +359 lines, -13 lines (main implementation)
- &lt;tt class="docutils literal"&gt;src/mower.h&lt;/tt&gt;: +21 lines, -1 line (new variables and methods)
- &lt;tt class="docutils literal"&gt;src/inputmaster.cpp&lt;/tt&gt;: +9 lines, -1 line (debug logging)
- &lt;tt class="docutils literal"&gt;src/mastercontrol.cpp&lt;/tt&gt;: +3 lines, -1 line (debug logging)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Feature Implementation by Parts&lt;/strong&gt;:
- &lt;strong&gt;Part 1 - Tank Movement&lt;/strong&gt;: ~80 lines (acceleration system, realistic physics)
- &lt;strong&gt;Part 2 - Health &amp;amp; Damage&lt;/strong&gt;: ~120 lines (collision detection, health bars, damage system)
- &lt;strong&gt;Part 3 - Death &amp;amp; Victory&lt;/strong&gt;: ~160 lines (explosions, camera effects, sparking, victory system)
- &lt;strong&gt;Supporting Code&lt;/strong&gt;: ~16 lines (debugging, initialization, cleanup)&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="scale-perspective"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-25"&gt;Scale Perspective&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Code Efficiency&lt;/strong&gt;:
- Our 376 lines represent &lt;strong&gt;0.16%&lt;/strong&gt; of the total MultiMower codebase
- Yet they transform the game from basic technical demonstration to complete tank combat with victory conditions
- Shows the power of focused, strategic modifications&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Engine vs Game&lt;/strong&gt;:
- Dry Engine: &lt;strong&gt;85x larger&lt;/strong&gt; than MultiMower (199K vs 2.3K lines)
- MultiMower: &lt;strong&gt;35x smaller&lt;/strong&gt; repository than Dry (31MB vs 1.1GB)
- Game executable includes the entire engine statically linked&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Development Effort&lt;/strong&gt;:
- &lt;strong&gt;Engine Development&lt;/strong&gt;: Years of work, complex graphics/physics systems
- &lt;strong&gt;Game Development&lt;/strong&gt;: Weeks of work, focused gameplay mechanics
- &lt;strong&gt;Our Game Development&lt;/strong&gt;: Hours of work, targeted feature additions&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build Performance&lt;/strong&gt;:
- &lt;strong&gt;Dry Engine&lt;/strong&gt;: Takes 5-8 minutes to compile from scratch
- &lt;strong&gt;MultiMower&lt;/strong&gt;: Takes 30-60 seconds with incremental changes
- &lt;strong&gt;Our Patches&lt;/strong&gt;: Compile in 5-10 seconds after initial build&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="troubleshooting-on-fedora"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-26"&gt;Troubleshooting on Fedora&lt;/a&gt;&lt;/h2&gt;
&lt;div class="section" id="common-build-issues"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-27"&gt;Common Build Issues&lt;/a&gt;&lt;/h3&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;strong&gt;&amp;quot;Dry/Dry.h: No such file or directory&amp;quot;&lt;/strong&gt;: This happens when DRY_HOME isn't set before running qmake:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Check if DRY_HOME is set correctly&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$DRY_HOME&lt;/span&gt;
&lt;span class="c1"&gt;# Should print: /home/yourusername/git/dry/build (with your actual username)&lt;/span&gt;

&lt;span class="c1"&gt;# If empty or wrong, fix it:&lt;/span&gt;
&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;DRY_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/git/dry/build

&lt;span class="c1"&gt;# Remove old Makefile and regenerate&lt;/span&gt;
rm&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;Makefile
qmake&lt;span class="w"&gt; &lt;/span&gt;../MultiMower.pro
make&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="k"&gt;$(&lt;/span&gt;nproc&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;ol class="arabic simple" start="2"&gt;
&lt;li&gt;&lt;strong&gt;Build directory confusion&lt;/strong&gt;: Always build in MultiMower's own build directory, not in the Dry engine:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Correct approach - build in game directory&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git/MultiMower/build

&lt;span class="c1"&gt;# Check your executable is where you expect&lt;/span&gt;
ls&lt;span class="w"&gt; &lt;/span&gt;-la&lt;span class="w"&gt; &lt;/span&gt;multimower
&lt;/pre&gt;&lt;/div&gt;
&lt;ol class="arabic simple" start="3"&gt;
&lt;li&gt;&lt;strong&gt;Resources not found at runtime&lt;/strong&gt;: The game needs the Resources folder:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Copy resources to the build directory&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git/MultiMower/build
cp&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;../Resources&lt;span class="w"&gt; &lt;/span&gt;.
&lt;/pre&gt;&lt;/div&gt;
&lt;ol class="arabic simple" start="4"&gt;
&lt;li&gt;&lt;strong&gt;Library not found errors&lt;/strong&gt;: Ensure all dependencies are installed:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Re-run the dependency installer&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git/dry
./script/installreq.sh
&lt;/pre&gt;&lt;/div&gt;
&lt;ol class="arabic simple" start="5"&gt;
&lt;li&gt;&lt;strong&gt;Qt6 vs Qt5 conflicts&lt;/strong&gt;: The build uses Qt6 by default on newer Fedora:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# If you need to force Qt5 (though Qt6 works fine)&lt;/span&gt;
/usr/lib64/qt5/bin/qmake&lt;span class="w"&gt; &lt;/span&gt;MultiMower.pro
&lt;/pre&gt;&lt;/div&gt;
&lt;ol class="arabic simple" start="6"&gt;
&lt;li&gt;&lt;strong&gt;Compiler warnings&lt;/strong&gt;: Expect many warnings - they're from the Dry engine and don't affect functionality:&lt;ul&gt;
&lt;li&gt;Unused parameter warnings&lt;/li&gt;
&lt;li&gt;Deprecated copy constructor warnings&lt;/li&gt;
&lt;li&gt;Type-punned pointer warnings&lt;/li&gt;
&lt;li&gt;These are normal and the game will work fine&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wayland linking or audio issues&lt;/strong&gt;: If you see &lt;tt class="docutils literal"&gt;undefined reference to 'wl_proxy_*'&lt;/tt&gt; errors or &amp;quot;No available audio device&amp;quot; errors:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Both issues are typically resolved by rebuilding Dry with proper dependencies&lt;/span&gt;
&lt;span class="c1"&gt;# Make sure audio packages are installed first:&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;alsa-lib-devel

&lt;span class="c1"&gt;# Then rebuild Dry without Wayland (X11 is preferred for gaming anyway)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git/dry
rm&lt;span class="w"&gt; &lt;/span&gt;-rf&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;build
cmake&lt;span class="w"&gt; &lt;/span&gt;..&lt;span class="w"&gt; &lt;/span&gt;-DDRY_64BIT&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-DCMAKE_BUILD_TYPE&lt;span class="o"&gt;=&lt;/span&gt;Release&lt;span class="w"&gt; &lt;/span&gt;-DVIDEO_WAYLAND&lt;span class="o"&gt;=&lt;/span&gt;OFF
make&lt;span class="w"&gt; &lt;/span&gt;-j&lt;span class="k"&gt;$(&lt;/span&gt;nproc&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="real-world-build-experience"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-28"&gt;Real-World Build Experience&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When I tested this process on Fedora, here's what actually happened:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;Final executable&lt;/strong&gt;: 15MB located in &lt;tt class="docutils literal"&gt;~/git/MultiMower/build/multimower&lt;/tt&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Warnings&lt;/strong&gt;: About 50+ compiler warnings (all safe to ignore)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory usage&lt;/strong&gt;: Game links against a 45MB libDry.a static library&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Success rate&lt;/strong&gt;: 100% when following the exact steps above&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Common issues&lt;/strong&gt;: Wayland linking errors (fixed by disabling Wayland), audio initialization failures (fixed by rebuilding Dry with audio packages installed)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audio confirmation&lt;/strong&gt;: Working audio with Launch.wav and Explode.wav sound effects&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enhanced controls&lt;/strong&gt;: WASD movement with realistic tank acceleration&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="conclusion"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-29"&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;MultiMower provides an excellent platform for learning game development and modding. The clean codebase and use of the Dry Engine make it accessible for beginners while offering enough depth for advanced modifications. Whether you're adding new mower types, creating custom game modes, or just tweaking the physics, there's plenty of room for creativity.&lt;/p&gt;
&lt;p&gt;The build process is straightforward on Fedora when you follow these tested steps, and the Dry engine's helper scripts make dependency management painless.&lt;/p&gt;
&lt;p&gt;Happy mowing &amp;amp; modding!&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#resources-and-community" id="toc-entry-1"&gt;Resources and Community&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#quick-start-summary" id="toc-entry-2"&gt;Quick Start Summary&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#what-you-ll-learn" id="toc-entry-3"&gt;What You'll Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#what-is-multimower" id="toc-entry-4"&gt;What is MultiMower?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#prerequisites" id="toc-entry-5"&gt;Prerequisites&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#downloading-the-source-code" id="toc-entry-6"&gt;Downloading the Source Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#setting-up-the-dry-engine" id="toc-entry-7"&gt;Setting Up the Dry Engine&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#building-dry-from-source-on-fedora" id="toc-entry-8"&gt;Building Dry from Source on Fedora&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#building-dry-with-debug-symbols" id="toc-entry-9"&gt;Building Dry with Debug Symbols&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#compiling-multimower" id="toc-entry-10"&gt;Compiling MultiMower&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#build-notes" id="toc-entry-11"&gt;Build Notes:&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#running-the-game" id="toc-entry-12"&gt;Running the Game&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#game-controls" id="toc-entry-13"&gt;Game Controls:&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#expected-output" id="toc-entry-14"&gt;Expected Output:&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#understanding-the-codebase" id="toc-entry-15"&gt;Understanding the Codebase&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#version-compatibility" id="toc-entry-16"&gt;Version Compatibility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#game-development-tutorials" id="toc-entry-17"&gt;Game Development Tutorials&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#part-1-tank-movement-controls-with-realistic-acceleration" id="toc-entry-18"&gt;Part 1: Tank Movement Controls with Realistic Acceleration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#part-2-health-and-damage-system-with-random-damage" id="toc-entry-19"&gt;Part 2: Health and Damage System with Random Damage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#part-3-death-and-victory-effects" id="toc-entry-20"&gt;Part 3: Death and Victory Effects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#complete-game-development-results" id="toc-entry-21"&gt;Complete Game Development Results&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#project-statistics" id="toc-entry-22"&gt;Project Statistics&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#engine-and-game-size" id="toc-entry-23"&gt;Engine and Game Size&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#our-game-development-impact" id="toc-entry-24"&gt;Our Game Development Impact&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#scale-perspective" id="toc-entry-25"&gt;Scale Perspective&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#troubleshooting-on-fedora" id="toc-entry-26"&gt;Troubleshooting on Fedora&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#common-build-issues" id="toc-entry-27"&gt;Common Build Issues&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#real-world-build-experience" id="toc-entry-28"&gt;Real-World Build Experience&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#conclusion" id="toc-entry-29"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Programming"/><category term="Game Development"/><category term="Code"/></entry><entry><title>Introducing SLOP</title><link href="https://russell.ballestrini.net/introducing-slop/" rel="alternate"/><published>2025-03-21T10:22:00-04:00</published><updated>2025-03-21T10:22:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2025-03-21:/introducing-slop/</id><summary type="html">&lt;p&gt;&lt;strong&gt;Introducing SLOP: A Simple Language Open Protocol for Machine Learning Interaction&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This post introduces the SLOP project, a community-driven effort to standardize Machine Learning API interactions. I'm Russell (fxhp), and I'm excited to share how this project came together and how you can get involved!&lt;/p&gt;
&lt;div class="section" id="a-community-effort-kicked-off-by-nathan"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;A Community Effort Kicked Off …&lt;/a&gt;&lt;/h2&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;Introducing SLOP: A Simple Language Open Protocol for Machine Learning Interaction&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This post introduces the SLOP project, a community-driven effort to standardize Machine Learning API interactions. I'm Russell (fxhp), and I'm excited to share how this project came together and how you can get involved!&lt;/p&gt;
&lt;div class="section" id="a-community-effort-kicked-off-by-nathan"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;A Community Effort Kicked Off by Nathan&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;First, a huge shoutout to Nathan from &lt;a class="reference external" href="https://agnt.gg/"&gt;agnt.gg&lt;/a&gt; for spearheading the SLOP project. Nathan organized the initial repository and rallied the community through his Discord server. His vision laid the groundwork for what SLOP has become today. You can find both the main repository and join the vibrant Discord community here: &lt;a class="reference external" href="https://i-love-slop.com?ref=GXSKWN"&gt;https://i-love-slop.com?ref=GXSKWN&lt;/a&gt;. If you're passionate about open Machine Learning standards, this is the place to connect and contribute!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="what-is-unturf"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;What is unturf. ?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Before diving into SLOP, let’s talk about &lt;a class="reference external" href="https://www.unturf.com/"&gt;unturf.&lt;/a&gt; to break wheels &amp;amp; fostering open collaboration. While the main page &lt;a class="reference external" href="https://www.unturf.com/"&gt;https://www.unturf.com&lt;/a&gt; describes a possible vision for the future, the real action happens in the community driven subdomains &amp;amp; community spaces. We host free Machine Learning services like &lt;a class="reference external" href="https://ai.unturf.com/"&gt;ai.unturf.com&lt;/a&gt; and supports projects like SLOP with infrastructure &amp;amp; a spirit of openness. It’s a loose-knit group of innovators—folks like me and you—working to make software as a service &amp;amp; Machine Learning accessible to everyone.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="what-is-slop"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;What is SLOP?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;SLOP stands for &lt;strong&gt;Simple Language Open Protocol&lt;/strong&gt;, a lightweight, RESTful pattern for interacting with Machine Learning services. Think of it as a universal handshake for Machine Learning APIs, making it easy to integrate models, tools, and resources across platforms. SLOP defines well-known core endpoints so that orgainizations can interopt using standard endpoints.&lt;/p&gt;
&lt;p&gt;Ideally a SLOP server will be created in every programming language and placed into the &lt;tt class="docutils literal"&gt;examples&lt;/tt&gt; directory of the main repo.&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;/chat&lt;/strong&gt;: Talk to Machine Learning models.&lt;/li&gt;
&lt;li&gt;&lt;dl class="first docutils"&gt;
&lt;dt&gt;&lt;strong&gt;/tools&lt;/strong&gt;: Use predefined utilities (e.g., calculator, greeter).&lt;/dt&gt;
&lt;dd&gt;This is where you can call public or private tools or algorithm to work on public or private data and return a result.
A tool could be written in ANY programming language &amp;amp; does not need to be written in the same language as your SLOP server.
As long as your code in your slop server knows how to dispatch the process and return a result it may be used in agentic flows!
Yes, this includes that narly ffmpeg bash script you still use sometimes.&lt;/dd&gt;
&lt;/dl&gt;
&lt;/li&gt;
&lt;li&gt;&lt;dl class="first docutils"&gt;
&lt;dt&gt;&lt;strong&gt;/memory&lt;/strong&gt;:&lt;/dt&gt;
&lt;dd&gt;Store and retrieve key-value pairs. CRUD lifecycle (create-read-update-delete).&lt;/dd&gt;
&lt;/dl&gt;
&lt;/li&gt;
&lt;li&gt;&lt;dl class="first docutils"&gt;
&lt;dt&gt;&lt;strong&gt;/models&lt;/strong&gt;: list of models &amp;amp; types upstream to the SLOP server.&lt;/dt&gt;
&lt;dd&gt;RFC in developemnt could be moved to /info/models (html) or /info/models.[html,xml,json,yaml,etc]
right now I have a Python algorithm that dynamically determines a list of models the upstream SLOP server can provide in my implementation.
People have ask for us to also keep track of the type of model, for example embeddings or image or video or various hybrid enums.
Right now it's a simple list returned in JSON.&lt;/dd&gt;
&lt;/dl&gt;
&lt;/li&gt;
&lt;li&gt;&lt;dl class="first docutils"&gt;
&lt;dt&gt;&lt;strong&gt;/resources&lt;/strong&gt;: Manage structured data or knowledge bases.&lt;/dt&gt;
&lt;dd&gt;Supports PUT to create new records (but currently no delete)
We are currently RFC the ability to grab a list of resources by a prefix.&lt;/dd&gt;
&lt;/dl&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;/info&lt;/strong&gt;: RFC in development&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;/pay&lt;/strong&gt;: Simulate transactions (for fun or future monetization).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s all about simplicity: HTTP requests with JSON payloads, no fuss, no proprietary lock-in. SLOP lets any Machine Learning service—from big providers to homebrew setups—speak the same language. The community is still refining the spec, but it’s already proving its worth.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="my-first-python-implementation"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;My First Python Implementation&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I’ve donated the first full Python implementation of SLOP to the community written in minimal flask. This server, running at &lt;a class="reference external" href="https://slop.unturf.com/openapi"&gt;https://slop.unturf.com/openapi&lt;/a&gt;, is in alpha but fully functional. It supports all the SLOP endpoints and connects to 14 open-source text models, thanks to unturf’s infrastructure!&lt;/p&gt;
&lt;p&gt;I plan to create mimimal SLOP servers fro all the Python frameworks: Flask, Pyramid, FastAPI, etc.&lt;/p&gt;
&lt;p&gt;You can fork the public domain code here: &lt;a class="reference external" href="https://git.unturf.com/engineering/unturf/slop.unturf.com"&gt;https://git.unturf.com/engineering/unturf/slop.unturf.com&lt;/a&gt;. At the moment, we use a file-based memory store (&lt;tt class="docutils literal"&gt;data/memory.json&lt;/tt&gt;, &lt;tt class="docutils literal"&gt;data/resources.json&lt;/tt&gt;), and is deployed via &lt;tt class="docutils literal"&gt;uWSGI&lt;/tt&gt; for production readiness.&lt;/p&gt;
&lt;p&gt;Here’s a quick example of chatting with it using curl:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;-X&lt;span class="w"&gt; &lt;/span&gt;POST&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-H&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Content-Type: application/json&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{&amp;quot;messages&amp;quot;: [{&amp;quot;role&amp;quot;: &amp;quot;user&amp;quot;, &amp;quot;content&amp;quot;: &amp;quot;Hello, SLOP!&amp;quot;}], &amp;quot;model&amp;quot;: &amp;quot;adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic&amp;quot;}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;https://slop.unturf.com/chat
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The response will come back with an Machine Learning-generated message—try it out!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="optional-streamlit-frontend"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;Optional Streamlit Frontend&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To make SLOP even more accessible, I’ve also provided an optional Streamlit frontend, live at &lt;a class="reference external" href="https://streamlit.slop.unturf.com"&gt;https://streamlit.slop.unturf.com&lt;/a&gt; (also in alpha). This web interface lets you interact with the SLOP server without writing code. You can:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Chat with models in a conversational UI.&lt;/li&gt;
&lt;li&gt;Use tools like the calculator or greeter.&lt;/li&gt;
&lt;li&gt;Manage memory entries.&lt;/li&gt;
&lt;li&gt;Explore and update resources (including the new prefix search and PUT features!).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The frontend is open-source too—find it in the main repo—and it’s a great way to demo SLOP to non-technical users. It’s built to handle long-running chat requests (up to 300 seconds), so even big completions work smoothly.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="get-involved"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;Get Involved!&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;SLOP is a community project, and we need your help to grow it. Here’s how you can dive in:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fork my SLOP&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you like Python, request an account on our gitlab server or &lt;strong&gt;fork this repo&lt;/strong&gt; &lt;a class="reference external" href="https://git.unturf.com/engineering/unturf/slop.unturf.com"&gt;https://git.unturf.com/engineering/unturf/slop.unturf.com&lt;/a&gt; and start experimenting. Add new tools, or adapt it to your needs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test the Server&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;Use the swagger docs or cURL or your programming language of choice with an HTTP client to interact directly with my SLOP server &lt;a class="reference external" href="https://slop.unturf.com/openapi"&gt;https://slop.unturf.com/openapi&lt;/a&gt; with your own requests. Report issues or suggest features to include.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Join the Community&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;Connect with Nathan and the crew at &lt;a class="reference external" href="https://i-love-slop.com?ref=GXSKWN"&gt;https://i-love-slop.com?ref=GXSKWN&lt;/a&gt;. The Discord is buzzing with ideas so jump in from time to time to help shape SLOP’s future.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Try the Optional Streamlit Frontend&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;Play with &lt;a class="reference external" href="https://streamlit.slop.unturf.com"&gt;https://streamlit.slop.unturf.com&lt;/a&gt; and let us know how it can improve.&lt;/p&gt;
&lt;p&gt;If you are a web dev or designer feel free to come up with new applications or frontends that use our standard community powered endpoints!&lt;/p&gt;
&lt;p&gt;SLOP is about democratizing Machine Learning interaction. Whether you’re a developer, a tinkerer, or just curious, there’s a place for you here. Let’s build something simple, open, and powerful together!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="what-s-next"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-7"&gt;What's Next?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Stateless HTTP for the win. ; )&lt;/p&gt;
&lt;p&gt;Our biggest open source competitor is MCP, we aim to be more open &amp;amp; transparent &amp;amp; organic &amp;amp; grass roots from the bottom up not dropped from the ivory tower.
If this vibes with you start building SLOP integrations whenever you reach for an MCP integration so that we can grow both communities &amp;amp; learn from each other.&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#a-community-effort-kicked-off-by-nathan" id="toc-entry-1"&gt;A Community Effort Kicked Off by Nathan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#what-is-unturf" id="toc-entry-2"&gt;What is unturf. ?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#what-is-slop" id="toc-entry-3"&gt;What is SLOP?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#my-first-python-implementation" id="toc-entry-4"&gt;My First Python Implementation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#optional-streamlit-frontend" id="toc-entry-5"&gt;Optional Streamlit Frontend&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#get-involved" id="toc-entry-6"&gt;Get Involved!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#what-s-next" id="toc-entry-7"&gt;What's Next?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Machine Learning"/><category term="Python"/></entry><entry><title>Free LLM Endpoints &amp; Dynamic Distributed Model Inference Client with UncloseAI.js</title><link href="https://russell.ballestrini.net/free-llm-endpoints-dynamic-distributed-model-inference-client-uncloseai/" rel="alternate"/><published>2025-02-19T21:28:00-05:00</published><updated>2025-02-19T21:28:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2025-02-19:/free-llm-endpoints-dynamic-distributed-model-inference-client-uncloseai/</id><summary type="html">&lt;div class="admonition note"&gt;
&lt;p class="first admonition-title"&gt;Note&lt;/p&gt;
&lt;p class="last"&gt;This post is a follow-up to our &lt;a class="reference external" href="/free-hermes-ai-unturf-com-uncloseai/"&gt;original post published on 2024-10-21&lt;/a&gt;. Over the last few months, we've partnered with &lt;strong&gt;TDC&lt;/strong&gt; - &lt;a class="reference external" href="https://matrix.to/#/#disruptive-collective:matrix.org"&gt;the disruptive collective&lt;/a&gt; to make cutting-edge Machine Learning models accessible from any device, via our always available model servers to our new bot's for discord, matrix, &amp;amp; web!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="overview"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Overview …&lt;/a&gt;&lt;/h2&gt;&lt;/div&gt;</summary><content type="html">&lt;div class="admonition note"&gt;
&lt;p class="first admonition-title"&gt;Note&lt;/p&gt;
&lt;p class="last"&gt;This post is a follow-up to our &lt;a class="reference external" href="/free-hermes-ai-unturf-com-uncloseai/"&gt;original post published on 2024-10-21&lt;/a&gt;. Over the last few months, we've partnered with &lt;strong&gt;TDC&lt;/strong&gt; - &lt;a class="reference external" href="https://matrix.to/#/#disruptive-collective:matrix.org"&gt;the disruptive collective&lt;/a&gt; to make cutting-edge Machine Learning models accessible from any device, via our always available model servers to our new bot's for discord, matrix, &amp;amp; web!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="overview"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Overview&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;We’ve been pushing the boundaries of accessible Machine Learning. We built &lt;a class="reference external" href="https://ai.unturf.com"&gt;ai.unturf.com&lt;/a&gt; and &lt;a class="reference external" href="hhttps://git.unturf.com/engineering/unturf/ai.unturf.com/-/blob/master/uncloseai.js"&gt;uncloseai.js&lt;/a&gt; to provide you with free, state-of-the-art Machine Learning tools that are as &lt;em&gt;free as in beer&lt;/em&gt; and as &lt;em&gt;free as in freedom&lt;/em&gt;. A huge shout out to &lt;strong&gt;TDC&lt;/strong&gt;  for powering all the development work on the various bots on the telegram, discord, &amp;amp; matrix platforms!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="dynamic-model-endpoint-discovery"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Dynamic Model &amp;amp; Endpoint Discovery&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Instead of maintaining a static list of models—which can quickly become outdated—our service is designed for dynamic discovery. Each of our model server endpoints offers an API route that returns the current list of supported models. This means you can always retrieve the latest model roster directly from the source.&lt;/p&gt;
&lt;p&gt;We encourage you to review the dynamic model querying approaches used by both &lt;cite&gt;uncloseai.js&lt;/cite&gt; and &lt;a class="reference external" href="https://opencompletion.com"&gt;opencompletion.com&lt;/a&gt; to see how they simplify integration &amp;amp; keep things current.&lt;/p&gt;
&lt;p&gt;Here’s where you can find the model lists for each endpoint:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;Hermes Endpoints:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;hermes1:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://hermes.ai.unturf.com/v1"&gt;https://hermes.ai.unturf.com/v1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://hermes.ai.unturf.com/v1/models"&gt;https://hermes.ai.unturf.com/v1/models&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;hermes2:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://hermes2.ai.unturf.com/v1"&gt;https://hermes2.ai.unturf.com/v1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://hermes2.ai.unturf.com/v1/models"&gt;https://hermes2.ai.unturf.com/v1/models&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;By dynamically querying each endpoint for its supported models, you ensure your integration remains up to date without needing to hardcode model names.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="example-usage"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Example Usage&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Below is a quick cURL example for interacting with our new &lt;cite&gt;adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic&lt;/cite&gt; endpoint:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;-X&lt;span class="w"&gt; &lt;/span&gt;POST&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-H&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Content-Type: application/json&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{&lt;/span&gt;
&lt;span class="s1"&gt;        &amp;quot;prompt&amp;quot;: &amp;quot;Give a Python Fizzbuzz solution in one line of code&amp;quot;,&lt;/span&gt;
&lt;span class="s1"&gt;        &amp;quot;model&amp;quot;: &amp;quot;adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic&amp;quot;,&lt;/span&gt;
&lt;span class="s1"&gt;        &amp;quot;temperature&amp;quot;: 0.5,&lt;/span&gt;
&lt;span class="s1"&gt;        &amp;quot;max_tokens&amp;quot;: 150&lt;/span&gt;
&lt;span class="s1"&gt;      }&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;https://hermes.ai.unturf.com/v1/completions
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="integration-made-easy"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Integration Made Easy&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Our service supports any programming language. With client libraries available for Python and Node.js—and a web-client-only solution via &lt;cite&gt;uncloseai.js&lt;/cite&gt;—integrating Machine Learning into your application or website has never been easier. Try to use the default openai client oin your native language.&lt;/p&gt;
&lt;p&gt;The browser-based integration is ideal for static sites or CDNs, eliminating the need for a dedicated backend server or API key.&lt;/p&gt;
&lt;p&gt;Add the following HTML snippet to your site to get started:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;https://uncloseai.com/uncloseai.js&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;module&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That's it! This single line provides a floating Machine Learning assistant button with page-aware contextual help, text-to-speech functionality, translation capabilities, conversation history storage, and compatibility with any CSS framework.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Try It Out!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There is a demo connected to this static page!&lt;/p&gt;
&lt;p&gt;Dive in and explore our service—use it as-is or build your own custom client. Let's keep democratizing Machine Learning and empower everyone to harness its power.&lt;/p&gt;
&lt;p&gt;We’ve also open-sourced &lt;a class="reference external" href="https://git.unturf.com/engineering/unturf/discord.agents.ai.unturf.com#unturfdiscordbot"&gt;Matrix &amp;amp; Discord Bots&lt;/a&gt; to showcase dynamic model loading in action.&lt;/p&gt;
&lt;p&gt;Join our &lt;a class="reference external" href="https://discord.gg/TZPmQdPj8a"&gt;Discord server&lt;/a&gt; to collaborate with us on building agents, environments, and much more!&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#overview" id="toc-entry-1"&gt;Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#dynamic-model-endpoint-discovery" id="toc-entry-2"&gt;Dynamic Model &amp;amp; Endpoint Discovery&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#example-usage" id="toc-entry-3"&gt;Example Usage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#integration-made-easy" id="toc-entry-4"&gt;Integration Made Easy&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Machine Learning"/><category term="Python"/></entry><entry><title>Free Hermes Machine Learning ai.unturf.com &amp; uncloseai.js</title><link href="https://russell.ballestrini.net/free-hermes-ai-unturf-com-uncloseai/" rel="alternate"/><published>2024-10-21T20:52:00-04:00</published><updated>2024-10-21T20:52:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2024-10-21:/free-hermes-ai-unturf-com-uncloseai/</id><summary type="html">&lt;p&gt;Hey all I created &lt;a class="reference external" href="https://ai.unturf.com"&gt;ai.unturf.com&lt;/a&gt; and &lt;a class="reference external" href="https://uncloseai.com/uncloseai.js"&gt;uncloseai.js&lt;/a&gt; over the past two weeks &amp;amp; this post introduces them to make Machine Learning more accessible to everyone.&lt;/p&gt;
&lt;p&gt;The Machine Learning service is powered by the model Nous Research's &lt;a class="reference external" href="https://nousresearch.com/hermes3/&amp;quot;"&gt;adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic&lt;/a&gt;. Our mission is to provide accessible Machine Learning tools …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Hey all I created &lt;a class="reference external" href="https://ai.unturf.com"&gt;ai.unturf.com&lt;/a&gt; and &lt;a class="reference external" href="https://uncloseai.com/uncloseai.js"&gt;uncloseai.js&lt;/a&gt; over the past two weeks &amp;amp; this post introduces them to make Machine Learning more accessible to everyone.&lt;/p&gt;
&lt;p&gt;The Machine Learning service is powered by the model Nous Research's &lt;a class="reference external" href="https://nousresearch.com/hermes3/&amp;quot;"&gt;adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic&lt;/a&gt;. Our mission is to provide accessible Machine Learning tools for everyone, embodying the principles of both free as in beer &amp;amp; free as in freedom. You can interact with our model without any cost, and we encourage to contributions and building upon the open-source code &amp;amp; models.&lt;/p&gt;
&lt;p&gt;One of the key features of ai.unturf.com is its compatibility with various programming languages. We provide examples in Python and Node.js, allowing developers to easily integrate our Machine Learning service into their applications. Moreover, we offer a web-client only solution using uncloseai.js, which enables static sites or CDNs hosting HTML content to interact with Machine Learning services directly from the browser without the need for an intermediary server/client. This approach eliminates the requirement for a valid API key, making it an efficient and accessible option for thin clients often on battery.&lt;/p&gt;
&lt;p&gt;To demonstrate the ease of use of our Machine Learning service, here is a cURL example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;-X&lt;span class="w"&gt; &lt;/span&gt;POST&lt;span class="w"&gt; &lt;/span&gt;-H&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Content-Type: application/json&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{&lt;/span&gt;
&lt;span class="s1"&gt;    &amp;quot;prompt&amp;quot;: &amp;quot;Give a Python Fizzbuzz solution in one line of code&amp;quot;,&lt;/span&gt;
&lt;span class="s1"&gt;    &amp;quot;model&amp;quot;: &amp;quot;adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic&amp;quot;,&lt;/span&gt;
&lt;span class="s1"&gt;    &amp;quot;temperature&amp;quot;: 0.5,&lt;/span&gt;
&lt;span class="s1"&gt;    &amp;quot;max_tokens&amp;quot;: 150&lt;/span&gt;
&lt;span class="s1"&gt;}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;https://hermes.ai.unturf.com/v1/completions
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Also the coolest part is we found that because we don't require a valid API key, a tiny bit of javascript goes a long way to create a web client-only solution which is great for static sites and CDNs.&lt;/p&gt;
&lt;p&gt;In this demo architecture, the browser serves as the client, directly interacting with the Machine Learning service without the need for an intermediary server/client.&lt;/p&gt;
&lt;p&gt;By using uncloseai.js, developers can create thin clients on battery-powered devices, making Machine Learning more accessible and efficient.&lt;/p&gt;
&lt;p&gt;Here is the HTML we needed to add to _this_ site, to make it work:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;https://uncloseai.com/uncloseai.js&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;module&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That's it! This single line provides a floating Machine Learning assistant button with page-aware contextual help, text-to-speech functionality, translation capabilities, conversation history storage, and compatibility with any CSS framework.&lt;/p&gt;
&lt;p&gt;I invite you to explore the service for yourself. Consider using it as is or building your own app/client. Let's make Machine Learning more accessible and create a world where everyone can benefit from its power.&lt;/p&gt;
&lt;p&gt;Before you go, if you want to try it out live on this static site, &lt;a class="reference external" href="#uncloseai-example"&gt;click here&lt;/a&gt; and and ask a question about the post. it knows how to code. : )&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Machine Learning"/><category term="Python"/></entry><entry><title>Efficient Battleship Board Generation and Data Management with Python</title><link href="https://russell.ballestrini.net/exploring-battleship-board-generation/" rel="alternate"/><published>2024-09-14T16:49:00-04:00</published><updated>2024-09-14T16:49:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2024-09-14:/exploring-battleship-board-generation/</id><summary type="html">&lt;p&gt;Important: In my research so far, nobody seems to have a definite answer for how many unique non-overlapping battleship configurations there truely are...&lt;/p&gt;
&lt;p&gt;In this blog post, we explore the process of generating all possible configurations of a Battleship game board using Python. Our goal is to efficiently generate a …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Important: In my research so far, nobody seems to have a definite answer for how many unique non-overlapping battleship configurations there truely are...&lt;/p&gt;
&lt;p&gt;In this blog post, we explore the process of generating all possible configurations of a Battleship game board using Python. Our goal is to efficiently generate a large number of game boards using parallel processing, while also considering the constraints of memory and storage.&lt;/p&gt;
&lt;div class="section" id="background"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Background&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The Battleship game involves placing ships on a grid without overlapping. Each board configuration must include all ships, and the challenge is to generate as many unique configurations as possible. With an estimated 30 billion possible boards, we need an efficient approach to explore this vast solution space.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="project-setup"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Project Setup&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To tackle this problem, we used Python and its multiprocessing capabilities. Here's a step-by-step guide to our approach:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;strong&gt;Define Ship Configurations&lt;/strong&gt;:
We defined the ships and their sizes, ensuring each ship has a unique identifier.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Random Ship Placement&lt;/strong&gt;:
We implemented a function to randomly place ships on a grid, ensuring no overlaps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parallel Processing&lt;/strong&gt;:
We used Python's &lt;cite&gt;multiprocessing&lt;/cite&gt; module to divide the task among several processes, each generating a subset of possible boards.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Progress Tracking&lt;/strong&gt;:
We implemented progress bars to track the number of solutions found in real-time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Storage&lt;/strong&gt;:
Each valid board configuration was saved immediately in JSON format to ensure progress was recorded.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="python-script"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Python Script&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here is the Python script used for the experiment:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;multiprocessing&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;tqdm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tqdm&lt;/span&gt;

&lt;span class="c1"&gt;# Define ship configurations&lt;/span&gt;
&lt;span class="n"&gt;ships&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;Carrier&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;Battleship&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;Cruiser&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;Submarine&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;Destroyer&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;place_ships&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Randomly place ships on a grid ensuring fair use of all cells.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;grid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;grid_size&lt;/span&gt;
    &lt;span class="n"&gt;positions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid_size&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ship&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ships&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;placed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;placed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;orientation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;horizontal&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;vertical&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;orientation&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;horizontal&amp;quot;&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
                    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                        &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ship&lt;/span&gt;
                    &lt;span class="n"&gt;placed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
            &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;orientation&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;vertical&amp;quot;&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
                    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                        &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ship&lt;/span&gt;
                    &lt;span class="n"&gt;placed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_boards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_queue&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Worker function to generate boards.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_boards&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;board&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;place_ships&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;output_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_boards&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_processes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Main function to manage multiprocessing and progress tracking.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;boards_per_process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total_boards&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;num_processes&lt;/span&gt;
    &lt;span class="n"&gt;output_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;processes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_processes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;boards_per_process&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_queue&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;processes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;battleship_boards.json&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;w&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tqdm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_boards&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Generating Boards&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;board&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;processes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a class="reference external" href="/uploads/2024/battleship-solvers/battleship_boards.py"&gt;battleship_boards.py&lt;/a&gt;&lt;/p&gt;
&lt;img alt="Time vs Processes" src="/uploads/2024/battleship-solvers/extended_estimated_days_vs_processes.png" /&gt;
&lt;p&gt;&lt;a class="reference external" href="/uploads/2024/battleship-solvers/plots_for_intro_blog_post.py"&gt;plots_for_intro_blog_post.py&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="data-compression-and-sampling"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Data Compression and Sampling&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Given the large size of the generated data, we used tarballs to compress the JSON file. This approach significantly reduced the storage requirements, making it easier to handle and transport the data. The compression was achieved using the following command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;tar&lt;span class="w"&gt; &lt;/span&gt;-czvf&lt;span class="w"&gt; &lt;/span&gt;battleship_boards.json.tar.gz&lt;span class="w"&gt; &lt;/span&gt;battleship_boards.json
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The original file size for 1,000,000 boards was 517 MB, which compressed down to 19 MB. This compression ratio highlights the efficiency of using tarballs for large datasets.&lt;/p&gt;
&lt;p&gt;To efficiently sample from the compressed tarball, we developed two bash scripts:&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Sample Multiple Boards&lt;/strong&gt;:
This script extracts a specified number of random boards from the tarball.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c1"&gt;# Variables&lt;/span&gt;
&lt;span class="nv"&gt;TARBALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;battleship_boards.json.tar.gz&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;SAMPLE_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sampled_boards.json&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;NUM_SAMPLES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Number of random samples to extract&lt;/span&gt;

&lt;span class="c1"&gt;# Stream the tarball and sample lines&lt;/span&gt;
tar&lt;span class="w"&gt; &lt;/span&gt;-xzOf&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TARBALL&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;shuf&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$NUM_SAMPLES&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$SAMPLE_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Output the location of the sampled file&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Random samples saved to &lt;/span&gt;&lt;span class="nv"&gt;$SAMPLE_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a class="reference external" href="/uploads/2024/battleship-solvers/sample_from_tarball.sh"&gt;sample_from_tarball.sh&lt;/a&gt;&lt;/p&gt;
&lt;ol class="arabic" start="2"&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Sample One Board&lt;/strong&gt;:
This script extracts a single random board from the tarball and outputs it to standard output.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c1"&gt;# Variable&lt;/span&gt;
&lt;span class="nv"&gt;TARBALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;battleship_boards.json.tar.gz&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Stream the tarball and sample one random line&lt;/span&gt;
tar&lt;span class="w"&gt; &lt;/span&gt;-xzOf&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$TARBALL&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;shuf&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a class="reference external" href="/uploads/2024/battleship-solvers/sample_one_from_tarball.sh"&gt;sample_one_from_tarball.sh&lt;/a&gt;&lt;/p&gt;
&lt;img alt="File Size Comparison" src="/uploads/2024/battleship-solvers/file_size_comparison.png" /&gt;
&lt;p&gt;&lt;a class="reference external" href="/uploads/2024/battleship-solvers/plots_for_intro_blog_post.py"&gt;plots_for_intro_blog_post.py&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="estimated-dataset-size"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;Estimated Dataset Size&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The complete dataset for 30,000,000,000 game boards, stored as a JSONL file, is estimated to be approximately 15.51 terabytes. This estimate is based on the size of 1,000,000 boards being 517 MB. The JSONL format is particularly useful for large datasets because it allows for efficient line-by-line processing.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="artifact-description"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;Artifact Description&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The JSONL file containing the complete dataset is a valuable artifact for researchers and developers interested in large-scale data analysis or machine learning applications. With sufficient storage space, this dataset can be used to explore various strategies for ship placement, analyze patterns, or train models for game Machine Learning development. The ability to sample from the dataset without full extraction further enhances its utility, allowing users to work with manageable subsets of data.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="results"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-7"&gt;Results&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The script successfully generated 1,000,000 boards in about 1 minute on a 4-core i5 from 2012. When considering a move to a 32 hyperthread machine, we estimated a theoretical speedup of 8x, reducing the time to approximately 2.6 days for 30 billion boards.&lt;/p&gt;
&lt;img alt="Ship Placement Distribution" src="/uploads/2024/battleship-solvers/ship_placement_distribution.png" /&gt;
&lt;p&gt;&lt;a class="reference external" href="/uploads/2024/battleship-solvers/plots_for_intro_blog_post.py"&gt;plots_for_intro_blog_post.py&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="analysis"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-8"&gt;Analysis&lt;/a&gt;&lt;/h2&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;strong&gt;Parallel Processing&lt;/strong&gt;: Using multiple processes allowed us to efficiently explore the solution space, significantly reducing computation time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Progress Tracking&lt;/strong&gt;: Real-time progress bars provided valuable feedback on the number of solutions found, enhancing the user experience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Storage&lt;/strong&gt;: Immediate saving of board configurations ensured that progress was not lost, even in the event of a system failure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compression&lt;/strong&gt;: We found that compressing the data reduced storage requirements significantly, making it feasible to handle large datasets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sampling&lt;/strong&gt;: The ability to sample from the compressed tarball without full extraction was crucial for managing large data efficiently.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="conclusion"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-9"&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This experiment demonstrated the power of parallel processing in Python for generating large datasets. By leveraging multiprocessing, we efficiently explored a vast solution space, providing insights into the potential of Python for handling complex computational tasks.&lt;/p&gt;
&lt;p&gt;If you're interested in exploring similar problems or optimizing computational tasks, consider using Python's multiprocessing capabilities to maximize performance and efficiency.&lt;/p&gt;
&lt;p&gt;This post was inspired by a conversation with an Machine Learning assistant, highlighting the potential of Machine Learning in guiding and enhancing problem-solving processes.&lt;/p&gt;
&lt;p&gt;Full conversation here:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="/uploads/2024/battleship-solvers/efficient-battleship-board-generation-data-management-python.json"&gt;efficient-battleship-board-generation-data-management-python.json&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;See &lt;a class="reference external" href="https://github.com/russellballestrini/flask-socketio-llm-completions"&gt;flask-socketio-llm-completions&lt;/a&gt; for more Machine Learning tooling!&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#background" id="toc-entry-1"&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#project-setup" id="toc-entry-2"&gt;Project Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#python-script" id="toc-entry-3"&gt;Python Script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#data-compression-and-sampling" id="toc-entry-4"&gt;Data Compression and Sampling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#estimated-dataset-size" id="toc-entry-5"&gt;Estimated Dataset Size&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#artifact-description" id="toc-entry-6"&gt;Artifact Description&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#results" id="toc-entry-7"&gt;Results&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#analysis" id="toc-entry-8"&gt;Analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#conclusion" id="toc-entry-9"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Python"/><category term="Machine Learning"/></entry><entry><title>Comparing Elixir and Phoenix Performance with a Community-Driven OpenAI Client</title><link href="https://russell.ballestrini.net/comparing-elixir-and-phoenix-performance-with-openai-client/" rel="alternate"/><published>2024-08-30T17:26:00-04:00</published><updated>2024-08-30T17:26:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2024-08-30:/comparing-elixir-and-phoenix-performance-with-openai-client/</id><summary type="html">&lt;p&gt;As a developer exploring the capabilities of Elixir and Phoenix, I recently conducted a performance experiment using a community-driven OpenAI client. The goal was to evaluate how well Elixir, with its powerful concurrency model, handles high-volume API requests. The results were impressive, demonstrating Elixir's efficiency and robustness in managing concurrent …&lt;/p&gt;</summary><content type="html">&lt;p&gt;As a developer exploring the capabilities of Elixir and Phoenix, I recently conducted a performance experiment using a community-driven OpenAI client. The goal was to evaluate how well Elixir, with its powerful concurrency model, handles high-volume API requests. The results were impressive, demonstrating Elixir's efficiency and robustness in managing concurrent tasks across multiple CPU cores.&lt;/p&gt;
&lt;div class="section" id="background"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Background&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;At &lt;a class="reference external" href="https://www.coursemojo.com/"&gt;CourseMojo, we are scaling an AI assistant teacher for public schools&lt;/a&gt; which provides real-time, intelligent responses to students' answers to open-ended questions. During our load testing, we identified that heavy CPU utilization was primarily due to calls to the OpenAI API. This prompted me to explore Elixir and Phoenix to see if they could offer a more efficient solution.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="project-setup"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Project Setup&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To set up the project, we used the following steps:&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Create a New Elixir Project&lt;/strong&gt;:
Run the following command to create a new Elixir project and navigate into the directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;mix&lt;span class="w"&gt; &lt;/span&gt;local.hex
mix&lt;span class="w"&gt; &lt;/span&gt;archive.install&lt;span class="w"&gt; &lt;/span&gt;hex&lt;span class="w"&gt; &lt;/span&gt;phx_new
mix&lt;span class="w"&gt; &lt;/span&gt;phx.new&lt;span class="w"&gt; &lt;/span&gt;openai_experiment&lt;span class="w"&gt; &lt;/span&gt;--no-ecto
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;openai_experiment
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Add Dependencies&lt;/strong&gt;:
Add the &lt;a class="reference external" href="https://github.com/dvcrn/ex_openai"&gt;ex_openai&lt;/a&gt;  library to your &lt;tt class="docutils literal"&gt;mix.exs&lt;/tt&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kd"&gt;defp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ex_openai&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;~&amp;gt; 1.6&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Run &lt;tt class="docutils literal"&gt;mix deps.get&lt;/tt&gt; to fetch the dependencies.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Configure the Client&lt;/strong&gt;:
Set up your configuration in &lt;tt class="docutils literal"&gt;config/config.exs&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:ex_openai&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="ss"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;OPENAI_API_KEY&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# the ex_openai http client seems to use this.&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:hackney&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="ss"&gt;pool_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="ss"&gt;max_connections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1800&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Increase File Descriptor Limit&lt;/strong&gt;:
To handle more than 1000 concurrent connections, increase the file descriptor limit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;ulimit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Implement the Concurrency Logic&lt;/strong&gt;:
We used &lt;tt class="docutils literal"&gt;Task.async_stream/3&lt;/tt&gt; to manage concurrency, allowing us to process tasks efficiently across multiple CPU cores.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="elixir-script"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Elixir Script&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here is the Elixir script &lt;tt class="docutils literal"&gt;lib/openai_experiment.ex&lt;/tt&gt; used for the experiment:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kd"&gt;defmodule&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;OpenaiExperiment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kn"&gt;alias&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ExOpenAI.Chat&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kn"&gt;alias&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ExOpenAI.Components.ChatCompletionRequestUserMessage&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="na"&gt;@max_retries&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="na"&gt;@retry_delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# milliseconds&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="na"&gt;@concurrency_limit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kd"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;generate_chat_completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;\\&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;msgs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="nc"&gt;ChatCompletionRequestUserMessage&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;You are a helpful assistant.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="nc"&gt;ChatCompletionRequestUserMessage&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;What is the number &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_chat_completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gpt-4o-mini&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nc"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Response for number &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;%&lt;/span&gt;&lt;span class="nc"&gt;HTTPoison.Error&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;when&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;@max_retries&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nc"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;HTTP error for number &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, attempt &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="ss"&gt;:timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;@retry_delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;generate_chat_completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;when&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;@max_retries&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nc"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Retrying number &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; due to &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, attempt &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="ss"&gt;:timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;@retry_delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;generate_chat_completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nc"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Final error for number &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kd"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;execute_chats_concurrently&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;1800&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="o"&gt;|&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;async_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;generate_chat_completion&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;max_concurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;@concurrency_limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="o"&gt;|&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;summarize_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nc"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Total execution time: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; seconds&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="kd"&gt;defp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;summarize_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;successes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_index&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="bp"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;false&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_reason&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="bp"&gt;_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;false&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nc"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Summary:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nc"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Successful tasks: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;successes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nc"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Failed tasks: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Run the test&lt;/span&gt;
&lt;span class="c1"&gt;#OpenaiExperiment.execute_chats_concurrently()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="compiling-and-running-the-example"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Compiling and Running the Example&lt;/a&gt;&lt;/h2&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Compile the Project&lt;/strong&gt;:
Ensure your project is compiled by running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;mix&lt;span class="w"&gt; &lt;/span&gt;compile
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Start the Elixir Interactive Shell&lt;/strong&gt;:
Launch the interactive Elixir shell with your project loaded:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;iex&lt;span class="w"&gt; &lt;/span&gt;-S&lt;span class="w"&gt; &lt;/span&gt;mix
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Run the Example&lt;/strong&gt;:
Once inside the interactive shell, execute the function to run the experiment:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nc"&gt;OpenaiExperiment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute_chats_concurrently&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="results"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;Results&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The results of the experiment were as follows:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;Successful tasks&lt;/strong&gt;: 1800&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failed tasks&lt;/strong&gt;: 0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total execution time&lt;/strong&gt;: 23.436011 seconds&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="section" id="analysis"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;Analysis&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The real time, which represents the total elapsed time, was significantly efficient for the Elixir script. Here are some key takeaways from the results:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;strong&gt;Concurrency Handling&lt;/strong&gt;: Elixir handled 1800 tasks efficiently, leveraging its lightweight process model with a concurrency limit of 500.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-Core Utilization&lt;/strong&gt;: Elixir utilized all 4 CPU cores, demonstrating its ability to efficiently distribute tasks across available resources.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in Retries&lt;/strong&gt;: The script included a retry mechanism to handle transient errors, ensuring robustness in task execution.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This experiment demonstrated that Elixir, with its concurrency model, is a strong contender for handling high-volume, concurrent API requests using a community-driven OpenAI client. Elixir's efficient concurrency handling makes it a superior choice for scenarios where performance and speed are critical.&lt;/p&gt;
&lt;p&gt;If you’re working on a project that involves extensive use of the OpenAI API or any other high-volume API interactions, consider leveraging Elixir to maximize performance and efficiency. The results of this experiment highlight the potential gains in speed and responsiveness that can be achieved with the right choice of technology.&lt;/p&gt;
&lt;p&gt;It's worth noting that the OpenAI client used in this experiment is community-driven, not official, which also impacts performance. Additionally, keep an eye on developments in Elixir and Phoenix, as they continue to evolve and improve.&lt;/p&gt;
&lt;p&gt;This post was written with the help of GPT-4 using the Elixir &lt;a class="reference external" href="https://github.com/dvcrn/ex_openai"&gt;ex_openai&lt;/a&gt; library.&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#background" id="toc-entry-1"&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#project-setup" id="toc-entry-2"&gt;Project Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#elixir-script" id="toc-entry-3"&gt;Elixir Script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#compiling-and-running-the-example" id="toc-entry-4"&gt;Compiling and Running the Example&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#results" id="toc-entry-5"&gt;Results&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#analysis" id="toc-entry-6"&gt;Analysis&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/></entry><entry><title>Comparing Node.js and Python Performance with the Official OpenAI Client</title><link href="https://russell.ballestrini.net/comparing-nodejs-and-python-performance-with-openai-client/" rel="alternate"/><published>2024-05-28T08:49:00-04:00</published><updated>2024-05-28T08:49:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2024-05-28:/comparing-nodejs-and-python-performance-with-openai-client/</id><summary type="html">&lt;p&gt;As a seasoned Python developer, I've always appreciated the versatility and power of Python. I found out today that when it comes to handling high-volume API requests, CPU performance can be a bottleneck and cause stability issues if not throttled to around 80% utilization. I conducted a performance comparison between …&lt;/p&gt;</summary><content type="html">&lt;p&gt;As a seasoned Python developer, I've always appreciated the versatility and power of Python. I found out today that when it comes to handling high-volume API requests, CPU performance can be a bottleneck and cause stability issues if not throttled to around 80% utilization. I conducted a performance comparison between Node.js and Python using the official OpenAI client, and the results were quite revealing. Node.js outperformes Python in this specific use case.&lt;/p&gt;
&lt;div class="section" id="background"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Background&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;At &lt;a class="reference external" href="https://coursemojo.com/"&gt;CourseMojo, we are scaling an Machine Learning assistant teacher for public schools&lt;/a&gt; which provides real-time, intelligent responses to students' answers to open ended questions. During our load testing in my sandbox environment, we identified that heavy CPU utilization was primarily due to calls to the OpenAI API. This prompted me to explore different programming languages to see if the problem existed elsewhere.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="the-experiment"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;The Experiment&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To compare the performance of Node.js and Python, I set up two scripts that interact with the OpenAI API. The goal was to measure the time taken to process a large number of tasks concurrently. Here’s a brief overview of the setup:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;Node.js Script&lt;/strong&gt;: Processed 1800 tasks, with a concurrency limit of 200 tasks at a time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python Script&lt;/strong&gt;: Processed 300 tasks, with a concurrency limit of 20 tasks at a time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both scripts were designed to send chat messages to the OpenAI API and receive responses. The tasks were structured to simulate real-world usage, where multiple requests are sent concurrently in the background with a single process running on a single CPU core. This mocks our production application.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="node-js-script"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Node.js Script&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here is the Node.js script used for the experiment, we use p-limit to throttle the concurrency to healthy CPU levels for background work to continue in process:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;OpenAI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;openai&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pLimit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;p-limit&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;OPENAI_API_KEY&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;generateChatCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;gpt-3.5-turbo&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;resultText&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;resultText&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;resultText&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Generate an array of chats&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chats&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1800&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;system&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;You are a helpful assistant.&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;user&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`What is the number &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;?`&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// Function to execute chats with controlled concurrency&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;executeChatsConcurrently&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;pLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// limit the number of concurrent API calls.&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;promises&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;chats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;generateChatCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;promises&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Print the completions&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`Chat Completion Result &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;executeChatsConcurrently&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="python-script"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Python Script&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here is the Python script used for the experiment, we use a semaphore to throttle the concurrency to healthy CPU levels for background work to continue in process:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncOpenAI&lt;/span&gt;

&lt;span class="n"&gt;async_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AsyncOpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_text_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;async_client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gpt-3.5-turbo&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;generated_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;generated_text&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Generated text for &amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;generated_text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Generate a list of chats&lt;/span&gt;
&lt;span class="n"&gt;chats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;messages&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;system&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;You are a helpful assistant.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;user&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;What is the number &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# limit concurrent tasks.&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;generate_text_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;messages&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chats&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="results"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;Results&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The results of the experiment were as follows:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Node.js Script&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;real    0m10.659s
user    0m8.277s
sys     0m0.544s
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Python Script&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;real    0m15.991s
user    0m10.041s
sys     0m0.229s
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="analysis"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;Analysis&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;cite&gt;real&lt;/cite&gt; time, which represents the total elapsed time, was significantly lower for the Node.js script compared to the Python script. Here are some key takeaways from the results:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;strong&gt;Concurrency Handling&lt;/strong&gt;: Node.js handled 1800 tasks with a concurrency limit of 200, while Python handled 300 tasks with a concurrency limit of 20. Despite the higher number of tasks and concurrency in Node.js, it completed the tasks faster.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event-Driven Architecture&lt;/strong&gt;: Node.js’s event-driven, non-blocking I/O model is highly efficient for handling multiple concurrent tasks. This architecture allows Node.js to manage a large number of simultaneous connections with minimal overhead, making it ideal for high-volume API interactions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU Utilization&lt;/strong&gt;: Both scripts showed healthy CPU core usage, but Node.js managed to utilize the CPU more efficiently, resulting in faster completion times.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User and System Time&lt;/strong&gt;: The &lt;cite&gt;user&lt;/cite&gt; and &lt;cite&gt;sys&lt;/cite&gt; times indicate the CPU time spent in user-mode and kernel-mode, respectively. Node.js showed a higher &lt;cite&gt;user&lt;/cite&gt; time but a lower &lt;cite&gt;sys&lt;/cite&gt; time compared to Python, suggesting that Node.js was more efficient in executing user-level code.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="conclusion"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-7"&gt;Conclusion&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The experiment clearly demonstrated that Node.js outperforms Python in handling high-volume, concurrent API requests using the official OpenAI client. The event-driven architecture of Node.js, combined with its efficient concurrency handling, makes it a superior choice for scenarios where performance and speed are critical.&lt;/p&gt;
&lt;p&gt;While Python remains a powerful and versatile language, especially in the realm of data science and machine learning, Node.js proves to be a better option for high-performance, real-time applications that require efficient handling of multiple concurrent tasks.&lt;/p&gt;
&lt;p&gt;If you’re working on a project that involves extensive use of the OpenAI API or any other high-volume API interactions, consider leveraging Node.js to maximize performance and efficiency. The results of this experiment highlight the potential gains in speed and responsiveness that can be achieved with the right choice of technology.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="final-thoughts"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-8"&gt;Final Thoughts&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Choosing the right tool for the job is crucial in software development. While both Node.js and Python have their strengths, understanding their performance characteristics can help you make informed decisions that align with your project’s requirements. In the case of high-volume API interactions, Node.js stands out as the clear winner, offering superior performance and efficiency.&lt;/p&gt;
&lt;p&gt;It's worth noting that the OpenAI client uses &lt;cite&gt;httpx&lt;/cite&gt; for Python and &lt;cite&gt;node-fetch&lt;/cite&gt; for Node.js by default. This choice of libraries also impacts performance. Additionally, there is hope for Python's future performance improvements, such as the potential removal of the Global Interpreter Lock (GIL) in later versions, which could significantly enhance Python's concurrency capabilities.&lt;/p&gt;
&lt;p&gt;For now, if performance is a critical factor in your project, especially for handling numerous concurrent API requests, Node.js is the way to go. But keep an eye on Python's developments, as it continues to evolve and improve.&lt;/p&gt;
&lt;p&gt;This post was written with the help of gpt-4 using &lt;a class="reference external" href="https://github.com/russellballestrini/flask-socketio-llm-completions"&gt;https://github.com/russellballestrini/flask-socketio-llm-completions&lt;/a&gt;&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#background" id="toc-entry-1"&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-experiment" id="toc-entry-2"&gt;The Experiment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#node-js-script" id="toc-entry-3"&gt;Node.js Script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#python-script" id="toc-entry-4"&gt;Python Script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#results" id="toc-entry-5"&gt;Results&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#analysis" id="toc-entry-6"&gt;Analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#conclusion" id="toc-entry-7"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#final-thoughts" id="toc-entry-8"&gt;Final Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Python"/><category term="Machine Learning"/><category term="Performance"/></entry><entry><title>Integrating OpenAI with Dry's Sample Chat Game</title><link href="https://russell.ballestrini.net/integrating-openai-with-dry-sample-chat-game/" rel="alternate"/><published>2024-02-17T14:11:00-05:00</published><updated>2024-02-17T14:11:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2024-02-17:/integrating-openai-with-dry-sample-chat-game/</id><summary type="html">&lt;p&gt;This tutorial demonstrates enhancing &lt;a class="reference external" href="https://gitlab.com/luckeyproductions/dry/-/blob/master/Source/Samples/16_Chat/Chat.cpp"&gt;Dry's Sample Chat Game&lt;/a&gt; by integrating OpenAI's language models, enabling the game to provide intelligent, Machine Learning-driven responses to user queries. Dry, the successor to Urho3D, offers a comprehensive framework for developing 2D and 3D games. Leveraging LLMs within the Dry engine opens up new possibilities …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This tutorial demonstrates enhancing &lt;a class="reference external" href="https://gitlab.com/luckeyproductions/dry/-/blob/master/Source/Samples/16_Chat/Chat.cpp"&gt;Dry's Sample Chat Game&lt;/a&gt; by integrating OpenAI's language models, enabling the game to provide intelligent, Machine Learning-driven responses to user queries. Dry, the successor to Urho3D, offers a comprehensive framework for developing 2D and 3D games. Leveraging LLMs within the Dry engine opens up new possibilities.&lt;/p&gt;
&lt;div class="section" id="background"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Background&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The Sample Chat game in Dry provides basic messaging functionality where multiple copies of the game client can connect to a server to communicate via text messages. Our aim is to extend this functionality to include responses from OpenAI's language models when messages contain specific triggers, such as &amp;quot;gpt-3&amp;quot;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="prerequisites"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Prerequisites&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ensure you have the following before starting:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;An OpenAI API key.&lt;/li&gt;
&lt;li&gt;A &lt;a class="reference external" href="https://russell.ballestrini.net/building-dry-and-park-from-source-on-fedora-linux/"&gt;configured Dry build environment&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Some familiarity with build tooling or at least the ability to copy and paste commands into the terminal.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="section" id="step-by-step-integration"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Step-by-Step Integration&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Follow these steps to integrate OpenAI with the Sample Chat game:&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Obtain the OpenAI C++ Client&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Before modifying the chat game, you need to download the necessary files from the OpenAI C++ client repository. Use the following commands to create a directory for these files and download them:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git/dry
mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;Source/ThirdParty/openai
mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;Source/ThirdParty/openai/nlohmann
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Source/ThirdParty/openai
wget&lt;span class="w"&gt; &lt;/span&gt;https://raw.githubusercontent.com/olrea/openai-cpp/main/include/openai/openai.hpp
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nlohmann
wget&lt;span class="w"&gt; &lt;/span&gt;https://raw.githubusercontent.com/olrea/openai-cpp/main/include/openai/nlohmann/json.hpp
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I also found the need to modify the client to code slightly for the JSON import, pardon me if this is ignorant:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;// #include &amp;lt;nlohmann/json.hpp&amp;gt;  // nlohmann/json&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;&amp;lt;Dry/ThirdParty/openai/nlohmann/json.hpp&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The client requires cURL development files, on Fedora:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;libcurl-devel
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Update the CMake Configuration&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Note: update &lt;tt class="docutils literal"&gt;PROJECT_SOURCE_DIR&lt;/tt&gt; with your file path.&lt;/p&gt;
&lt;p&gt;Apply the following diff to &lt;cite&gt;CMakeLists.txt&lt;/cite&gt; to include the OpenAI C++ client as well as cURL in your dry project:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/CMakeLists.txt b/CMakeLists.txt&lt;/span&gt;
&lt;span class="gh"&gt;index a1eff04..0f32bc7 100644&lt;/span&gt;
&lt;span class="gd"&gt;--- a/CMakeLists.txt&lt;/span&gt;
&lt;span class="gi"&gt;+++ b/CMakeLists.txt&lt;/span&gt;
&lt;span class="gu"&gt;@@ -188,6 +188,20 @@ else ()&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;endif ()
&lt;span class="w"&gt; &lt;/span&gt;file (MAKE_DIRECTORY ${THIRD_PARTY_INCLUDE_DIR})

&lt;span class="gi"&gt;+# Find the cURL library&lt;/span&gt;
&lt;span class="gi"&gt;+find_package(CURL REQUIRED)&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;span class="gi"&gt;+# Globally link cURL to all targets&lt;/span&gt;
&lt;span class="gi"&gt;+link_libraries(${CURL_LIBRARIES})&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;span class="gi"&gt;+# Assuming this is set to your project&amp;#39;s root directory&lt;/span&gt;
&lt;span class="gi"&gt;+set(PROJECT_SOURCE_DIR /home/fox/git/dry)&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;span class="gi"&gt;+# Create a symbolic link for openai&lt;/span&gt;
&lt;span class="gi"&gt;+execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink&lt;/span&gt;
&lt;span class="gi"&gt;+                ${PROJECT_SOURCE_DIR}/Source/ThirdParty/openai&lt;/span&gt;
&lt;span class="gi"&gt;+                ${CMAKE_BINARY_DIR}/include/Dry/ThirdParty/openai)&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Modify Sample/16_Chat/chat.cpp file&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Implement the changes outlined in the diffs below for &lt;tt class="docutils literal"&gt;chat.cpp&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gh"&gt;diff --git a/Source/Samples/16_Chat/Chat.cpp b/Source/Samples/16_Chat/Chat.cpp&lt;/span&gt;
&lt;span class="gh"&gt;index ee7c2b7..9d0e454 100644&lt;/span&gt;
&lt;span class="gd"&gt;--- a/Source/Samples/16_Chat/Chat.cpp&lt;/span&gt;
&lt;span class="gi"&gt;+++ b/Source/Samples/16_Chat/Chat.cpp&lt;/span&gt;
&lt;span class="gu"&gt;@@ -41,6 +41,7 @@&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;#include &amp;lt;Dry/UI/Text.h&amp;gt;
&lt;span class="w"&gt; &lt;/span&gt;#include &amp;lt;Dry/UI/UI.h&amp;gt;
&lt;span class="w"&gt; &lt;/span&gt;#include &amp;lt;Dry/UI/UIEvents.h&amp;gt;
&lt;span class="gi"&gt;+#include &amp;lt;Dry/ThirdParty/openai/openai.hpp&amp;gt;&lt;/span&gt;

&lt;span class="w"&gt; &lt;/span&gt;#include &amp;quot;Chat.h&amp;quot;

&lt;span class="gu"&gt;@@ -201,16 +202,58 @@ void Chat::HandleSend(StringHash /*eventType*/, VariantMap&amp;amp; eventData)&lt;/span&gt;

&lt;span class="w"&gt; &lt;/span&gt;    if (serverConnection)
&lt;span class="w"&gt; &lt;/span&gt;    {
&lt;span class="gd"&gt;-        // A VectorBuffer object is convenient for constructing a message to send&lt;/span&gt;
&lt;span class="gd"&gt;-        VectorBuffer msg;&lt;/span&gt;
&lt;span class="gd"&gt;-        msg.WriteString(text);&lt;/span&gt;
&lt;span class="gd"&gt;-        // Send the chat message as in-order and reliable&lt;/span&gt;
&lt;span class="gd"&gt;-        serverConnection-&amp;gt;SendMessage(MSG_CHAT, true, true, msg);&lt;/span&gt;
&lt;span class="gi"&gt;+        // Check if the message contains &amp;quot;gpt-3&amp;quot;&lt;/span&gt;
&lt;span class="gi"&gt;+        if (text.Contains(&amp;quot;gpt-3&amp;quot;))&lt;/span&gt;
&lt;span class="gi"&gt;+        {&lt;/span&gt;
&lt;span class="gi"&gt;+            // Initialize OpenAI&lt;/span&gt;
&lt;span class="gi"&gt;+            openai::start();&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;span class="gi"&gt;+            // Correctly construct the JSON payload as a std::string&lt;/span&gt;
&lt;span class="gi"&gt;+            std::string payload = std::string(R&amp;quot;({&amp;quot;model&amp;quot;: &amp;quot;gpt-3.5-turbo&amp;quot;, &amp;quot;messages&amp;quot;:[{&amp;quot;role&amp;quot;:&amp;quot;user&amp;quot;, &amp;quot;content&amp;quot;:&amp;quot;)&amp;quot;) + text.CString() + std::string(R&amp;quot;(&amp;quot;}], &amp;quot;max_tokens&amp;quot;: 600, &amp;quot;temperature&amp;quot;: 0.5})&amp;quot;);&lt;/span&gt;
&lt;span class="gi"&gt;+            nlohmann::json gptResponse;&lt;/span&gt;
&lt;span class="gi"&gt;+            try {&lt;/span&gt;
&lt;span class="gi"&gt;+                // Parse the payload to JSON and make the API call&lt;/span&gt;
&lt;span class="gi"&gt;+                gptResponse = openai::chat().create(nlohmann::json::parse(payload));&lt;/span&gt;
&lt;span class="gi"&gt;+            } catch (const std::exception&amp;amp; e) {&lt;/span&gt;
&lt;span class="gi"&gt;+                // Handle JSON parsing errors or API call failures&lt;/span&gt;
&lt;span class="gi"&gt;+                std::cerr &amp;lt;&amp;lt; &amp;quot;Error making API call or parsing response: &amp;quot; &amp;lt;&amp;lt; e.what() &amp;lt;&amp;lt; &amp;#39;\n&amp;#39;;&lt;/span&gt;
&lt;span class="gi"&gt;+                return;&lt;/span&gt;
&lt;span class="gi"&gt;+            }&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;span class="gi"&gt;+            std::string responseText;&lt;/span&gt;
&lt;span class="gi"&gt;+            try {&lt;/span&gt;
&lt;span class="gi"&gt;+                // Extract the response text from the JSON response&lt;/span&gt;
&lt;span class="gi"&gt;+                responseText = gptResponse[&amp;quot;choices&amp;quot;][0][&amp;quot;message&amp;quot;][&amp;quot;content&amp;quot;].get&amp;lt;std::string&amp;gt;();&lt;/span&gt;
&lt;span class="gi"&gt;+            } catch (const std::exception&amp;amp; e) {&lt;/span&gt;
&lt;span class="gi"&gt;+                // Handle errors in accessing the response content&lt;/span&gt;
&lt;span class="gi"&gt;+                std::cerr &amp;lt;&amp;lt; &amp;quot;Error extracting response text: &amp;quot; &amp;lt;&amp;lt; e.what() &amp;lt;&amp;lt; &amp;#39;\n&amp;#39;;&lt;/span&gt;
&lt;span class="gi"&gt;+                return;&lt;/span&gt;
&lt;span class="gi"&gt;+            }&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;span class="gi"&gt;+            // send the user&amp;#39;s message to gpt-3 to the server.&lt;/span&gt;
&lt;span class="gi"&gt;+            VectorBuffer msg1;&lt;/span&gt;
&lt;span class="gi"&gt;+            msg1.WriteString(text);&lt;/span&gt;
&lt;span class="gi"&gt;+            serverConnection-&amp;gt;SendMessage(MSG_CHAT, true, true, msg1);&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;span class="gi"&gt;+            // send the gpt-3 completion to the server.&lt;/span&gt;
&lt;span class="gi"&gt;+            VectorBuffer msg2;&lt;/span&gt;
&lt;span class="gi"&gt;+            msg2.WriteString(String(responseText.c_str()));&lt;/span&gt;
&lt;span class="gi"&gt;+            serverConnection-&amp;gt;SendMessage(MSG_CHAT, true, true, msg2);&lt;/span&gt;
&lt;span class="gi"&gt;+        }&lt;/span&gt;
&lt;span class="gi"&gt;+        else&lt;/span&gt;
&lt;span class="gi"&gt;+        {&lt;/span&gt;
&lt;span class="gi"&gt;+            // Normal chat message handling&lt;/span&gt;
&lt;span class="gi"&gt;+            VectorBuffer msg;&lt;/span&gt;
&lt;span class="gi"&gt;+            msg.WriteString(text);&lt;/span&gt;
&lt;span class="gi"&gt;+            serverConnection-&amp;gt;SendMessage(MSG_CHAT, true, true, msg);&lt;/span&gt;
&lt;span class="gi"&gt;+        }&lt;/span&gt;
&lt;span class="gi"&gt;+&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;        // Empty the text edit after sending
&lt;span class="w"&gt; &lt;/span&gt;        textEdit_-&amp;gt;SetText(String::EMPTY);
&lt;span class="w"&gt; &lt;/span&gt;    }
&lt;span class="w"&gt; &lt;/span&gt;}
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Prepare the Build Environment&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Before running CMake, ensure that any previous build configurations are cleared to avoid conflicts. This might involve deleting the CMake cache file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;CMakeCache.txt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# If exists&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then, generate the build configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;cmake&lt;span class="w"&gt; &lt;/span&gt;..&lt;span class="w"&gt; &lt;/span&gt;-DCMAKE_BUILD_TYPE&lt;span class="o"&gt;=&lt;/span&gt;Debug&lt;span class="w"&gt; &lt;/span&gt;-DRY_64BIT&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&lt;strong&gt;Build the Chat Game&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Compile the Sample Chat game with the newly integrated OpenAI C++ client:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;16_Chat/fast
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="testing-the-integration"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Testing the Integration&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After applying the changes and compiling the game, ensure your OpenAI API key is available to the game:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;your_openai_api_key_here&amp;#39;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Run the Sample Chat game and try sending a message containing &amp;quot;gpt-3&amp;quot;. You should see an intelligent response generated by OpenAI's language model.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;./bin/16_Chat
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Remember you'll need at least one instance of the game running as server mode before a client can interact with the LLM.&lt;/p&gt;
&lt;p&gt;That means you need to run &lt;tt class="docutils literal"&gt;./bin/16_Chat&lt;/tt&gt; at least twice in two different windows to see the experience.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="what-s-next"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;What's Next?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You've now integrated OpenAI into Dry's Sample Chat game, enhancing it with Machine Learning-driven conversational capabilities. Explore further by customizing triggers, integrating other models using the same OpenAI client, or expanding the game's features.&lt;/p&gt;
&lt;p&gt;I think personally I will try to get the client communicating with vllm likely running openchat.&lt;/p&gt;
&lt;p&gt;Happy coding, and enjoy bringing LLM capabilities to your Dry games!&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#background" id="toc-entry-1"&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#prerequisites" id="toc-entry-2"&gt;Prerequisites&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#step-by-step-integration" id="toc-entry-3"&gt;Step-by-Step Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#testing-the-integration" id="toc-entry-4"&gt;Testing the Integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#what-s-next" id="toc-entry-5"&gt;What's Next?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Machine Learning"/><category term="Game Development"/></entry><entry><title>Optimizing Memory Management for Plausible Docker on Ubuntu</title><link href="https://russell.ballestrini.net/optimizing-memory-management-for-plausible-docker-on-ubuntu/" rel="alternate"/><published>2024-02-05T17:40:00-05:00</published><updated>2024-02-05T17:40:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2024-02-05:/optimizing-memory-management-for-plausible-docker-on-ubuntu/</id><summary type="html">&lt;p&gt;Running Docker containers on a server with limited memory can lead to out-of-memory (OOM) issues, which can disrupt services and lead to downtime. This guide will show you how to increase swap space on an Ubuntu server to provide a buffer against OOM errors, using a real-world example from a …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Running Docker containers on a server with limited memory can lead to out-of-memory (OOM) issues, which can disrupt services and lead to downtime. This guide will show you how to increase swap space on an Ubuntu server to provide a buffer against OOM errors, using a real-world example from a server running multiple Docker containers.&lt;/p&gt;
&lt;div class="section" id="background"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Background&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Consider a typical scenario where an Ubuntu server is running several Docker containers:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;fox@analytics:~$ sudo docker ps
CONTAINER ID        IMAGE                                           PORTS                          NAMES
7677a77e5606        plausible/analytics:v2.0                        0.0.0.0:8000-&amp;gt;8000/tcp         plausible-plausible-1
0ea79b7d0c03        clickhouse/clickhouse-server:23.3.7.5-alpine    8123/tcp, 9000/tcp, 9009/tcp   plausible-plausible_events_db-1
33f6e8a30da4        postgres:14-alpine                              5432/tcp                       plausible-plausible_db-1
40400c725d7c        bytemark/smtp                                   25/tcp                         plausible-mail-1

fox@analytics:~$ free -m
              total        used        free      shared  buff/cache   available
Mem:            981         693          62          70         225          66
Swap:             0           0           0
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In this example, the server has less than 1 GB of RAM and no swap space configured. This configuration is prone to OOM errors, especially when the containers require more memory than what is available. In my case the service stayed online but the configuration management service &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;salt-minion&lt;/span&gt;&lt;/tt&gt; was unable to be reached to deploy new TLS certificates, the cert expired and prevented clients from sending metrics.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="creating-and-enabling-swap-space"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Creating and Enabling Swap Space&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To prevent OOM errors, we'll create a swap file. This script automates the process, ensuring that your system has additional virtual memory to handle peak loads.&lt;/p&gt;
&lt;p&gt;Save the script as &lt;tt class="docutils literal"&gt;setup_swap.sh&lt;/tt&gt; and execute it on your server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c1"&gt;# Size of the swap file&lt;/span&gt;
&lt;span class="nv"&gt;SWAP_SIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1G
&lt;span class="nv"&gt;SWAP_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/swapfile
&lt;span class="nv"&gt;SWAPPINESS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
&lt;span class="nv"&gt;CACHE_PRESSURE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;

&lt;span class="c1"&gt;# Create a swap file&lt;/span&gt;
fallocate&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$SWAP_SIZE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$SWAP_FILE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dd&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$SWAP_FILE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1048576&lt;/span&gt;

&lt;span class="c1"&gt;# Secure swap file permissions&lt;/span&gt;
chmod&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;600&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$SWAP_FILE&lt;/span&gt;

&lt;span class="c1"&gt;# Set up a Linux swap area&lt;/span&gt;
mkswap&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$SWAP_FILE&lt;/span&gt;

&lt;span class="c1"&gt;# Enable the swap file&lt;/span&gt;
swapon&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$SWAP_FILE&lt;/span&gt;

&lt;span class="c1"&gt;# Make the swap file permanent&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$SWAP_FILE&lt;/span&gt;&lt;span class="s2"&gt; none swap sw 0 0&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;/etc/fstab

&lt;span class="c1"&gt;# Set the swappiness value&lt;/span&gt;
sysctl&lt;span class="w"&gt; &lt;/span&gt;vm.swappiness&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$SWAPPINESS&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vm.swappiness=&lt;/span&gt;&lt;span class="nv"&gt;$SWAPPINESS&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;/etc/sysctl.conf

&lt;span class="c1"&gt;# Set the cache pressure value&lt;/span&gt;
sysctl&lt;span class="w"&gt; &lt;/span&gt;vm.vfs_cache_pressure&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$CACHE_PRESSURE&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vm.vfs_cache_pressure=&lt;/span&gt;&lt;span class="nv"&gt;$CACHE_PRESSURE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;/etc/sysctl.conf

&lt;span class="c1"&gt;# Output the results&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Swap file created and enabled:&amp;quot;&lt;/span&gt;
swapon&lt;span class="w"&gt; &lt;/span&gt;--show
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Swappiness set to &lt;/span&gt;&lt;span class="nv"&gt;$SWAPPINESS&lt;/span&gt;&lt;span class="s2"&gt; and cache pressure set to &lt;/span&gt;&lt;span class="nv"&gt;$CACHE_PRESSURE&lt;/span&gt;&lt;span class="s2"&gt;.&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This script will create a 1 GB swap file, configure the system to use it, and adjust kernel parameters to optimize memory usage.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="understanding-swappiness-and-cache-pressure"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Understanding Swappiness and Cache Pressure&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;tt class="docutils literal"&gt;swappiness&lt;/tt&gt; parameter influences how often the system uses swap space. A value of 10 encourages the system to keep processes in RAM, resorting to swap only when necessary.&lt;/p&gt;
&lt;p&gt;The &lt;tt class="docutils literal"&gt;vfs_cache_pressure&lt;/tt&gt; setting determines how aggressively the kernel reclaims memory from the cache. A value of 50 provides a balance between reclaiming memory and maintaining cache for quick file access.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="monitoring-your-system"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Monitoring Your System&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After increasing the swap space, monitor your system's memory usage with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;free&lt;span class="w"&gt; &lt;/span&gt;-m
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will help you understand if the swap space is sufficient or if further adjustments are needed.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="what-s-next"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;What's Next?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;By adding swap space and tuning kernel parameters, you've bolstered your server's ability to handle memory-intensive Docker containers. However, swap is not a replacement for physical RAM. If your server consistently uses a lot of swap, consider upgrading the RAM for better performance.&lt;/p&gt;
&lt;p&gt;Stay proactive in managing your server's resources to ensure uninterrupted service for your Dockerized applications. Happy hosting!&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#background" id="toc-entry-1"&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#creating-and-enabling-swap-space" id="toc-entry-2"&gt;Creating and Enabling Swap Space&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#understanding-swappiness-and-cache-pressure" id="toc-entry-3"&gt;Understanding Swappiness and Cache Pressure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#monitoring-your-system" id="toc-entry-4"&gt;Monitoring Your System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#what-s-next" id="toc-entry-5"&gt;What's Next?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Docker"/><category term="DevOps"/><category term="Salt"/></entry><entry><title>Understanding Pause Functionality in LucKey Park Built on Dry Engine</title><link href="https://russell.ballestrini.net/understanding-pause-functionality-in-luckey-park-game/" rel="alternate"/><published>2024-02-05T08:57:00-05:00</published><updated>2024-02-05T08:57:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2024-02-05:/understanding-pause-functionality-in-luckey-park-game/</id><summary type="html">&lt;p class="first last"&gt;Dive deep into the pause functionality of LucKey Park built on the Dry engine, exploring how the game's status is managed and toggled end-to-end.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;In today's post, we're examining the pause functionality of the &lt;a class="reference external" href="https://gitlab.com/luckeyproductions/games/park"&gt;LucKey Park game code&lt;/a&gt;, which is built on the 'Dry' engine. We'll uncover how the game's status is managed and toggled, especially in response to the cancel button (ESC key), and provide insights into the game's input handling and state management.&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Check out &lt;a class="reference external" href="https://luckeyproductions.itch.io/park"&gt;screenshots and videos of LucKey Park on itch.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="section" id="understanding-the-game-s-status-management"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Understanding the Game's Status Management&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The 'Park' game uses an enumeration &lt;tt class="docutils literal"&gt;GameStatus&lt;/tt&gt; with values like &lt;tt class="docutils literal"&gt;GS_MAIN&lt;/tt&gt;, &lt;tt class="docutils literal"&gt;GS_PLAY&lt;/tt&gt;, &lt;tt class="docutils literal"&gt;GS_PAUSED&lt;/tt&gt;, and &lt;tt class="docutils literal"&gt;GS_MODAL&lt;/tt&gt; to represent the game's current state. The &lt;tt class="docutils literal"&gt;Game&lt;/tt&gt; class in &lt;tt class="docutils literal"&gt;game.h&lt;/tt&gt; maintains a &lt;tt class="docutils literal"&gt;status_&lt;/tt&gt; variable to track this state.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;enum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;GameStatus&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GS_MAIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GS_PLAY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GS_PAUSED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GS_MODAL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Game&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;GameStatus&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;tt class="docutils literal"&gt;Game&lt;/tt&gt; class provides methods to get and set this status, as well as a &lt;tt class="docutils literal"&gt;TogglePause()&lt;/tt&gt; method that switches between &lt;tt class="docutils literal"&gt;GS_PLAY&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;GS_PAUSED&lt;/tt&gt;.&lt;/p&gt;
&lt;div class="section" id="binding-the-cancel-action"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Binding the Cancel Action&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In &lt;tt class="docutils literal"&gt;inputmaster.cpp&lt;/tt&gt;, the &lt;tt class="docutils literal"&gt;InputMaster&lt;/tt&gt; class binds the &lt;tt class="docutils literal"&gt;IA_CANCEL&lt;/tt&gt; action to the ESC key. This setup is crucial for handling the pause functionality when the player presses ESC.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IA_CANCEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;KEY_ESCAPE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="handling-the-cancel-action"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Handling the Cancel Action&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When the &lt;tt class="docutils literal"&gt;IA_CANCEL&lt;/tt&gt; action is triggered, the &lt;tt class="docutils literal"&gt;HandleAction&lt;/tt&gt; method in &lt;tt class="docutils literal"&gt;InputMaster&lt;/tt&gt; responds accordingly. If no tool is active, it calls &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gui-&amp;gt;ShowMainMenu()&lt;/span&gt;&lt;/tt&gt;, which indirectly pauses the game by changing the game's status.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;IA_CANCEL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sceneCursor&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gui&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ShowMainMenu&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="showing-the-main-menu-and-pausing-the-game"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Showing the Main Menu and Pausing the Game&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;tt class="docutils literal"&gt;ShowMainMenu()&lt;/tt&gt; method in &lt;tt class="docutils literal"&gt;gui.cpp&lt;/tt&gt; is where the game's status is set to &lt;tt class="docutils literal"&gt;GS_MODAL&lt;/tt&gt;, effectively pausing the game when the main menu is displayed.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;GUI::ShowMainMenu&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;mainMenu_&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Game&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GetSubsystem&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Game&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetStatus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GS_PLAY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;game&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GS_MODAL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;HideDash&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="the-main-update-loop-controlled-by-dry-engine"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;The Main Update Loop Controlled by Dry Engine&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;The Dry engine, like its predecessor Urho3D, manages the main game loop internally. Developers hook into this loop by subscribing to the &lt;tt class="docutils literal"&gt;E_UPDATE&lt;/tt&gt; event, which the engine dispatches every frame. This event allows developers to perform per-frame updates to their game logic.&lt;/p&gt;
&lt;p&gt;While the main update loop is not explicitly shown in the provided snippets, it's typically where the game checks the &lt;tt class="docutils literal"&gt;status_&lt;/tt&gt; variable each frame. If the status is &lt;tt class="docutils literal"&gt;GS_PAUSED&lt;/tt&gt; or &lt;tt class="docutils literal"&gt;GS_MODAL&lt;/tt&gt;, the loop skips updating the game world, pausing the game's logic.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="end-to-end-pause-functionality"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;End-to-End Pause Functionality&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;From the moment the player presses the ESC key to the game entering a paused state, the flow is as follows:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;The ESC key is pressed.&lt;/li&gt;
&lt;li&gt;&lt;tt class="docutils literal"&gt;InputMaster&lt;/tt&gt; detects the &lt;tt class="docutils literal"&gt;IA_CANCEL&lt;/tt&gt; action.&lt;/li&gt;
&lt;li&gt;&lt;tt class="docutils literal"&gt;HandleAction&lt;/tt&gt; calls &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gui-&amp;gt;ShowMainMenu()&lt;/span&gt;&lt;/tt&gt; if no tool is active.&lt;/li&gt;
&lt;li&gt;&lt;tt class="docutils literal"&gt;ShowMainMenu()&lt;/tt&gt; sets the game's status to &lt;tt class="docutils literal"&gt;GS_MODAL&lt;/tt&gt;.&lt;/li&gt;
&lt;li&gt;The main update loop, controlled by the Dry engine, respects this status and pauses the game.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This exploration has provided a clear understanding of how the pause functionality is implemented in Lucky Park using the Dry engine. It's a great example of how game state management and input handling work together to create responsive gameplay.&lt;/p&gt;
&lt;p&gt;Stay tuned for more deep dives into game development.&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#understanding-the-game-s-status-management" id="toc-entry-1"&gt;Understanding the Game's Status Management&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#binding-the-cancel-action" id="toc-entry-2"&gt;Binding the Cancel Action&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#handling-the-cancel-action" id="toc-entry-3"&gt;Handling the Cancel Action&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#showing-the-main-menu-and-pausing-the-game" id="toc-entry-4"&gt;Showing the Main Menu and Pausing the Game&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-main-update-loop-controlled-by-dry-engine" id="toc-entry-5"&gt;The Main Update Loop Controlled by Dry Engine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#end-to-end-pause-functionality" id="toc-entry-6"&gt;End-to-End Pause Functionality&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Programming"/><category term="Game Development"/></entry><entry><title>Installing YaCy on Ubuntu</title><link href="https://russell.ballestrini.net/installing-yacy-on-ubuntu/" rel="alternate"/><published>2024-02-04T11:02:00-05:00</published><updated>2024-02-04T11:02:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2024-02-04:/installing-yacy-on-ubuntu/</id><summary type="html">&lt;p&gt;This guide will walk you through installing YaCy on Ubuntu.&lt;/p&gt;
&lt;p&gt;By default YaCy is configured to bind to &lt;tt class="docutils literal"&gt;0.0.0.0&lt;/tt&gt; but it's admin interface is only accessible by default to a white list which includes &lt;tt class="docutils literal"&gt;localhost&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;127.0.0.1&lt;/tt&gt;, since my install is headless on a …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This guide will walk you through installing YaCy on Ubuntu.&lt;/p&gt;
&lt;p&gt;By default YaCy is configured to bind to &lt;tt class="docutils literal"&gt;0.0.0.0&lt;/tt&gt; but it's admin interface is only accessible by default to a white list which includes &lt;tt class="docutils literal"&gt;localhost&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;127.0.0.1&lt;/tt&gt;, since my install is headless on a remote Ubuntu server, I access the admin interface remotely via an SSH tunnel.&lt;/p&gt;
&lt;div class="section" id="installing-yacy"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Installing YaCy&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;run this script in a terminal, &lt;tt class="docutils literal"&gt;install_yacy.sh&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c1"&gt;# Update package lists&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;update

&lt;span class="c1"&gt;# Install Java Development Kit (JDK)&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;openjdk-21-jdk

&lt;span class="c1"&gt;# Check if Java is correctly installed&lt;/span&gt;
java&lt;span class="w"&gt; &lt;/span&gt;-version

&lt;span class="c1"&gt;# Download YaCy&lt;/span&gt;
wget&lt;span class="w"&gt; &lt;/span&gt;https://download.yacy.net/yacy_v1.924_20210209_10069.tar.gz

&lt;span class="c1"&gt;# Extract the downloaded file&lt;/span&gt;
tar&lt;span class="w"&gt; &lt;/span&gt;xfz&lt;span class="w"&gt; &lt;/span&gt;yacy_v1.924_20210209_10069.tar.gz

&lt;span class="c1"&gt;# Change to the extracted directory&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;yacy

&lt;span class="c1"&gt;# Start YaCy&lt;/span&gt;
./startYACY.sh
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="accessing-the-admin-interface-remotely-via-an-ssh-tunnel"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Accessing the Admin Interface Remotely via an SSH Tunnel&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You can create an SSH tunnel to your remote YaCy server, for example &lt;tt class="docutils literal"&gt;yacy.foxhop.net&lt;/tt&gt; so that when you access &lt;tt class="docutils literal"&gt;localhost:8090&lt;/tt&gt;, it tunnels to the remote host.&lt;/p&gt;
&lt;p&gt;Open a terminal on your local machine and run the following command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;-L&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8090&lt;/span&gt;:localhost:8090&lt;span class="w"&gt; &lt;/span&gt;user@yacy.foxhop.net
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Replace &lt;cite&gt;user&lt;/cite&gt; with your username on the remote host. Now, when you access &lt;cite&gt;localhost:8090&lt;/cite&gt; on your local machine, it will be tunneled to the remote host.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="securing-yacy-portal-and-admin-access-with-ssl-tls"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Securing YaCy Portal and admin access with SSL/TLS&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the early days of YaCy, encryption was not as prevalent as it is today. However, securing your YaCy instance with SSL/TLS is essential for modern web safety standards. Here's how to enable SSL/TLS encryption for your YaCy server:&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;Generate a keystore using the &lt;tt class="docutils literal"&gt;keytool&lt;/tt&gt; command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;keytool&lt;span class="w"&gt; &lt;/span&gt;-keystore&lt;span class="w"&gt; &lt;/span&gt;mySrvKeystore&lt;span class="w"&gt; &lt;/span&gt;-genkey&lt;span class="w"&gt; &lt;/span&gt;-keyalg&lt;span class="w"&gt; &lt;/span&gt;RSA&lt;span class="w"&gt; &lt;/span&gt;-alias&lt;span class="w"&gt; &lt;/span&gt;yacy.foxhop.net
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This command creates a keystore file named &lt;tt class="docutils literal"&gt;mySrvKeystore&lt;/tt&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Move the &lt;tt class="docutils literal"&gt;mySrvKeystore&lt;/tt&gt; file to the &lt;tt class="docutils literal"&gt;DATA/SETTINGS/&lt;/tt&gt; directory in your YaCy installation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;mySrvKeystore&lt;span class="w"&gt; &lt;/span&gt;/path/to/YaCy/DATA/SETTINGS/
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Edit the &lt;tt class="docutils literal"&gt;yacy.conf&lt;/tt&gt; file to configure YaCy to use the new keystore:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;vim&lt;span class="w"&gt; &lt;/span&gt;/path/to/YaCy/DATA/SETTINGS/yacy.conf
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Add or modify the following lines:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;keyStore=DATA/SETTINGS/mySrvKeystore
keyStorePassword=YourKeystorePassword
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Replace &lt;tt class="docutils literal"&gt;YourKeystorePassword&lt;/tt&gt; with the password you chose when you created the keystore with the &lt;tt class="docutils literal"&gt;keytool&lt;/tt&gt; command.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Restart YaCy to apply the SSL/TLS settings:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;/path/to/YaCy/stopYACY.sh
/path/to/YaCy/startYACY.sh
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now, you can access the YaCy admin interface securely via &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;https://localhost:8443&lt;/span&gt;&lt;/tt&gt;. By default, YaCy listens on port &lt;tt class="docutils literal"&gt;8443&lt;/tt&gt; for HTTPS, but this can be changed in the admin console, as was done in this case to use port &lt;tt class="docutils literal"&gt;8091&lt;/tt&gt; so that &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;https://localhost:8091&lt;/span&gt;&lt;/tt&gt; works instead. Ensure that HTTP remains on port &lt;tt class="docutils literal"&gt;8090&lt;/tt&gt; for DHT network access by peers.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="what-s-next"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;What's next?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;With SSL/TLS enabled, it's time to start a local crawl and consider sharing the results with the YaCy network. To do this, you may need to open port &lt;tt class="docutils literal"&gt;8090&lt;/tt&gt; on your router and forward it to your YaCy host. Before making changes to your network configuration, verify your peer mode status in the YaCy admin interface to understand your node's role in the network.&lt;/p&gt;
&lt;p&gt;Sharing your crawl index with the network allows for a distributed scraping effort, meaning that the data you collect can benefit all users of YaCy, not just your local instance. This is the essence of the Distributed Hash Table (DHT) that underpins YaCy's decentralized architecture.&lt;/p&gt;
&lt;p&gt;Continue to explore the capabilities of your YaCy server, and remember to update your documentation to assist others in their journey toward a more open and collaborative internet.`&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#installing-yacy" id="toc-entry-1"&gt;Installing YaCy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#accessing-the-admin-interface-remotely-via-an-ssh-tunnel" id="toc-entry-2"&gt;Accessing the Admin Interface Remotely via an SSH Tunnel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#securing-yacy-portal-and-admin-access-with-ssl-tls" id="toc-entry-3"&gt;Securing YaCy Portal and admin access with SSL/TLS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#what-s-next" id="toc-entry-4"&gt;What's next?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/></entry><entry><title>Building 'Dry' and 'Park' from Source on Fedora Linux</title><link href="https://russell.ballestrini.net/building-dry-and-park-from-source-on-fedora-linux/" rel="alternate"/><published>2024-02-03T09:19:00-05:00</published><updated>2024-02-03T09:19:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2024-02-03:/building-dry-and-park-from-source-on-fedora-linux/</id><summary type="html">&lt;p class="first last"&gt;Learn how to compile the 'Dry' game engine and the 'Park' game from source on Fedora Linux with debugging enabled, and how to analyze core dumps for debugging.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;In this guide, we'll explore how to compile the 'Dry' game engine and the 'Park' game from source on Fedora Linux with debugging enabled. This process will give you a deeper understanding of the inner workings of game development and the satisfaction of running a game you built yourself.&lt;/p&gt;
&lt;p&gt;For Ubuntu, go here for pre-compiled game:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://luckeyproductions.itch.io/park"&gt;https://luckeyproductions.itch.io/park&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="section" id="prerequisites"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Prerequisites&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Before diving in, ensure you have the necessary tools and libraries installed:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;li&gt;CMake&lt;/li&gt;
&lt;li&gt;GNU Make&lt;/li&gt;
&lt;li&gt;GCC or Clang&lt;/li&gt;
&lt;li&gt;X11 and audio system development libraries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Install these on Fedora with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;groupinstall&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Development Tools&amp;quot;&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;dnf&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;cmake&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;gcc-c++&lt;span class="w"&gt; &lt;/span&gt;libX11-devel&lt;span class="w"&gt; &lt;/span&gt;libXcursor-devel&lt;span class="w"&gt; &lt;/span&gt;libXinerama-devel&lt;span class="w"&gt; &lt;/span&gt;libXi-devel&lt;span class="w"&gt; &lt;/span&gt;libXrandr-devel&lt;span class="w"&gt; &lt;/span&gt;libXrender-devel&lt;span class="w"&gt; &lt;/span&gt;libXScrnSaver-devel&lt;span class="w"&gt; &lt;/span&gt;libXxf86vm-devel&lt;span class="w"&gt; &lt;/span&gt;pulseaudio-libs-devel&lt;span class="w"&gt; &lt;/span&gt;nas-libs-devel&lt;span class="w"&gt; &lt;/span&gt;qt5-qtbase-devel
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="cloning-the-repositories"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Cloning the Repositories&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Clone the 'Dry' and 'Park' repositories using Git:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/git
git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://gitlab.com/luckeyproductions/dry.git
git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://gitlab.com/luckeyproductions/games/park.git
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="building-dry-with-debugging-enabled"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Building Dry with Debugging Enabled&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Navigate to the 'dry' directory and prepare the build environment:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dry
mkdir&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;build
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Configure the build with CMake and enable debugging:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;cmake&lt;span class="w"&gt; &lt;/span&gt;..&lt;span class="w"&gt; &lt;/span&gt;-DCMAKE_BUILD_TYPE&lt;span class="o"&gt;=&lt;/span&gt;Debug&lt;span class="w"&gt; &lt;/span&gt;-DDRY_64BIT&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Compile the Dry library with debug symbols:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;make
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="building-park-with-debugging-enabled"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Building Park with Debugging Enabled&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the 'Park' project, modify the &lt;tt class="docutils literal"&gt;Park.pro&lt;/tt&gt; file to add the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-g&lt;/span&gt; &lt;span class="pre"&gt;-O0&lt;/span&gt;&lt;/tt&gt; flags for debugging, for example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nv"&gt;QMAKE_CXXFLAGS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-std&lt;span class="o"&gt;=&lt;/span&gt;c++17&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="w"&gt; &lt;/span&gt;-O0
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then set the &lt;tt class="docutils literal"&gt;DRY_HOME&lt;/tt&gt; environment variable to the path of your compiled Dry library:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;DRY_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/fox/git/dry/build
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Navigate to the 'park' directory and compile the game:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/fox/git/park
mkdir&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;build
qmake&lt;span class="w"&gt; &lt;/span&gt;../Park.pro
make
cp&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;../Resources&lt;span class="w"&gt; &lt;/span&gt;.
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="running-park"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;Running Park&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After a successful build, run the Park executable located in the &lt;tt class="docutils literal"&gt;build&lt;/tt&gt; directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;./park
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="analyzing-core-dumps-on-fedora"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;Analyzing Core Dumps on Fedora&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If your application crashes, Fedora can generate core dumps, which are snapshots of the program's state at the time of the crash. These can be invaluable for debugging.&lt;/p&gt;
&lt;p&gt;List recent core dumps with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;coredumpctl&lt;span class="w"&gt; &lt;/span&gt;list
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To analyze a specific core dump, use &lt;tt class="docutils literal"&gt;gdb&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;gdb&lt;span class="w"&gt; &lt;/span&gt;/path/to/executable&lt;span class="w"&gt; &lt;/span&gt;/path/to/coredump
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;gdb&lt;span class="w"&gt; &lt;/span&gt;/home/fox/git/park/Park/park&lt;span class="w"&gt; &lt;/span&gt;core.291993
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Once in &lt;tt class="docutils literal"&gt;gdb&lt;/tt&gt;, use the &lt;tt class="docutils literal"&gt;bt&lt;/tt&gt; command to print a backtrace:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;(gdb) bt
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will show you the call stack at the time of the crash, which can help pinpoint the source of the problem.&lt;/p&gt;
&lt;p&gt;Happy building and debugging!&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#prerequisites" id="toc-entry-1"&gt;Prerequisites&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#cloning-the-repositories" id="toc-entry-2"&gt;Cloning the Repositories&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#building-dry-with-debugging-enabled" id="toc-entry-3"&gt;Building Dry with Debugging Enabled&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#building-park-with-debugging-enabled" id="toc-entry-4"&gt;Building Park with Debugging Enabled&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#running-park" id="toc-entry-5"&gt;Running Park&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#analyzing-core-dumps-on-fedora" id="toc-entry-6"&gt;Analyzing Core Dumps on Fedora&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Programming"/><category term="Game Development"/><category term="DevOps"/></entry><entry><title>Ubuntu 22.04 Letsencrypt Docker Hints</title><link href="https://russell.ballestrini.net/ubuntu-22-04-letsencrypt-docker-hints/" rel="alternate"/><published>2022-05-20T20:10:00-04:00</published><updated>2022-05-20T20:10:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2022-05-20:/ubuntu-22-04-letsencrypt-docker-hints/</id><summary type="html">&lt;p&gt;letsencrypt certbot is now installable via snap (the deb apt repository is no longer maintained).&lt;/p&gt;
&lt;p&gt;alternatively you can use certbot via docker if you plan to use the &lt;tt class="docutils literal"&gt;certonly&lt;/tt&gt; mode.&lt;/p&gt;
&lt;p&gt;I did run into some issues &amp;amp; I will document my workarounds here:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nv"&gt;domains&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;example.com
&lt;span class="w"&gt;    &lt;/span&gt;shop.example.com
&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;domain …&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;letsencrypt certbot is now installable via snap (the deb apt repository is no longer maintained).&lt;/p&gt;
&lt;p&gt;alternatively you can use certbot via docker if you plan to use the &lt;tt class="docutils literal"&gt;certonly&lt;/tt&gt; mode.&lt;/p&gt;
&lt;p&gt;I did run into some issues &amp;amp; I will document my workarounds here:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nv"&gt;domains&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;example.com
&lt;span class="w"&gt;    &lt;/span&gt;shop.example.com
&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;domain&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;[*]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;certifying: &lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;.&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;domain_parts&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;domain_parts_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;domain_parts&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$domain_parts_length&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-eq&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# https://eff-certbot.readthedocs.io/en/stable/install.html#running-with-docker&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-it&lt;span class="w"&gt; &lt;/span&gt;--rm&lt;span class="w"&gt; &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;certbot&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/etc/letsencrypt:/etc/letsencrypt&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/lib/letsencrypt:/var/lib/letsencrypt&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/log/letsencrypt:/var/log/letsencrypt&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/www:/www&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;certbot/certbot&lt;span class="w"&gt; &lt;/span&gt;certonly&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;--renew-by-default&lt;span class="w"&gt; &lt;/span&gt;--webroot&lt;span class="w"&gt; &lt;/span&gt;-w&lt;span class="w"&gt; &lt;/span&gt;/www&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;www.&lt;span class="nv"&gt;$domain&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# https://eff-certbot.readthedocs.io/en/stable/install.html#running-with-docker&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-it&lt;span class="w"&gt; &lt;/span&gt;--rm&lt;span class="w"&gt; &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;certbot&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/etc/letsencrypt:/etc/letsencrypt&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/lib/letsencrypt:/var/lib/letsencrypt&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/var/log/letsencrypt:/var/log/letsencrypt&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/www:/www&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;certbot/certbot&lt;span class="w"&gt; &lt;/span&gt;certonly&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;--renew-by-default&lt;span class="w"&gt; &lt;/span&gt;--webroot&lt;span class="w"&gt; &lt;/span&gt;-w&lt;span class="w"&gt; &lt;/span&gt;/www&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# copy certificate links to a known file path and extention.&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;cp&lt;span class="w"&gt; &lt;/span&gt;/etc/letsencrypt/live/&lt;span class="nv"&gt;$domain&lt;/span&gt;/fullchain.pem&lt;span class="w"&gt; &lt;/span&gt;/etc/letsencrypt/live/&lt;span class="nv"&gt;$domain&lt;/span&gt;/crt.crt
&lt;span class="w"&gt;    &lt;/span&gt;cp&lt;span class="w"&gt; &lt;/span&gt;/etc/letsencrypt/live/&lt;span class="nv"&gt;$domain&lt;/span&gt;/privkey.pem&lt;span class="w"&gt; &lt;/span&gt;/etc/letsencrypt/live/&lt;span class="nv"&gt;$domain&lt;/span&gt;/key.key
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c1"&gt;# use minionfs to stage all certificates onto the salt master.&lt;/span&gt;
salt-call&lt;span class="w"&gt; &lt;/span&gt;cp.push_dir&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/etc/letsencrypt/live/&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;glob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;*.crt&amp;#39;&lt;/span&gt;
salt-call&lt;span class="w"&gt; &lt;/span&gt;cp.push_dir&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/etc/letsencrypt/live/&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;glob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;*.key
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So the key is the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-v&lt;/span&gt;&lt;/tt&gt; mounts, I needed one for my &lt;tt class="docutils literal"&gt;webroot&lt;/tt&gt; of &lt;tt class="docutils literal"&gt;/www&lt;/tt&gt; &amp;amp; one for logs.&lt;/p&gt;
&lt;p&gt;This is different or not assumed in the official guide notes.&lt;/p&gt;
&lt;p&gt;Additionally on Ubuntu 22.04 LTS in Linode, I was not any to use the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-v&lt;/span&gt;&lt;/tt&gt; mounts until I ran these commands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;su&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;root

mkdir&lt;span class="w"&gt; &lt;/span&gt;/sys/fs/cgroup/systemd
mount&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;cgroup&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;none,name&lt;span class="o"&gt;=&lt;/span&gt;systemd&lt;span class="w"&gt; &lt;/span&gt;cgroup&lt;span class="w"&gt; &lt;/span&gt;/sys/fs/cgroup/systemd
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The mount command never persists across reboots so you will want the following &lt;tt class="docutils literal"&gt;/etc/fstab&lt;/tt&gt; entry:&lt;/p&gt;
&lt;p&gt;Brian Amedro ended up modifying grub to avoid loading the certain systemd subsystem which were found to cause us the trouble:&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://github.com/docker/for-linux/issues/219#issuecomment-817318014"&gt;https://github.com/docker/for-linux/issues/219#issuecomment-817318014&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For now i will place the &lt;tt class="docutils literal"&gt;mkdir&lt;/tt&gt; &amp;amp; &lt;tt class="docutils literal"&gt;mount&lt;/tt&gt; commands into the renew script since the fstab solution doesn't work&lt;/p&gt;
&lt;p&gt;because the directory doesn't exist, gets deleted on each reboot so it isn't present during boot mount time.&lt;/p&gt;
&lt;p&gt;/etc/fstab&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;cgroup&lt;span class="w"&gt;    &lt;/span&gt;/sys/fs/cgroup/systemd&lt;span class="w"&gt;    &lt;/span&gt;cgroup&lt;span class="w"&gt;    &lt;/span&gt;defaults
&lt;/pre&gt;&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Salt"/><category term="Docker"/></entry><entry><title>Uncle Drops off server with note asking u to cl0ne ubuntu22 kvm</title><link href="https://russell.ballestrini.net/uncle-drops-off-server-with-note-asking-u-to-cl0ne-ubuntu22-kvm/" rel="alternate"/><published>2022-05-09T13:14:00-04:00</published><updated>2022-05-09T13:14:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2022-05-09:/uncle-drops-off-server-with-note-asking-u-to-cl0ne-ubuntu22-kvm/</id><summary type="html">&lt;p&gt;Imagine your uncle just dropped off a TrueNAS Core server with root credentials, &amp;amp; a preconfigured IP Address in the subnet space of 192.168.1.1/24.&lt;/p&gt;
&lt;p&gt;The note also highly suggests you to create your desired arch with
KVM by making a cl0ne of ubuntu22, into separate VMs for …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Imagine your uncle just dropped off a TrueNAS Core server with root credentials, &amp;amp; a preconfigured IP Address in the subnet space of 192.168.1.1/24.&lt;/p&gt;
&lt;p&gt;The note also highly suggests you to create your desired arch with
KVM by making a cl0ne of ubuntu22, into separate VMs for different purposes.&lt;/p&gt;
&lt;p&gt;Kind of open ended when to end a note, but whatever, you have nothing else to
do this __________ so you crack open the card board box &amp;amp; pull out an enormous 2u blade server! (this is like an FF8 Gunsword cutscene moment). We say enourmous not because it's any different in size than any other 2u, but this thing is extra heavy, I mean real heavy, 4 mechanical drives but fits up to 12!&lt;/p&gt;
&lt;p&gt;The 4 hard drives, configured in ZFS mirrors, labeled &amp;quot;downloads&amp;quot; &amp;amp; &amp;quot;personal&amp;quot;.&lt;/p&gt;
&lt;p&gt;upon booting the The TrueNAS Core server graphical user interface is connected over HTTPS as a web application, available from the IP address on the sticky notes!&lt;/p&gt;
&lt;p&gt;The UI seems to show the baremetal system has 128G of memory, most is free!
Additionally it has 2 x Intel(R) Xeon(R) CPU E5-2670 0 &amp;#64; 2.60GHz with a 1% Avg Usage on 32 threads!&lt;/p&gt;
&lt;p&gt;You clone a KVM guest from ubuntu22 &amp;amp; name it  ______________________________.&lt;/p&gt;
&lt;p&gt;Now you will need to configure the new cloned guest via VNC in order to change the following list of configurations. The ubuntu22 base image is the vanilla defaults of Ubuntu 22.04 LTS server without any Snap (uncle likes flatpak if anything) so this means the server is &amp;quot;headless&amp;quot; there isn't a graphical user interface.&lt;/p&gt;
&lt;p&gt;You'll need to configure:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;hostname &lt;tt class="docutils literal"&gt;vim /etc/hostname&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;cammy.foxhop.net
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;network  &amp;amp; dns &lt;tt class="docutils literal"&gt;vim &lt;span class="pre"&gt;/etc/netplan/00-installer-config.yaml&lt;/span&gt;&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# run this command after making changes!&lt;/span&gt;
&lt;span class="c1"&gt;# sudo netplan --debug apply&lt;/span&gt;
&lt;span class="nt"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;ethernets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;enp0s4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;addresses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;192.168.1.64/24&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;default&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;via&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;192.168.1.1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;nameservers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;addresses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;### CloudFlare.&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;#- 1.1.1.1&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;### Google DNS.&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;#- 8.8.8.8&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;#- 8.8.4.4&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;### OpenDNS.&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;#- 208.67.220.220&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;#- 208.67.222.222&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;208.67.222.220&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;### Quad9.&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;9.9.9.9&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;foxhop.net&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;2&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;You remembered to try to document steps needed for next time a person needs to clone the image we will have a checklist to follow so no steps get missed!&lt;/p&gt;
&lt;p&gt;What is that checklist for you?&lt;/p&gt;
&lt;p&gt;Do you bootstrap configuration management next? Or maybe some in house remote execution? Do you outsource your admin hacker tooling?&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;p&gt;Ok, so the next day you wake up &amp;amp; decide to try out SaltStack configuration management, so that means
you will want to clone ubuntu22 kvm into a guest named &lt;tt class="docutils literal"&gt;master&lt;/tt&gt; (short for salt-master, &lt;tt class="docutils literal"&gt;salt&lt;/tt&gt; consistently crashes [me shrugs]).&lt;/p&gt;
&lt;p&gt;Anyways, you do the tricks above to configure netplan (networking &amp;amp; DNS) &amp;amp; then go about installing &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;salt-master&lt;/span&gt;&lt;/tt&gt; service. The &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-M&lt;/span&gt;&lt;/tt&gt; flag signals to install both &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;salt-minion&lt;/span&gt;&lt;/tt&gt; &amp;amp; &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;salt-master&lt;/span&gt;&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;wget&lt;span class="w"&gt; &lt;/span&gt;-O&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;https://bootstrap.saltstack.com&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;sh&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;stable&lt;span class="w"&gt; &lt;/span&gt;-M&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Of course this doesn't always work if for example you are on a very new Ubuntu LTS (which you are) no fear,
you remembered uncle documented a similar snag in a &lt;a class="reference external" href="https://github.com/saltstack/salt-bootstrap/issues/1821#issuecomment-1113868737"&gt;github comment&lt;/a&gt; that said you adapted to also install the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;salt-master&lt;/span&gt;&lt;/tt&gt; daemon, since this is the salt master vm.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/saltstack/salt-bootstrap.git
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;salt-bootstrap
bash&lt;span class="w"&gt; &lt;/span&gt;salt-bootstrap.sh&lt;span class="w"&gt; &lt;/span&gt;-M
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;By now you are watching the growing list of hosts using ubuntu22's SSH host key...&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;[ ] &lt;a class="reference external" href="https://blog.g3rt.nl/regenerate-ssh-host-keys.html"&gt;https://blog.g3rt.nl/regenerate-ssh-host-keys.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You do this to fix salt host &amp;amp; start to consider ways to deal with this in the future, should likely be one of the first steps or maybe we could run this on ubuntu22? You decide to write this down as a (rabbit)(hole) for another day.&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;p&gt;Dual booting Ubuntu 22.04 LTS?&lt;/p&gt;
&lt;p&gt;Did you know grub 2 OS Prober was recently disabled by default?&lt;/p&gt;
&lt;p&gt;If plan to dual boot with windows or any other OS, try these settings:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;[x] &lt;a class="reference external" href="https://www.solaris-cookbook.eu/linux/linux-ubuntu/ubuntu-22-04-fix-grub-dual-boot-with-windows/"&gt;https://www.solaris-cookbook.eu/linux/linux-ubuntu/ubuntu-22-04-fix-grub-dual-boot-with-windows/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;vi&lt;span class="w"&gt; &lt;/span&gt;/etc/default/grub

&lt;span class="nv"&gt;GRUB_CMDLINE_LINUX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="nv"&gt;GRUB_DISABLE_OS_PROBER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;applied but it didn't seem to work...&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;p&gt;While doing this work you remember you needed to back up your gpg keys (and verify the restore process!):&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;[] &lt;a class="reference external" href="https://www.jwillikers.com/backup-and-restore-a-gpg-key"&gt;https://www.jwillikers.com/backup-and-restore-a-gpg-key&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;amp; also make sure you can access pass from multiple machines, using git!&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;[] &lt;a class="reference external" href="https://www.passwordstore.org/"&gt;https://www.passwordstore.org/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;and get multiple machines pushing &amp;amp; pulling from the same remote origin this way it is safer to loose a node on the &amp;quot;cluster&amp;quot;,&lt;/p&gt;
&lt;p&gt;for full transparency, this is what I did to look around &amp;amp; export / import&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;fox@blanka&lt;span class="w"&gt; &lt;/span&gt;~&lt;span class="o"&gt;]&lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;gpg&lt;span class="w"&gt; &lt;/span&gt;--list-secret-keys&lt;span class="w"&gt; &lt;/span&gt;--keyid-format&lt;span class="w"&gt; &lt;/span&gt;LONG

/home/fox/.gnupg/pubring.gpg
----------------------------
sec&lt;span class="w"&gt;   &lt;/span&gt;rsa2048/23CDA6102BFB3D7D&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2014&lt;/span&gt;-06-23&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;SC&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;4ABE744F1FDAF12C78E2E5D923CDA6102BFB3D7D
uid&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;ultimate&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Russell&lt;span class="w"&gt; &lt;/span&gt;Ballestrini&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;Personal&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;russell@ballestrini.net&amp;gt;
ssb&lt;span class="w"&gt;   &lt;/span&gt;rsa2048/1E6F09A2613E8133&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2014&lt;/span&gt;-06-23&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;E&lt;span class="o"&gt;]&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;fox@blanka&lt;span class="w"&gt; &lt;/span&gt;~&lt;span class="o"&gt;]&lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;gpg&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;russell-at-ballestrini-dot-net.gpg&lt;span class="w"&gt; &lt;/span&gt;--export-options&lt;span class="w"&gt; &lt;/span&gt;backup&lt;span class="w"&gt; &lt;/span&gt;--export-secret-keys&lt;span class="w"&gt; &lt;/span&gt;russell@ballestrini.net

&lt;span class="o"&gt;[&lt;/span&gt;fox@blanka&lt;span class="w"&gt; &lt;/span&gt;~&lt;span class="o"&gt;]&lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;ll&lt;span class="w"&gt; &lt;/span&gt;russell-at-ballestrini-dot-net.gpg
-rw-------.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2645&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;May&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;11&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:59&lt;span class="w"&gt; &lt;/span&gt;russell-at-ballestrini-dot-net.gpg

&lt;span class="o"&gt;[&lt;/span&gt;fox@blanka&lt;span class="w"&gt; &lt;/span&gt;~&lt;span class="o"&gt;]&lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;file&lt;span class="w"&gt; &lt;/span&gt;russell-at-ballestrini-dot-net.gpg
russell-at-ballestrini-dot-net.gpg:&lt;span class="w"&gt; &lt;/span&gt;OpenPGP&lt;span class="w"&gt; &lt;/span&gt;Secret&lt;span class="w"&gt; &lt;/span&gt;Key&lt;span class="w"&gt; &lt;/span&gt;Version&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;Created&lt;span class="w"&gt; &lt;/span&gt;Mon&lt;span class="w"&gt; &lt;/span&gt;Jun&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;23&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;01&lt;/span&gt;:44:34&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2014&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;RSA&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;Encrypt&lt;span class="w"&gt; &lt;/span&gt;or&lt;span class="w"&gt; &lt;/span&gt;Sign,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2048&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bits&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;fox@blanka&lt;span class="w"&gt; &lt;/span&gt;~&lt;span class="o"&gt;]&lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;scp&lt;span class="w"&gt; &lt;/span&gt;russell-at-ballestrini-dot-net.gpg&lt;span class="w"&gt; &lt;/span&gt;fox@akuma.foxhop.net:/home/fox/russell-at-ballestrini-dot-net.gpg
russell-at-ballestrini-dot-net.gpg
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;you think this will allow you to copy the private key file around to various computers via SSH and import the key using this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;gpg&lt;span class="w"&gt; &lt;/span&gt;--import-options&lt;span class="w"&gt; &lt;/span&gt;restore&lt;span class="w"&gt; &lt;/span&gt;--import&lt;span class="w"&gt; &lt;/span&gt;russell-at-ballestrini-dot-net.gpg
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;you admit whenever working with pgp or gpg makes you feel squeemish, but a key is a key right? ...&lt;/p&gt;
&lt;p&gt;should not treat any differently than a private SSH key.&lt;/p&gt;
&lt;p&gt;You will however want to trust this key completely with your whole heart &amp;amp; soul &amp;amp; sole so you modify the newly imported key to trust it completely and ultimately which just so happens to be a 5 in this tool's universe!&lt;/p&gt;
&lt;p&gt;so you follow these prompts:&lt;/p&gt;
&lt;p&gt;gpg --edit-key  &lt;a class="reference external" href="mailto:russell&amp;#64;ballestrini.net"&gt;russell&amp;#64;ballestrini.net&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;gpg&amp;gt; trust&lt;/p&gt;
&lt;p&gt;and choose 5!&lt;/p&gt;
&lt;p&gt;gpg&amp;gt; quit&lt;/p&gt;
&lt;p&gt;Then finally you restored your repo to a fresh new computer by using:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;fox@play:~$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;ssh://fox@akuma.foxhop.net:/home/fox/git/pass.git&lt;span class="w"&gt; &lt;/span&gt;.password-store
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;with the private gpg key in place &amp;amp; the repo following the shared remote, we now have our password store sharded across workstation nodes!&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;p&gt;on ubuntu desktop network manager hijacks /etc/netplan but also makes it impossible to configure the search DNS settings from the GUI, you track down a blog post which helps you!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;nmcli&lt;span class="w"&gt; &lt;/span&gt;con&lt;span class="w"&gt; &lt;/span&gt;mod&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Wired connection 1&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ipv4.dns-search&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;foxhop.net&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;your only hope is this setting persists across reboots.&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Salt"/></entry><entry><title>Vertically Scaling GitLab Server For As Cheap As Possible</title><link href="https://russell.ballestrini.net/vertically-scaling-gitlab-server-for-as-cheap-as-possible/" rel="alternate"/><published>2022-01-05T19:13:00-05:00</published><updated>2022-01-05T19:13:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2022-01-05:/vertically-scaling-gitlab-server-for-as-cheap-as-possible/</id><summary type="html">&lt;p&gt;Today's essay acts as a power-up love story for the underdog.&lt;/p&gt;
&lt;p&gt;A living document &amp;amp; quickstart for:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;bootstrappers&lt;/li&gt;
&lt;li&gt;small business under 99 employees&lt;/li&gt;
&lt;li&gt;solo ops or devs&lt;/li&gt;
&lt;li&gt;entrepreneurs&lt;/li&gt;
&lt;li&gt;hackers&lt;/li&gt;
&lt;li&gt;tinkerers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You also deserve a quick start to a GitLab Server power house!&lt;/p&gt;
&lt;p&gt;Regardless of my intended audience, this strategy should scale …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Today's essay acts as a power-up love story for the underdog.&lt;/p&gt;
&lt;p&gt;A living document &amp;amp; quickstart for:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;bootstrappers&lt;/li&gt;
&lt;li&gt;small business under 99 employees&lt;/li&gt;
&lt;li&gt;solo ops or devs&lt;/li&gt;
&lt;li&gt;entrepreneurs&lt;/li&gt;
&lt;li&gt;hackers&lt;/li&gt;
&lt;li&gt;tinkerers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You also deserve a quick start to a GitLab Server power house!&lt;/p&gt;
&lt;p&gt;Regardless of my intended audience, this strategy should scale to
500-1,000 concurrent engineers on the smallest of the instance classes
which satisfy the current GitLab Server &amp;quot;minimum requirements&amp;quot;.&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://docs.gitlab.com/ee/install/requirements.html"&gt;https://docs.gitlab.com/ee/install/requirements.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="section" id="why-gitlab-server"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Why GitLab Server?&lt;/a&gt;&lt;/h2&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;GitLab is open source (not black box)&lt;/li&gt;
&lt;li&gt;GitLab CI is a game changer&lt;/li&gt;
&lt;li&gt;GitLab CI is extendable to any workflow&lt;/li&gt;
&lt;li&gt;GitLab Server wants 4G of memory&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="section" id="expected-results"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Expected Results&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This guide prescribes the following setup:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;A Physical or Virtual Machine with at least 4G of RAM&lt;/li&gt;
&lt;li&gt;Docker installed&lt;/li&gt;
&lt;li&gt;Docker container started via docker-compose to manage
a monolithic Omnibus installation of GitLab Server&lt;/li&gt;
&lt;li&gt;A Cronjob to backup GitLab Server to &lt;tt class="docutils literal"&gt;/tmp&lt;/tt&gt;&lt;/li&gt;
&lt;li&gt;A Cronjob to collect Gitlab Server backup tarball &amp;amp; &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;/etc/gitlab/gitlab-secrets.json&lt;/span&gt;&lt;/tt&gt;&lt;/li&gt;
&lt;li&gt;A strong recommendation to test the complete backup and recovery process
at least once to prepare for when times get rough.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="section" id="gitlab-server-omnibus-in-docker"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;GitLab Server Omnibus In Docker&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The installation &amp;amp; configuration is documented using SaltStack to make it easy
to spin up GitLab Servers.&lt;/p&gt;
&lt;p&gt;To install SaltStack on the new host dedicated for GitLab Server, Run the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;wget&lt;span class="w"&gt; &lt;/span&gt;-O&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;https://bootstrap.saltstack.com&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;sh&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;stable&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;master: salt.example.com&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/etc/salt/minion.d/custom.conf&lt;span class="p"&gt;;&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;service&lt;span class="w"&gt; &lt;/span&gt;salt-minion&lt;span class="w"&gt; &lt;/span&gt;restart
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;On the instance designated as salt-master, accept the new salt-minion key:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;salt-key&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;git2.unturf.com
The&lt;span class="w"&gt; &lt;/span&gt;following&lt;span class="w"&gt; &lt;/span&gt;keys&lt;span class="w"&gt; &lt;/span&gt;are&lt;span class="w"&gt; &lt;/span&gt;going&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;be&lt;span class="w"&gt; &lt;/span&gt;accepted:
Unaccepted&lt;span class="w"&gt; &lt;/span&gt;Keys:
Git2.unturf.com
Proceed?&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;n/Y&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;y
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;and make sure communication is established:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;salt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;git2*&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;test.ping
git2.unturf.com:
&lt;span class="w"&gt;    &lt;/span&gt;True
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;and run a highstate to configure that new host instance:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;salt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;git2*&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;state.highstate
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Once the command returns, assuming we had no errors or failures we
have docker installed and an Omnibus Gitlab Server booted inside a container.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;Summary&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git2.unturf.com
--------------
Succeeded:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;104&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;changed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;69&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
Failed:&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
--------------
Total&lt;span class="w"&gt; &lt;/span&gt;states&lt;span class="w"&gt; &lt;/span&gt;run:&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="m"&gt;104&lt;/span&gt;
Total&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;time:&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;194&lt;/span&gt;.431&lt;span class="w"&gt; &lt;/span&gt;s
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The GitLab Server's internal services take over 3 minutes to come online.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="disaster-recovery"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Disaster Recovery&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Prepare for real disaster by practicing the entire backup and restore process.&lt;/p&gt;
&lt;p&gt;As extra homework, it is high advisable to spin up a 2nd host for GitLab Server
in order to test the backup and restore process from start to finish:&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://docs.gitlab.com/ee/raketasks/backup_restore.html#restore-gitlab"&gt;https://docs.gitlab.com/ee/raketasks/backup_restore.html#restore-gitlab&lt;/a&gt;&lt;/p&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;GitLab CI is a game changer.&lt;/dt&gt;
&lt;dd&gt;Gitlab is like Circle CI 2.0 only not black box.&lt;/dd&gt;
&lt;dt&gt;GitLab CI is extendable to any workflow&lt;/dt&gt;
&lt;dd&gt;but you should keep it simple, learn to use the tool as intended,
and rethink legacy workflows rather than port.&lt;/dd&gt;
&lt;/dl&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#why-gitlab-server" id="toc-entry-1"&gt;Why GitLab Server?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#expected-results" id="toc-entry-2"&gt;Expected Results&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#gitlab-server-omnibus-in-docker" id="toc-entry-3"&gt;GitLab Server Omnibus In Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#disaster-recovery" id="toc-entry-4"&gt;Disaster Recovery&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Salt"/><category term="Docker"/></entry><entry><title>Russell Open Sources Remarkbox &amp; MakePostSell into Public Domain!</title><link href="https://russell.ballestrini.net/russell-open-sources-remarkbox-and-make-post-sell-into-public-domain/" rel="alternate"/><published>2021-12-23T12:08:00-05:00</published><updated>2021-12-23T12:08:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2021-12-23:/russell-open-sources-remarkbox-and-make-post-sell-into-public-domain/</id><summary type="html">&lt;img alt="green pixel art style Christmas tree with animated blinking lights" class="align-right" src="/uploads/2018/pixel-art-yuletide-tree.gif" /&gt;
&lt;p&gt;Merry Christmas &amp;amp; Happy Holidays, Everyone!&lt;/p&gt;
&lt;p&gt;My wife prompts students with this fun holiday writing idea:&lt;/p&gt;
&lt;blockquote&gt;
&lt;strong&gt;&amp;quot;If you could gift the world anything, what would it be?&amp;quot;&lt;/strong&gt;&lt;/blockquote&gt;
&lt;p&gt;I love to read responses in the comments!&lt;/p&gt;
&lt;img alt="pixel art style seasonal wrapped gift" class="align-right" src="/uploads/2018/pixel-art-gift.png" /&gt;
&lt;p&gt;&lt;strong&gt;As for me, I have wonderful news, my dream gift to the world will come true …&lt;/strong&gt;&lt;/p&gt;</summary><content type="html">&lt;img alt="green pixel art style Christmas tree with animated blinking lights" class="align-right" src="/uploads/2018/pixel-art-yuletide-tree.gif" /&gt;
&lt;p&gt;Merry Christmas &amp;amp; Happy Holidays, Everyone!&lt;/p&gt;
&lt;p&gt;My wife prompts students with this fun holiday writing idea:&lt;/p&gt;
&lt;blockquote&gt;
&lt;strong&gt;&amp;quot;If you could gift the world anything, what would it be?&amp;quot;&lt;/strong&gt;&lt;/blockquote&gt;
&lt;p&gt;I love to read responses in the comments!&lt;/p&gt;
&lt;img alt="pixel art style seasonal wrapped gift" class="align-right" src="/uploads/2018/pixel-art-gift.png" /&gt;
&lt;p&gt;&lt;strong&gt;As for me, I have wonderful news, my dream gift to the world will come true this year!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For the holiday season, I AM gifting to the public domain my most essential code in hopes to trigger a &lt;strong&gt;positive trend of doing in 2022&lt;/strong&gt;!&lt;/p&gt;
&lt;blockquote&gt;
&lt;strong&gt;&amp;quot;in 2022, dream &amp;amp; do!&amp;quot;&lt;/strong&gt;&lt;/blockquote&gt;
&lt;p&gt;---&lt;/p&gt;
&lt;p&gt;Each project has a link to guide on how to access the source code.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;img alt="Remarkbox Logo Trademark" src="https://www.remarkbox.com/remarkbox-minified.png" style="width: 400px;" /&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://git.unturf.com/engineering/remarkbox/remarkbox"&gt;https://git.unturf.com/engineering/remarkbox/remarkbox&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;img alt="MakePostSell Logo Trademark" src="https://www.makepostsell.com/static/mps.png" style="width: 400px;" /&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://git.unturf.com/engineering/make-post-sell/make_post_sell"&gt;https://git.unturf.com/engineering/make-post-sell/make_post_sell&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;In 2021, I imagined many possible paths, yet all seemed to lead to one word:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OPEN&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I will continue to operate the SaaS under Remarkbox and MakePostSell trademarks &amp;amp; operations will fall under &lt;tt class="docutils literal"&gt;unturf.&lt;/tt&gt;&lt;/p&gt;
&lt;p&gt;I will take an additional role of &lt;tt class="docutils literal"&gt;BDFL&lt;/tt&gt; in regards to:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;onboarding volunteers to maintain our &lt;tt class="docutils literal"&gt;common code&lt;/tt&gt; in perpetuity&lt;/li&gt;
&lt;li&gt;tie breaking&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It feels vulnerable to share code without team support. Think from end &amp;amp; this feeling will pass.&lt;/p&gt;
&lt;img alt="animated gif of a pixel art style circular bomb with fuse burning down" src="/uploads/2018/pixel-art-bomb.gif" /&gt;
&lt;img alt="steal this image glider.png 1337 h4x0r h4ndb00k" src="/uploads/2019/glider.png" /&gt;
&lt;p&gt;&lt;strong&gt;Risk:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Alice may finally read the code &amp;amp; find vulnerabilities,
burning a hole between us crippling our communication, taking down the system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;More environments in wild, more eyes on problems, more options communicated.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Risk:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The masses may fork &amp;amp; divide the perfect circle.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We volunteer to grow &amp;amp; multiply our systems, not worry about forks.
We avoid focus on those who ignorantly mutilate or divide,
instead focus on projects and forks which sprout, multiply, &amp;amp; thrive!&lt;/p&gt;
&lt;p&gt;---&lt;/p&gt;
&lt;p&gt;At a high level the &lt;tt class="docutils literal"&gt;unturf.&lt;/tt&gt; stack currently consists of the following:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;HTML&lt;/li&gt;
&lt;li&gt;CSS&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.python.org/"&gt;Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.sqlalchemy.org/"&gt;SQLAlchemy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://sqlite.org/index.html"&gt;Sqlite3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://docs.pylonsproject.org/projects/pyramid/en/latest/index.html"&gt;Pyramid&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://uwsgi-docs.readthedocs.io/en/latest/"&gt;uWSGI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://nginx.org/en/"&gt;Nginx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="http://www.postfix.org/documentation.html"&gt;Postfix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://letsencrypt.org/"&gt;Let's Encrypt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html"&gt;Object Store (Boto3/S3 Compatible)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And of course we dogfood:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.remarkbox.com"&gt;Remarkbox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.makepostsell.com"&gt;MakePostSell&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For analytics we self host our own:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://plausible.io"&gt;Plausible&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If the documentation works, people should be able to use the services without understanding the fundementals of each keyword listed above.&lt;/p&gt;
&lt;p&gt;Pull requests welcome.&lt;/p&gt;
&lt;p&gt;---&lt;/p&gt;
&lt;p&gt;The writing of this essay has unfolded liberation in me and so,
I speak words in favor of Truth, Freedom, and Love.&lt;/p&gt;
&lt;p&gt;I AM now free to Grow, Explore, Document, and Multiply!&lt;/p&gt;
&lt;p&gt;I love you, have a great day reader!&lt;/p&gt;
&lt;img alt="pixel art style santa hat" src="/uploads/2018/pixel-art-santa-hat.png" /&gt;
</content><category term="misc"/><category term="Project"/><category term="Python"/><category term="AWS"/></entry><entry><title>GNUnet GNS Nameserver Operator Notes, Quickstart, &amp; Cheatsheet</title><link href="https://russell.ballestrini.net/gnunet-gns-nameserver-operator-notes-quickstart-cheatsheet/" rel="alternate"/><published>2021-12-03T18:21:00-05:00</published><updated>2021-12-03T18:21:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2021-12-03:/gnunet-gns-nameserver-operator-notes-quickstart-cheatsheet/</id><summary type="html">&lt;p&gt;We GROW with Truth, Freedom, Love.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&amp;quot;You have an ally&amp;quot;.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;We scroll through the official GNUnet handbook, everything is covered in details, sections have examples. Seems verbose at first glance but also familiar .&lt;/p&gt;
&lt;p&gt;Freedom of speech is under attack.&lt;/p&gt;
&lt;p&gt;I AM not scared, we understand the technology we have and …&lt;/p&gt;</summary><content type="html">&lt;p&gt;We GROW with Truth, Freedom, Love.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&amp;quot;You have an ally&amp;quot;.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;We scroll through the official GNUnet handbook, everything is covered in details, sections have examples. Seems verbose at first glance but also familiar .&lt;/p&gt;
&lt;p&gt;Freedom of speech is under attack.&lt;/p&gt;
&lt;p&gt;I AM not scared, we understand the technology we have and the technology we need. We will rescue ourselves and GROW together.&lt;/p&gt;
&lt;p&gt;GNS right NOW!&lt;/p&gt;
&lt;p&gt;Why?&lt;/p&gt;
&lt;p&gt;Our Collective's Internet Nameservice (DNS) is UNDER ACTIVE ATTACK by groups of governments.
Multitiple reports of DNS Name registrars breaking registrants Internet Domains on the behalf of NON-HUMAN ENTITIES.&lt;/p&gt;
&lt;p&gt;To avoid any erosion of our Freedom of speech, we will use GNS to make sure our web domains continue to map to our resources for people looking for us.&lt;/p&gt;
&lt;p&gt;I AM a seasoned 17 year Bind9 DNS administrator. I had my start on FreeBSD 5.0, virtualized over the years, now on Ubuntu VMs running on &amp;quot;the cloud&amp;quot; &amp;amp; local hypervisors hosting on broken laptops.&lt;/p&gt;
&lt;p&gt;We know how to scale DNS, it sort of just works once you figure out the config files.&lt;/p&gt;
&lt;p&gt;So let's figure out GNS and GROW a new Internet Culture, eh?&lt;/p&gt;
&lt;p&gt;---&lt;/p&gt;
&lt;p&gt;We pull down the source code using git:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://git.gnunet.org/gnunet.git
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We see a lot of C code, interesting choice.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;gnunet
&lt;span class="o"&gt;[&lt;/span&gt;fox@blanka&lt;span class="w"&gt; &lt;/span&gt;gnunet&lt;span class="o"&gt;]&lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;ls&lt;span class="w"&gt; &lt;/span&gt;-hal&lt;span class="w"&gt; &lt;/span&gt;src/dns/
total&lt;span class="w"&gt; &lt;/span&gt;144K
drwxrwxr-x.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;362&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Dec&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:09&lt;span class="w"&gt; &lt;/span&gt;.
drwxrwxr-x.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;834&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Dec&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:09&lt;span class="w"&gt; &lt;/span&gt;..
-rw-rw-r--.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;9&lt;/span&gt;.3K&lt;span class="w"&gt; &lt;/span&gt;Dec&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:09&lt;span class="w"&gt; &lt;/span&gt;dns_api.c
-rw-rw-r--.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;.3K&lt;span class="w"&gt; &lt;/span&gt;Dec&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:09&lt;span class="w"&gt; &lt;/span&gt;dns.conf.in
-rw-rw-r--.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;.2K&lt;span class="w"&gt; &lt;/span&gt;Dec&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:09&lt;span class="w"&gt; &lt;/span&gt;dns.h
-rw-rw-r--.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;126&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Dec&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:09&lt;span class="w"&gt; &lt;/span&gt;.gitignore
-rw-rw-r--.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt;  &lt;/span&gt;11K&lt;span class="w"&gt; &lt;/span&gt;Dec&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:09&lt;span class="w"&gt; &lt;/span&gt;gnunet-dns-monitor.c
-rw-rw-r--.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;.1K&lt;span class="w"&gt; &lt;/span&gt;Dec&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:09&lt;span class="w"&gt; &lt;/span&gt;gnunet-dns-redirector.c
-rw-rw-r--.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt;  &lt;/span&gt;32K&lt;span class="w"&gt; &lt;/span&gt;Dec&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:09&lt;span class="w"&gt; &lt;/span&gt;gnunet-helper-dns.c
-rw-rw-r--.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt;  &lt;/span&gt;35K&lt;span class="w"&gt; &lt;/span&gt;Dec&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:09&lt;span class="w"&gt; &lt;/span&gt;gnunet-service-dns.c
-rw-rw-r--.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt;  &lt;/span&gt;14K&lt;span class="w"&gt; &lt;/span&gt;Dec&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:09&lt;span class="w"&gt; &lt;/span&gt;gnunet-zonewalk.c
-rw-rw-r--.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;.2K&lt;span class="w"&gt; &lt;/span&gt;Dec&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:09&lt;span class="w"&gt; &lt;/span&gt;Makefile.am
-rw-rw-r--.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;.5K&lt;span class="w"&gt; &lt;/span&gt;Dec&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:09&lt;span class="w"&gt; &lt;/span&gt;plugin_block_dns.c
-rwxrwxr-x.&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;fox&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;.6K&lt;span class="w"&gt; &lt;/span&gt;Dec&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;:09&lt;span class="w"&gt; &lt;/span&gt;test_gnunet_dns.sh
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;GNUnet 0.15.0 released: &lt;a class="reference external" href="https://www.gnunet.org/en/news/2021-08-0.15.0.html"&gt;https://www.gnunet.org/en/news/2021-08-0.15.0.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;GNS First-come-first-served GNUnet top-level domain &amp;quot;.pin&amp;quot; zone key and website updated&lt;/p&gt;
&lt;p&gt;Register now: &lt;a class="reference external" href="https://fcfs.gnunet.org/"&gt;https://fcfs.gnunet.org/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Wow, Ok - We want to reserve our favorite names, right?&lt;/p&gt;
&lt;p&gt;How do we generate our &lt;tt class="docutils literal"&gt;zone key`&lt;/tt&gt;?&lt;/p&gt;
&lt;p&gt;A section in the GNUnet handbook which seems to cover zones and keys:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://docs.gnunet.org/handbook/gnunet.html#First-steps-_002d-Using-the-GNU-Name-System"&gt;https://docs.gnunet.org/handbook/gnunet.html#First-steps-_002d-Using-the-GNU-Name-System&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;To register a top-level domain of the &lt;tt class="docutils literal"&gt;.pin&lt;/tt&gt; we need to generate a public/private keypair for each zone.&lt;/p&gt;
&lt;p&gt;But before we do that we need to install GNUnet and configure the tool.&lt;/p&gt;
&lt;p&gt;Ok, I would prefer some up-to-date binaries than the source tree.&lt;/p&gt;
&lt;p&gt;Fedora package looks old and abandoned, same with Ubuntu, &amp;amp; Alpine...&lt;/p&gt;
&lt;p&gt;I AM dreaming of a lightweight Alpine docker container to pass around the nets with with GNUnet prebaked in. Maybe a project for another day.&lt;/p&gt;
&lt;p&gt;It seems like the NixOS package manager has an uptodate version 0.15.3!&lt;/p&gt;
&lt;p&gt;To install GNUnet binaries, we must first learn how to install &lt;tt class="docutils literal"&gt;nix&lt;/tt&gt; package manager.&lt;/p&gt;
&lt;p&gt;Installing nix package manager, run the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# reference: https://nixos.org/download.html&lt;/span&gt;
curl&lt;span class="w"&gt; &lt;/span&gt;-L&lt;span class="w"&gt; &lt;/span&gt;https://nixos.org/nix/install&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sh
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Once installed try to install GNUnet, like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;nix-env&lt;span class="w"&gt; &lt;/span&gt;-iA&lt;span class="w"&gt; &lt;/span&gt;nixpkgs.gnunet
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Errors complaining about certificates might be an easy fix:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/home/fox/.nix-profile/etc/profile.d/nix.sh
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For example the install script placed the following into my &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;~/.bash_profile&lt;/span&gt;&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;/home/fox/.nix-profile/etc/profile.d/nix.sh&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.&lt;span class="w"&gt; &lt;/span&gt;/home/fox/.nix-profile/etc/profile.d/nix.sh&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# added by Nix installer&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Once you get all that sorted you should have installed GNUnet!&lt;/p&gt;
&lt;p&gt;To verify try to interact with &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;~/.nix-profile/bin/gnunet-config&lt;/span&gt;&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;Battleship Operational, A Carrier Has Arrived.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;fox@blanka&lt;span class="w"&gt; &lt;/span&gt;russell.ballestrini.net&lt;span class="o"&gt;]&lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;gnunet-config&lt;span class="w"&gt; &lt;/span&gt;--version
gnunet-config&lt;span class="w"&gt; &lt;/span&gt;v0.15.3&lt;span class="w"&gt; &lt;/span&gt;release
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Alright now we must somehow configure zones and public/private keypairs with this tool.&lt;/p&gt;
&lt;p&gt;The handbook gives us this as an example, and we break down the anatomy of the command and outputs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;gnunet-config&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;gns&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;.myfriend&lt;span class="w"&gt; &lt;/span&gt;-V&lt;span class="w"&gt; &lt;/span&gt;PUBLIC_KEY
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Let's try passing &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;--help&lt;/span&gt;&lt;/tt&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;fox@blanka&lt;span class="w"&gt; &lt;/span&gt;russell.ballestrini.net&lt;span class="o"&gt;]&lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;gnunet-config&lt;span class="w"&gt; &lt;/span&gt;--help

gnunet-config&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;OPTIONS&lt;span class="o"&gt;]&lt;/span&gt;
Manipulate&lt;span class="w"&gt; &lt;/span&gt;GNUnet&lt;span class="w"&gt; &lt;/span&gt;configuration&lt;span class="w"&gt; &lt;/span&gt;files
Arguments&lt;span class="w"&gt; &lt;/span&gt;mandatory&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;long&lt;span class="w"&gt; &lt;/span&gt;options&lt;span class="w"&gt; &lt;/span&gt;are&lt;span class="w"&gt; &lt;/span&gt;also&lt;span class="w"&gt; &lt;/span&gt;mandatory&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;short&lt;span class="w"&gt; &lt;/span&gt;options.
&lt;span class="w"&gt;  &lt;/span&gt;-b,&lt;span class="w"&gt; &lt;/span&gt;--supported-backend&lt;span class="o"&gt;=&lt;/span&gt;BACKEND
&lt;span class="w"&gt;                             &lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;current&lt;span class="w"&gt; &lt;/span&gt;installation&lt;span class="w"&gt; &lt;/span&gt;supports&lt;span class="w"&gt; &lt;/span&gt;the
&lt;span class="w"&gt;                               &lt;/span&gt;specified&lt;span class="w"&gt; &lt;/span&gt;BACKEND
&lt;span class="w"&gt;  &lt;/span&gt;-c,&lt;span class="w"&gt; &lt;/span&gt;--config&lt;span class="o"&gt;=&lt;/span&gt;FILENAME&lt;span class="w"&gt;      &lt;/span&gt;use&lt;span class="w"&gt; &lt;/span&gt;configuration&lt;span class="w"&gt; &lt;/span&gt;file&lt;span class="w"&gt; &lt;/span&gt;FILENAME
&lt;span class="w"&gt;  &lt;/span&gt;-d,&lt;span class="w"&gt; &lt;/span&gt;--diagnostics&lt;span class="w"&gt;          &lt;/span&gt;output&lt;span class="w"&gt; &lt;/span&gt;extra&lt;span class="w"&gt; &lt;/span&gt;diagnostics
&lt;span class="w"&gt;  &lt;/span&gt;-F,&lt;span class="w"&gt; &lt;/span&gt;--full&lt;span class="w"&gt;                 &lt;/span&gt;write&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;full&lt;span class="w"&gt; &lt;/span&gt;configuration&lt;span class="w"&gt; &lt;/span&gt;file,&lt;span class="w"&gt; &lt;/span&gt;including
&lt;span class="w"&gt;                               &lt;/span&gt;default&lt;span class="w"&gt; &lt;/span&gt;values
&lt;span class="w"&gt;  &lt;/span&gt;-f,&lt;span class="w"&gt; &lt;/span&gt;--filename&lt;span class="w"&gt;             &lt;/span&gt;interpret&lt;span class="w"&gt; &lt;/span&gt;option&lt;span class="w"&gt; &lt;/span&gt;value&lt;span class="w"&gt; &lt;/span&gt;as&lt;span class="w"&gt; &lt;/span&gt;a&lt;span class="w"&gt; &lt;/span&gt;filename&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;with
&lt;span class="w"&gt;                               &lt;/span&gt;&lt;span class="nv"&gt;$-&lt;/span&gt;expansion&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-h,&lt;span class="w"&gt; &lt;/span&gt;--help&lt;span class="w"&gt;                 &lt;/span&gt;print&lt;span class="w"&gt; &lt;/span&gt;this&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;help&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-L,&lt;span class="w"&gt; &lt;/span&gt;--log&lt;span class="o"&gt;=&lt;/span&gt;LOGLEVEL&lt;span class="w"&gt;         &lt;/span&gt;configure&lt;span class="w"&gt; &lt;/span&gt;logging&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;use&lt;span class="w"&gt; &lt;/span&gt;LOGLEVEL
&lt;span class="w"&gt;  &lt;/span&gt;-l,&lt;span class="w"&gt; &lt;/span&gt;--logfile&lt;span class="o"&gt;=&lt;/span&gt;FILENAME&lt;span class="w"&gt;     &lt;/span&gt;configure&lt;span class="w"&gt; &lt;/span&gt;logging&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;write&lt;span class="w"&gt; &lt;/span&gt;logs&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;FILENAME
&lt;span class="w"&gt;  &lt;/span&gt;-o,&lt;span class="w"&gt; &lt;/span&gt;--option&lt;span class="o"&gt;=&lt;/span&gt;OPTION&lt;span class="w"&gt;        &lt;/span&gt;name&lt;span class="w"&gt; &lt;/span&gt;of&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;option&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;access
&lt;span class="w"&gt;  &lt;/span&gt;-r,&lt;span class="w"&gt; &lt;/span&gt;--rewrite&lt;span class="w"&gt;              &lt;/span&gt;rewrite&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;configuration&lt;span class="w"&gt; &lt;/span&gt;file,&lt;span class="w"&gt; &lt;/span&gt;even&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;nothing
&lt;span class="w"&gt;                               &lt;/span&gt;changed
&lt;span class="w"&gt;  &lt;/span&gt;-S,&lt;span class="w"&gt; &lt;/span&gt;--list-sections&lt;span class="w"&gt;        &lt;/span&gt;print&lt;span class="w"&gt; &lt;/span&gt;available&lt;span class="w"&gt; &lt;/span&gt;configuration&lt;span class="w"&gt; &lt;/span&gt;sections
&lt;span class="w"&gt;  &lt;/span&gt;-s,&lt;span class="w"&gt; &lt;/span&gt;--section&lt;span class="o"&gt;=&lt;/span&gt;SECTION&lt;span class="w"&gt;      &lt;/span&gt;name&lt;span class="w"&gt; &lt;/span&gt;of&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;section&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;access
&lt;span class="w"&gt;  &lt;/span&gt;-V,&lt;span class="w"&gt; &lt;/span&gt;--value&lt;span class="o"&gt;=&lt;/span&gt;VALUE&lt;span class="w"&gt;          &lt;/span&gt;value&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-v,&lt;span class="w"&gt; &lt;/span&gt;--version&lt;span class="w"&gt;              &lt;/span&gt;print&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;version&lt;span class="w"&gt; &lt;/span&gt;number
Report&lt;span class="w"&gt; &lt;/span&gt;bugs&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;gnunet-developers@gnu.org.
Home&lt;span class="w"&gt; &lt;/span&gt;page:&lt;span class="w"&gt; &lt;/span&gt;http://www.gnu.org/s/gnunet/
General&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;using&lt;span class="w"&gt; &lt;/span&gt;GNU&lt;span class="w"&gt; &lt;/span&gt;software:&lt;span class="w"&gt; &lt;/span&gt;http://www.gnu.org/gethelp/
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Ok so it seems like this command simply edits config files for GNUnet.&lt;/p&gt;
&lt;p&gt;Ok but where are these enigmatic config files?&lt;/p&gt;
&lt;p&gt;This is a living document we will continue the story toward a resolution and simple quickstart guide!&lt;/p&gt;
&lt;p&gt;Leave comments below!&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Docker"/></entry><entry><title>Copying files between cloud object stores like S3 GCP and Spaces using Boto3</title><link href="https://russell.ballestrini.net/copying-files-between-cloud-object-stores-like-s3-gcp-and-spaces-using-boto3/" rel="alternate"/><published>2021-05-12T18:50:00-04:00</published><updated>2021-05-12T18:50:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2021-05-12:/copying-files-between-cloud-object-stores-like-s3-gcp-and-spaces-using-boto3/</id><summary type="html">&lt;p&gt;tl;dr if you just want something like &lt;tt class="docutils literal"&gt;aws s3 cp&lt;/tt&gt; cli, try &lt;tt class="docutils literal"&gt;gsutil rsync&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;At work one key item of team's sprint is properly utilizing and securing Google Cloud Platform (GCP).&lt;/p&gt;
&lt;p&gt;For one of my projects, I'm learning GCP's Object Store called Google Cloud Storage (GCS).&lt;/p&gt;
&lt;p&gt;I have prior …&lt;/p&gt;</summary><content type="html">&lt;p&gt;tl;dr if you just want something like &lt;tt class="docutils literal"&gt;aws s3 cp&lt;/tt&gt; cli, try &lt;tt class="docutils literal"&gt;gsutil rsync&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;At work one key item of team's sprint is properly utilizing and securing Google Cloud Platform (GCP).&lt;/p&gt;
&lt;p&gt;For one of my projects, I'm learning GCP's Object Store called Google Cloud Storage (GCS).&lt;/p&gt;
&lt;p&gt;I have prior experience using AWS S3 and Digital Ocean Spaces via &lt;tt class="docutils literal"&gt;aws s3&lt;/tt&gt; CLI and &lt;tt class="docutils literal"&gt;boto3&lt;/tt&gt; and I've even blogged in the past about some pretty advanced topics, like &lt;a class="reference external" href="/pre-signed-get-and-post-for-digital-ocean-spaces/"&gt;Pre-signed GET and POST requests&lt;/a&gt;. Pre-signing allows the client to perform specific actions on specific private objects, which is great for short time-to-live link downloads or direct browser uploads into the object store, all without compromising data security (hampering link sharing and preventing replay attacks)&lt;/p&gt;
&lt;p&gt;At work, learning how to utilize and secure the GCS object store is critical to our teams interopability between clouds. We want to be able to store security artifacts into a single GCS bucket and sync all data to our main AWS S3 bucket as a long term archive.&lt;/p&gt;
&lt;div class="section" id="working-with-gcs-using-boto3"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Working with GCS using Boto3&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Given my prior experience with &lt;tt class="docutils literal"&gt;boto3&lt;/tt&gt; I decided to test interoperability between it's &lt;tt class="docutils literal"&gt;s3&lt;/tt&gt; client and GCS.&lt;/p&gt;
&lt;p&gt;For this test I created the following via the GCP Console:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://console.cloud.google.com/iam-admin/iam"&gt;IAM Service Account&lt;/a&gt; (with GCS Editor and GCS Viewer roles)&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://console.cloud.google.com/storage/browser"&gt;GCS Bucket&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;dl class="first docutils"&gt;
&lt;dt&gt;&lt;a class="reference external" href="https://console.cloud.google.com/storage/settings;tab=interoperabily"&gt;Service Account HMAC&lt;/a&gt; (linked to the new IAM Service Account)&lt;/dt&gt;
&lt;dd&gt;I also set the default GCP project which is important because by default boto3 is not configured to pass the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;x-amz-project-id&lt;/span&gt;&lt;/tt&gt; HTTP header.&lt;/dd&gt;
&lt;/dl&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At this point I have an access key and secret key (&lt;tt class="docutils literal"&gt;hmac&lt;/tt&gt;) which in theory I may pass to my existing code to communicate with GCP in a similar way to how I communicate with S3 or Spaces.&lt;/p&gt;
&lt;p&gt;Armed with this information I ran the following test and it worked:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Reference:&lt;/span&gt;
&lt;span class="c1"&gt;# https://cloud.google.com/storage/docs/migrating#storage-list-objects-s3-python&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;boto3&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_gcs_objects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;google_access_key_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;google_access_key_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucket_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Lists GCS objects using boto3 SDK&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="c1"&gt;# Create a new client and do the following:&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Change the endpoint URL to use the&lt;/span&gt;
    &lt;span class="c1"&gt;#    Google Cloud Storage XML API endpoint.&lt;/span&gt;
    &lt;span class="c1"&gt;# 2. Use Cloud Storage HMAC Credentials.&lt;/span&gt;

    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;s3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;auto&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;endpoint_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://storage.googleapis.com&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;aws_access_key_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;google_access_key_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;aws_secret_access_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;google_access_key_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Call GCS to list objects in bucket_name&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list_objects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Print object names&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Objects:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Contents&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Key&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you are working with many GCP projects, you'll want to figure out how to configure &lt;tt class="docutils literal"&gt;boto3&lt;/tt&gt; to pass the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;x-amz-project-id&lt;/span&gt;&lt;/tt&gt;. I found a good example reference but have not put it to practice: &lt;a class="reference external" href="https://github.com/boto/boto3/issues/2251"&gt;https://github.com/boto/boto3/issues/2251&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The TL;DR is AWS doesn't have a concept of projects or default projects so this header is something GCP introduced for interopability.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="gcp-iam-first-impressions"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;GCP IAM first impressions&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;From a surface level look GCP IAM appears less complicated than AWS IAM but also less granular. I'm not sure how I feel at this point but I'm excited to see what the rest of my team uncovers when it comes to permissions, roles, and best practices.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="working-with-gcs-using-a-cli"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Working with GCS using a CLI&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Now that we covered &lt;tt class="docutils literal"&gt;boto3&lt;/tt&gt; (Python) interoperability between object stores, I started looking at some other simple CLI workflows which typically utilize &lt;tt class="docutils literal"&gt;aws s3&lt;/tt&gt;. In the case of GCP the prefered CLI is &lt;tt class="docutils literal"&gt;gsutil&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;The subcommand &lt;a class="reference external" href="https://cloud.google.com/storage/docs/gsutil/commands/rsync"&gt;gsutil rsync&lt;/a&gt; in particular caught my eye as a simple way to setup a cross cloud object store synchronization!&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;gsutil&lt;span class="w"&gt; &lt;/span&gt;rsync&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;gs://my-gs-bucket&lt;span class="w"&gt; &lt;/span&gt;s3://my-s3-bucket
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For my next test, I'd like to try to setup a cronjob style automation to trigger &lt;tt class="docutils literal"&gt;gsutil rsync&lt;/tt&gt; to copy and sync data from GCP GCS into AWS S3 for our long term security and governance artifacts, likely using a Gitlab CI Pipeline on a schedule.&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#working-with-gcs-using-boto3" id="toc-entry-1"&gt;Working with GCS using Boto3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#gcp-iam-first-impressions" id="toc-entry-2"&gt;GCP IAM first impressions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#working-with-gcs-using-a-cli" id="toc-entry-3"&gt;Working with GCS using a CLI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/><category term="AWS"/></entry><entry><title>I tried to install GitLab on TrueNAS and failed</title><link href="https://russell.ballestrini.net/i-tried-to-install-gitlab-on-truenas-and-failed/" rel="alternate"/><published>2020-10-25T14:28:00-04:00</published><updated>2020-10-25T14:28:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2020-10-25:/i-tried-to-install-gitlab-on-truenas-and-failed/</id><summary type="html">&lt;p&gt;Ok, so a month ago I changed employment and the new company uses GitLab exclusively for centralized code version control system.&lt;/p&gt;
&lt;p&gt;This is my first time using GitLab and my first projects was integrating a &lt;tt class="docutils literal"&gt;dou/cloudmapper&lt;/tt&gt; with a GitLab runner on a schedule.&lt;/p&gt;
&lt;p&gt;After a couple weeks I've learned …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Ok, so a month ago I changed employment and the new company uses GitLab exclusively for centralized code version control system.&lt;/p&gt;
&lt;p&gt;This is my first time using GitLab and my first projects was integrating a &lt;tt class="docutils literal"&gt;dou/cloudmapper&lt;/tt&gt; with a GitLab runner on a schedule.&lt;/p&gt;
&lt;p&gt;After a couple weeks I've learned how GitLab runners work and wrote a wrapper to run cloudmapper concurrently so that each accounts is collected in parallel (the basic logic is running docker exec on a custom entrypoint and then backgrounds the task in a bash loop over each account configured in cloudmapper's config file)&lt;/p&gt;
&lt;p&gt;GitLab runners are awesome.&lt;/p&gt;
&lt;p&gt;I've used CircleCI for the better part of 3 years, having lifted the better part of 60 code bases from an internal app called conveyor to CircleCI 1.0 and performing CircleCI 2.0 conversions at Remind.&lt;/p&gt;
&lt;p&gt;CircleCI 2.0 is awesome...&lt;/p&gt;
&lt;p&gt;But GitLab runners are not black box which makes them so powerful.&lt;/p&gt;
&lt;p&gt;Granted not all organizations need or even want this much power, but I'm a nerd and I want it, so I decided I also want to run GitLab at home.&lt;/p&gt;
&lt;p&gt;My first thought was to try to just run GitLab on a VM on my &lt;tt class="docutils literal"&gt;SmartOS&lt;/tt&gt; host called &lt;tt class="docutils literal"&gt;mbison&lt;/tt&gt;, but that's a bit janky of a setup since it's an &lt;tt class="docutils literal"&gt;T420 ThinkPad&lt;/tt&gt; after all. Also I learned the recommended minimum memory footprint of &lt;tt class="docutils literal"&gt;GitLab&lt;/tt&gt; is &lt;tt class="docutils literal"&gt;4G&lt;/tt&gt; so a &lt;tt class="docutils literal"&gt;4 of 16G&lt;/tt&gt; allocation without even discussing a separate KVM for the runner. It feels weird that a single rails application would need _that_ much memory.&lt;/p&gt;
&lt;p&gt;Anyways, I have a FreeNAS &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;FreeNAS-11.3-U5&lt;/span&gt;&lt;/tt&gt; host at my house with &lt;tt class="docutils literal"&gt;46G&lt;/tt&gt; of memory and a bunch of idle cores, So I wondered if they have a plug-in for GitLab... I'm sure they do by now.&lt;/p&gt;
&lt;p&gt;After a bit of research I found that &lt;tt class="docutils literal"&gt;iXsystems&lt;/tt&gt; renamed the &lt;tt class="docutils literal"&gt;FreeNAS&lt;/tt&gt; codebase to &lt;tt class="docutils literal"&gt;TrueNAS&lt;/tt&gt; and version &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;TrueNAS-12.0-RELEASE&lt;/span&gt;&lt;/tt&gt; has a GitLab plugin! Sweet!&lt;/p&gt;
&lt;p&gt;Thankfully I remembered that back in 2019 I replaced my FreeNAS USB boot device with an SSD so it should be a safe operation to attempt a TrueNAS upgrade on a Saturday morning since I'd have the weekend to try to recover if things went sour. Fun side story, I ran into trouble last year trying to upgrade FreeNAS train because I didn't have room for the upgrade because I was using a USB drive as the boot disk and it had like 5 previous version on the 8G file system.&lt;/p&gt;
&lt;p&gt;The kind people in the freenode &lt;tt class="docutils literal"&gt;#FreeNAS&lt;/tt&gt; channel helped me switch back to an older version using SSH and told me I should not boot from a USB drive which was a thing I picked up from back in the FreeNAS 6 or 7 days. Boy were they correct, I moved the OS to an SSD and the server boots _WAY_ faster now, plus I have more capacity to try new versions.&lt;/p&gt;
&lt;p&gt;Anyways, here is a fun command to determine when a ZFS pool was created, I used this to get the exact date and time I moved from USB to SSD for this blog post:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;root@guile&lt;span class="o"&gt;[&lt;/span&gt;~&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="c1"&gt;# zpool history -i freenas-boot | grep &amp;quot;create pool&amp;quot;&lt;/span&gt;

&lt;span class="m"&gt;2019&lt;/span&gt;-11-12.13:00:30&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;txg:5&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;pool&lt;span class="w"&gt; &lt;/span&gt;version&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;28&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;software&lt;span class="w"&gt; &lt;/span&gt;version&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5000&lt;/span&gt;/5&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uts&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;11&lt;/span&gt;.2-STABLE&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1102500&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;amd64
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So weilding this confidence, I switched FreeNAS trains in the UI and clicked upgrade, following the prompts to create a backup of my NAS config.
After the server rebooted, FreeNAS was now TrueNAS. I logged into the web UI to look around and make sure my data was still there.&lt;/p&gt;
&lt;p&gt;Everything seemed fine, so I went to the plug-in tab and attempted to install GitLab. That failed with an error.&lt;/p&gt;
&lt;p&gt;So I decided oh well, I'll try spinning up an Ubuntu VM on the &lt;tt class="docutils literal"&gt;TrueNAS&lt;/tt&gt; box, apparently &lt;tt class="docutils literal"&gt;FreeBSD&lt;/tt&gt; uses &lt;tt class="docutils literal"&gt;bhyve&lt;/tt&gt; for that.&lt;/p&gt;
&lt;p&gt;Attempting to start the VM does nothing in the UI, it appears like it will start but then the page refreshes and the VM is stopped.&lt;/p&gt;
&lt;p&gt;I did eventually find the error message, by running&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;tail&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;/var/log/libvirt/*/*
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;/usr/sbin/bhyve&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;,sockets&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;,cores&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;,threads&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;-w&lt;span class="w"&gt; &lt;/span&gt;-H&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:0,hostbridge&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;bootrom,/usr/local/share/uefi-firmware/BHYVE_UEFI.fd&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;:0,ahci,cd:/mnt/downloads/iso/ubuntu-20.04.1-live-server-amd64.iso&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;:0,xhci,tablet&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;:0,ahci&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;:0,virtio-net,tap1,mac&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;00&lt;/span&gt;:a0:98:4f:98:13&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;:0,virtio-blk,/dev/zvol/downloads/gitlab-vcrg2z&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;31&lt;/span&gt;,lpc&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;com1,/dev/nmdm1A&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;29&lt;/span&gt;,fbuf,vncserver,tcp&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0.0.0:48009,w&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1024&lt;/span&gt;,h&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;768&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;1_gitlab
&lt;span class="m"&gt;25&lt;/span&gt;/10/2020&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;17&lt;/span&gt;:49:58&lt;span class="w"&gt; &lt;/span&gt;Listening&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;VNC&lt;span class="w"&gt; &lt;/span&gt;connections&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;TCP&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;48009&lt;/span&gt;
&lt;span class="m"&gt;25&lt;/span&gt;/10/2020&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;17&lt;/span&gt;:49:58&lt;span class="w"&gt; &lt;/span&gt;Listening&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;VNC&lt;span class="w"&gt; &lt;/span&gt;connections&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;TCP6&lt;span class="w"&gt; &lt;/span&gt;port&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;48009&lt;/span&gt;
ROM&lt;span class="w"&gt; &lt;/span&gt;boot&lt;span class="w"&gt; &lt;/span&gt;failed:&lt;span class="w"&gt; &lt;/span&gt;unrestricted&lt;span class="w"&gt; &lt;/span&gt;guest&lt;span class="w"&gt; &lt;/span&gt;capability&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;available
fbuf&lt;span class="w"&gt; &lt;/span&gt;frame&lt;span class="w"&gt; &lt;/span&gt;buffer&lt;span class="w"&gt; &lt;/span&gt;base:&lt;span class="w"&gt; &lt;/span&gt;0x941e00000&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;sz&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;16777216&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The important part is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;ROM boot failed: unrestricted guest capability not available
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It seems if you ask &lt;tt class="docutils literal"&gt;bhyve&lt;/tt&gt; to use the UEFI bootloader it will require UG support in the Intel CPU : (&lt;/p&gt;
&lt;p&gt;I attempted to switch to the grub bootloader but in that case I get the following error message:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;File &amp;quot;/usr/local/lib/python3.8/site-packages/middlewared/plugins/vm.py&amp;quot;, line 1414, in do_update
  raise verrors
middlewared.service_exception.ValidationErrors: [EINVAL] vm_update.devices.3.dtype: VNC only works with UEFI bootloader.
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;and the &lt;tt class="docutils literal"&gt;TrueNAS&lt;/tt&gt; UI does not let you disable VNC on VMs at this point...&lt;/p&gt;
&lt;p&gt;Another limitation of a missing UG CPU flag is &lt;tt class="docutils literal"&gt;bhyve&lt;/tt&gt; may only allocate 1 core, and 1 vcpu to a VM.&lt;/p&gt;
&lt;p&gt;Anyways I'm out of luck, I can't use the plugin which uses jails due to a strange error and no other hints as to the issue and &lt;tt class="docutils literal"&gt;byhve&lt;/tt&gt; / &lt;tt class="docutils literal"&gt;TrueNAS&lt;/tt&gt; not supporting VMs for my CPU...&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UPDATE&lt;/strong&gt; I recently upgraded my TrueNAS server hardware to an old &lt;tt class="docutils literal"&gt;Dell PowerEdge R720 R720xd&lt;/tt&gt; with two Intel(R) Xeon(R) CPU E5-2670 &amp;#64; 2.60GHz each with 8 Physical Cores and 128G of ECC Memory! Booyah!&lt;/p&gt;
&lt;p&gt;I was able to flash the included Dell H710 Mini RAID card into IT mode for ZFS support using the awesome guides and tools found here: &lt;a class="reference external" href="https://fohdeesha.com/docs/perc/"&gt;https://fohdeesha.com/docs/perc/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I now have GitLab running on a &lt;tt class="docutils literal"&gt;byhve&lt;/tt&gt; instance and also a dedicated runner instance. It's working great and really fun to have this extremely powerful gear in my home lab!&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Docker"/></entry><entry><title>WeeChat on-boot in a tmux session</title><link href="https://russell.ballestrini.net/weechat-on-boot-in-a-tmux-session/" rel="alternate"/><published>2020-09-15T13:32:00-04:00</published><updated>2020-09-15T13:32:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2020-09-15:/weechat-on-boot-in-a-tmux-session/</id><summary type="html">&lt;p&gt;Chronicles of a washed up systems administrator.&lt;/p&gt;
&lt;p&gt;In the basement, M. Bison (&lt;tt class="docutils literal"&gt;mbision.foxhop.net.&lt;/tt&gt;), a T430 with a cracked screen runs quietly with his lid closed, acting as a SmartOS hypervisor to 3 Solaris derived zones and 4 KVM ubuntu guests.&lt;/p&gt;
&lt;p&gt;One of these guests is the oldest of …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Chronicles of a washed up systems administrator.&lt;/p&gt;
&lt;p&gt;In the basement, M. Bison (&lt;tt class="docutils literal"&gt;mbision.foxhop.net.&lt;/tt&gt;), a T430 with a cracked screen runs quietly with his lid closed, acting as a SmartOS hypervisor to 3 Solaris derived zones and 4 KVM ubuntu guests.&lt;/p&gt;
&lt;p&gt;One of these guests is the oldest of the bunch and his name is Akuma (&lt;tt class="docutils literal"&gt;akuma.foxhop.net.&lt;/tt&gt;).&lt;/p&gt;
&lt;p&gt;Akuma and I go way back, over 13 years now. At one point Akuma was a physical machine, who ran FreeBSD and then later Ubuntu 6.04 with it's own guest kernel virtual machines.&lt;/p&gt;
&lt;p&gt;Akuma has been &amp;quot;home base&amp;quot; for as long as I remember. A place where shit got done. A place to perform operations. A place to &amp;quot;jump&amp;quot; from with SSH.&lt;/p&gt;
&lt;p&gt;It makes sense that Akuma would evolve and gain many roles over the years.&lt;/p&gt;
&lt;p&gt;Akuma performs DNS caching and forwarding for my LAN, it acts as the internal authoritative Nameserver for &lt;tt class="docutils literal"&gt;foxhop.net.&lt;/tt&gt;, and is the Salt Master for all my hosts both internally (hosted at my house) and externally in the &amp;quot;cloud&amp;quot;.&lt;/p&gt;
&lt;p&gt;Akuma uses &lt;tt class="docutils literal"&gt;tmux&lt;/tt&gt; with default settings to allow for a &amp;quot;remote shell&amp;quot; vibe.&lt;/p&gt;
&lt;p&gt;This allows me attach to my session running on Akuma from anywhere in the world.&lt;/p&gt;
&lt;p&gt;Only problem is, Akuma reboots weekly when M. Bison &lt;a class="reference external" href="/backup-all-virtual-machines-on-a-smartos-hypervisor-with-smart-back-sh/"&gt;triggers a complete backup of all guests&lt;/a&gt;.  Backups are stored on Guile, the FreeNAS server, uncompressed since disk space is cheap and this process allows for downtime, however I try to limit it.&lt;/p&gt;
&lt;p&gt;Since Akuma reboots each week, I needed a way to create tmux sessions on boot if I wanted to continue to use it as my &amp;quot;home base&amp;quot;, and I did so using SaltStack:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nt"&gt;create-tmux-session-on-boot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;cron.present&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;create&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tmux&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;boot&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/bin/bash /home/fox/new-tmux&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;create-tmux-session-on-boot&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;special&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;@reboot&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;fox&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;require&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;fox&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This salt state results in the following &lt;tt class="docutils literal"&gt;crontab &lt;span class="pre"&gt;-l&lt;/span&gt;&lt;/tt&gt; entry:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# create a new tmux session on system boot SALT_CRON_IDENTIFIER:create-tmux-session-on-boot&lt;/span&gt;
@reboot&lt;span class="w"&gt; &lt;/span&gt;/bin/bash&lt;span class="w"&gt; &lt;/span&gt;/home/fox/new-tmux
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And of course I what tutorial wouldn't be complete without a script! (&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;/home/fox/new-tmux&lt;/span&gt;&lt;/tt&gt;) :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c1"&gt;# The -A flag makes new-session behave like attach-session if session-name&lt;/span&gt;
&lt;span class="c1"&gt;# already exists; in this case, -D behaves like -d to attach-session.&lt;/span&gt;
tmux&lt;span class="w"&gt; &lt;/span&gt;new-session&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-A&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;remote-shell&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;weechat-curses&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# also create a couple windows to use as &amp;quot;remote shells&amp;quot;.&lt;/span&gt;
tmux&lt;span class="w"&gt; &lt;/span&gt;new-window
tmux&lt;span class="w"&gt; &lt;/span&gt;new-window
&lt;span class="c1"&gt;# finally attach to the new session!&lt;/span&gt;
tmux&lt;span class="w"&gt; &lt;/span&gt;a&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;remote-shell&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Thanks for joining me on this escape into the world of computers.&lt;/p&gt;
&lt;p&gt;This website is hosted on Ryu (&lt;tt class="docutils literal"&gt;ryu.foxhop.net&lt;/tt&gt;) another Ubuntu guest running on M. Bison, humming away downstairs on that T430 with a cracked screen, but thats a story for another time.&lt;/p&gt;
&lt;p&gt;Please feel free to comment below if you have suggestions for this post.&lt;/p&gt;
</content><category term="misc"/><category term="Automation"/><category term="Salt"/></entry><entry><title>Dreaming of unlimited home computer storage capacity</title><link href="https://russell.ballestrini.net/dreaming-of-unlimited-home-computer-storage-capacity/" rel="alternate"/><published>2019-12-15T09:52:00-05:00</published><updated>2019-12-15T09:52:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2019-12-15:/dreaming-of-unlimited-home-computer-storage-capacity/</id><summary type="html">&lt;p&gt;Unlimited computer storage capacity is a common science fiction trope and fun thought experiment.&lt;/p&gt;
&lt;p&gt;What would be possible if we could potentially store near infinite data at your house in a normal sized desktop computer?&lt;/p&gt;
&lt;p&gt;Even better using today's tech, what ideas could we dream up?&lt;/p&gt;
&lt;p&gt;This post will explore …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Unlimited computer storage capacity is a common science fiction trope and fun thought experiment.&lt;/p&gt;
&lt;p&gt;What would be possible if we could potentially store near infinite data at your house in a normal sized desktop computer?&lt;/p&gt;
&lt;p&gt;Even better using today's tech, what ideas could we dream up?&lt;/p&gt;
&lt;p&gt;This post will explore ideas as we surf the bleeding edge of home computing.&lt;/p&gt;
&lt;div class="section" id="idea-1"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Idea 1&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Live stream everything in your life in HD and store it locally at your house as a hyper real augment reality. A person could store and recall any event that he or she has witnessed using a headcam with wifi syncing capabilities.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;One of the many new features within the iPhone 6s is the ability for the 12MP camera on the back to record 4K video. It’s a big selling point for Apple, but what about storage space?&lt;/p&gt;
&lt;p&gt;According to a video published by MKBHD, just how much a one-minute long video shot in 4K will take up in your iPhone 6s’ storage has been discovered. According to the screenshot captured by Ryan Jones (&amp;#64;rjones) of the hands-on video, a one-minute video captured at 720p HD at 30fps will take up 60MB of space on the iPhone 6s. At 1080p HD and 30fps, it’s 130MB of space. A 1080p HD video at 60fps will take up 200MB of space. And, finally, the 4K video at 30fps will take up 375MB of space.&lt;/p&gt;
&lt;p&gt;reference: &lt;a class="reference external" href="http://www.iphonehacks.com/2015/09/iphone-6s-4k-video-space.html"&gt;http://www.iphonehacks.com/2015/09/iphone-6s-4k-video-space.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If we were streaming at 4k for 24 hours, it would be:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; 60 * 24 * 375
540000
&lt;/pre&gt;
&lt;p&gt;540,000MB or 540GB or .54TB per day of streaming everything.&lt;/p&gt;
&lt;p&gt;A years worth of 4k streaming from some guys headcam would consume:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; 365.25 * .54
197.235
&lt;/pre&gt;
&lt;p&gt;or 197TB worth of capacity.&lt;/p&gt;
&lt;p&gt;With today's technology and hardware available as home computing, including NAS systems, streaming 4k of a persons headcam would be possible... but not really practical without advancements in hard drive density.&lt;/p&gt;
&lt;p&gt;A 6TB drive is $100, and by my math, we would need 34 drives (not really even counting for redundancy):&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; 197.235 / 6
32.8725
&lt;/pre&gt;
&lt;p&gt;So the cost of a years worth a capacity would be:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
(197.235 / 6) * 100
3287.25
&lt;/pre&gt;
&lt;p&gt;or $3,287 for 34 x 6TB drives.&lt;/p&gt;
&lt;p&gt;It would fit in an oversized NAS, just barely... in another section I'll figure out what hard drive density is needed to make 4k video at 30fps more practical.&lt;/p&gt;
&lt;p&gt;Ok so, 4k streaming is currently not practical, so how about 1080p HD at 30fps? Could we do that with today's home tech?&lt;/p&gt;
&lt;p&gt;One minute of 1080p HD at 30fps is 130MB of space.&lt;/p&gt;
&lt;p&gt;If we were streaming at 1080p at 30fps for 24 hours, it would be:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; 60 * 24 * 130
187200
&lt;/pre&gt;
&lt;p&gt;187,200MB or 187GB or .187TB per day.&lt;/p&gt;
&lt;p&gt;A years worth of non-stop 1080P streaming at 30fps off some guys headcam would consume:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; 365.25 * .184
67.235
&lt;/pre&gt;
&lt;p&gt;Or about 68TB per year (rounded up).&lt;/p&gt;
&lt;p&gt;Like before a 6TB drive is $100, and by my math, we would need 12 drives (not really even counting for redundancy):&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; 67.206 / 6
11.201
&lt;/pre&gt;
&lt;p&gt;so the cost of a years worth a capacity would be:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; 12 * 100
1200
&lt;/pre&gt;
&lt;p&gt;or $1,200 for 12 x 6TB drives.&lt;/p&gt;
&lt;p&gt;12 drives should easily fit in a mid-range home NAS. That said, a mid-range home NAS holding 12 drives is a clunky machine for most home owners.&lt;/p&gt;
&lt;p&gt;It might be possible, but likely not practical without a support plan, in home repairs, and on-going maintenance.&lt;/p&gt;
&lt;p&gt;It would likely require having a home NAS specialist in every region to offer up white gloved support for anyone who would be willing to pay for it.&lt;/p&gt;
&lt;p&gt;How could we make this even easier with today's tech?&lt;/p&gt;
&lt;p&gt;We could lower the quality even more...&lt;/p&gt;
&lt;p&gt;Streaming 720P at 30fps you would need 36T per year or about 6 x 6TB drives for about $600 plus a NAS.&lt;/p&gt;
&lt;p&gt;So at this time, if seems practical and economical to stream 720P at 30fps every minute of the year.&lt;/p&gt;
&lt;p&gt;What would we need to build to make all this data useful?&lt;/p&gt;
&lt;p&gt;Well we would need a way to index all this footage, and likely edit out useless footage.&lt;/p&gt;
&lt;p&gt;We would need at least two headcams with wifi sync, so that we could have one charging while we use the other.&lt;/p&gt;
&lt;p&gt;We would need software to sync the footage to the NAS, like &lt;tt class="docutils literal"&gt;syncthing&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;Besides that, all we need is some software to host the videos, edit, and index them to be searchable.&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#idea-1" id="toc-entry-1"&gt;Idea 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Opinion"/><category term="DevOps"/></entry><entry><title>Unity Collab not working on Fedora because of an invalid CA certificate path</title><link href="https://russell.ballestrini.net/unity-collab-not-working-on-fedora-because-of-invalid-ca-certificate-path/" rel="alternate"/><published>2019-11-08T12:19:00-05:00</published><updated>2019-11-08T12:19:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2019-11-08:/unity-collab-not-working-on-fedora-because-of-invalid-ca-certificate-path/</id><summary type="html">&lt;p&gt;This issue blocked me for about two days and prevented me from collaborating with another engineer at &lt;a class="reference external" href="https://www.gumyum.com"&gt;gumyum co-operative video game studio&lt;/a&gt;.&lt;/p&gt;
&lt;img alt="An image showing that Unity Collab is not linked" class="align-center" src="/uploads/2019/unity-collab-fedora-ca-certificate-error-no-link.png" /&gt;
&lt;p&gt;The other engineer's environment was Windows and he was able to interact with collab and our shared project owned by our shared org. My environment was Fedora 30 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This issue blocked me for about two days and prevented me from collaborating with another engineer at &lt;a class="reference external" href="https://www.gumyum.com"&gt;gumyum co-operative video game studio&lt;/a&gt;.&lt;/p&gt;
&lt;img alt="An image showing that Unity Collab is not linked" class="align-center" src="/uploads/2019/unity-collab-fedora-ca-certificate-error-no-link.png" /&gt;
&lt;p&gt;The other engineer's environment was Windows and he was able to interact with collab and our shared project owned by our shared org. My environment was Fedora 30 and was not working with collab, however I was able to download the project files.&lt;/p&gt;
&lt;img alt="An image showing that Unity is looking for the CA Certificates in the wrong location" class="align-center" src="/uploads/2019/unity-collab-fedora-ca-certificate-error-curl.png" /&gt;
&lt;p&gt;The issue?&lt;/p&gt;
&lt;p&gt;Apparently Unity naively assumes that a Linux system's copy of the root CA certificates is located &lt;cite&gt;/etc/ssl/certs/ca-certificates.crt&lt;/cite&gt;. This is the default location on Debian and Ubuntu based systems but not Fedora or Redhat, which uses the &lt;cite&gt;/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem&lt;/cite&gt; file.&lt;/p&gt;
&lt;p&gt;The work around is simple, create a soft link between the paths:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo ln -s /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem /etc/ssl/certs/ca-certificates.crt
&lt;/pre&gt;
&lt;img alt="An image showing that Unity Collab is working again!" class="align-center" src="/uploads/2019/unity-collab-fedora-ca-certificate-error-fixed.png" /&gt;
&lt;p&gt;Please come back again soon to learn more about the games we are building over here at &lt;cite&gt;gumyum&lt;/cite&gt;!&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="Games"/><category term="Project"/></entry><entry><title>How to fallback to an older FreeNAS version on update failure</title><link href="https://russell.ballestrini.net/how-to-fallback-to-an-older-freenas-version-on-update-failure/" rel="alternate"/><published>2019-10-31T17:06:00-04:00</published><updated>2019-10-31T17:06:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2019-10-31:/how-to-fallback-to-an-older-freenas-version-on-update-failure/</id><summary type="html">&lt;p&gt;try to log in as root locally, I needed to press &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;ctrl-alt-f7&lt;/span&gt;&lt;/tt&gt; to get to a log in prompt.&lt;/p&gt;
&lt;p&gt;Once logged in as root, I used the &lt;tt class="docutils literal"&gt;beadm&lt;/tt&gt; command.&lt;/p&gt;
&lt;p&gt;For example, &lt;tt class="docutils literal"&gt;beadm list&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;beadm activate &lt;span class="pre"&gt;&amp;lt;old-version&amp;gt;&lt;/span&gt;&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;Finally &lt;tt class="docutils literal"&gt;reboot&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;I followed this process and then got my FreeNAS server to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;try to log in as root locally, I needed to press &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;ctrl-alt-f7&lt;/span&gt;&lt;/tt&gt; to get to a log in prompt.&lt;/p&gt;
&lt;p&gt;Once logged in as root, I used the &lt;tt class="docutils literal"&gt;beadm&lt;/tt&gt; command.&lt;/p&gt;
&lt;p&gt;For example, &lt;tt class="docutils literal"&gt;beadm list&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;beadm activate &lt;span class="pre"&gt;&amp;lt;old-version&amp;gt;&lt;/span&gt;&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;Finally &lt;tt class="docutils literal"&gt;reboot&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;I followed this process and then got my FreeNAS server to boot again.&lt;/p&gt;
&lt;p&gt;From the GUI I was able to create myself a config backup.&lt;/p&gt;
&lt;p&gt;I then burned a new FreeNAS CD and used that to install a fresh new install on my USB drive and then once that booted I used the GUI to restore my config.&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/></entry><entry><title>Installing Unity on Linux</title><link href="https://russell.ballestrini.net/installing-unity-on-linux/" rel="alternate"/><published>2019-09-25T18:55:00-04:00</published><updated>2019-09-25T18:55:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2019-09-25:/installing-unity-on-linux/</id><summary type="html">&lt;p&gt;Russell, why are you installing Unity? I thought you were an open source developer and &lt;a class="reference external" href="/yuletide-trains-and-homegrown-video-games/"&gt;homegrown video game engine builder&lt;/a&gt;?!&lt;/p&gt;
&lt;p&gt;&lt;img alt="gumyum logo" src="/uploads/2010/12/gumyumgameslogo.png" /&gt;&lt;/p&gt;
&lt;p&gt;The TL;DR we are starting &lt;a class="reference external" href="https://gumyum.com"&gt;a video game co-operative called gumyum&lt;/a&gt;. The basic idea is to build a large video game company owned completely by members. There will be …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Russell, why are you installing Unity? I thought you were an open source developer and &lt;a class="reference external" href="/yuletide-trains-and-homegrown-video-games/"&gt;homegrown video game engine builder&lt;/a&gt;?!&lt;/p&gt;
&lt;p&gt;&lt;img alt="gumyum logo" src="/uploads/2010/12/gumyumgameslogo.png" /&gt;&lt;/p&gt;
&lt;p&gt;The TL;DR we are starting &lt;a class="reference external" href="https://gumyum.com"&gt;a video game co-operative called gumyum&lt;/a&gt;. The basic idea is to build a large video game company owned completely by members. There will be no employees and most revenue after paying taxes, operational expenses, and marketing, will be divided among members.&lt;/p&gt;
&lt;p&gt;We are actively looking for members, specifically engineers who want to build games. We already have a distributed team of 4 artists and 2 engineers who meet on Slack as we progress toward delivering our first two games.&lt;/p&gt;
&lt;p&gt;Adopting Unity, at least for our first couple games, will lower the barrier of entry for other engineers when joining the co-operative.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Installing Unity On Linux&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To get started first we will create a Unity Account and install the &lt;tt class="docutils literal"&gt;Unity Hub&lt;/tt&gt;.
You need both in order to aquire a free licence for personal use.&lt;/p&gt;
&lt;p&gt;The &lt;tt class="docutils literal"&gt;Unity Hub&lt;/tt&gt; software will help you download various Unity versions, manage your licence, and manage game projects.&lt;/p&gt;
&lt;p&gt;You should go to the Unity website and created an account.&lt;/p&gt;
&lt;p&gt;I found the Linux &lt;tt class="docutils literal"&gt;Unity Hub&lt;/tt&gt; installer in &lt;a class="reference external" href="https://forum.unity.com/threads/unity-hub-v-1-0-0-is-now-available.555547/"&gt;this thread&lt;/a&gt; and downloaded it to my &lt;tt class="docutils literal"&gt;~/Downloads&lt;/tt&gt; directory.&lt;/p&gt;
&lt;p&gt;Next I opened a terminal and made the file executable, and executed it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/Downloads
chmod&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;755&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;UnityHub.AppImage
./UnityHub.AppImage
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;From there I needed to sign in and request a licence. I chose the free option since at this point we have a $0 revenue project.&lt;/p&gt;
&lt;p&gt;On the &lt;tt class="docutils literal"&gt;Installs&lt;/tt&gt; tab in &lt;tt class="docutils literal"&gt;Unity Hub&lt;/tt&gt; you may download various stable and LTS (long-time-supported) versions of Unity. I suggest using one of these stable versions, I accidentally grabbed a non-stable one and all I had was pink screens inside of Unity!&lt;/p&gt;
&lt;p&gt;Anyways, now you may start a Unity project. &lt;tt class="docutils literal"&gt;Unity Hub&lt;/tt&gt; seems to store files locally and also allows you to sync to the cloud. This enables easy collaboration on the same project.&lt;/p&gt;
&lt;p&gt;Thats all for now, please check back for more posts about gumyum!&lt;/p&gt;
&lt;img alt="Unity Hub interface" src="/uploads/2019/unity-hub-gumyum.png" /&gt;
</content><category term="misc"/><category term="Code"/><category term="Games"/><category term="Project"/></entry><entry><title>Pre-signed GET and POST for Digital Ocean Spaces</title><link href="https://russell.ballestrini.net/pre-signed-get-and-post-for-digital-ocean-spaces/" rel="alternate"/><published>2019-07-12T16:01:00-04:00</published><updated>2019-07-12T16:01:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2019-07-12:/pre-signed-get-and-post-for-digital-ocean-spaces/</id><summary type="html">&lt;p&gt;A pre-signed request grants a semi-trusted user temporary access to a private resource.&lt;/p&gt;
&lt;p&gt;Let's unpack that statement ...&lt;/p&gt;
&lt;p&gt;Pre-signed means, we bless a specific action on a specific private resource
for a short duration of time.&lt;/p&gt;
&lt;p&gt;Semi-trusted means, we have authenticated the user, but we don't trust them
to have full …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A pre-signed request grants a semi-trusted user temporary access to a private resource.&lt;/p&gt;
&lt;p&gt;Let's unpack that statement ...&lt;/p&gt;
&lt;p&gt;Pre-signed means, we bless a specific action on a specific private resource
for a short duration of time.&lt;/p&gt;
&lt;p&gt;Semi-trusted means, we have authenticated the user, but we don't trust them
to have full access to our private resources.&lt;/p&gt;
&lt;p&gt;That still seems a bit generic ...&lt;/p&gt;
&lt;p&gt;No worries, our next example will appear more concrete:&lt;/p&gt;
&lt;p&gt;Today we will pre-sign HTTP &lt;tt class="docutils literal"&gt;POST&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;GET&lt;/tt&gt; requests to grant a
semi-trusted user temporary access to objects in a private Digital Ocean Space.&lt;/p&gt;
&lt;p&gt;Pre-signing allows the client to perform specific actions on specific private
objects while still protecting us from link sharing and replay attacks.&lt;/p&gt;
&lt;p&gt;The following examples use Python (Boto3), and Curl.&lt;/p&gt;
&lt;p&gt;Before we start, you should reference the official &lt;a class="reference external" href="https://boto3.amazonaws.com/v1/documentation/api/latest/guide/s3-presigned-urls.html"&gt;Boto3 Presigned Urls&lt;/a&gt; documentation.&lt;/p&gt;
&lt;div class="section" id="pre-signed-get-or-download"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Pre-signed GET or Download&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Allow a semi-trusted user the ability to download a specific file named &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;my-object.zip&lt;/span&gt;&lt;/tt&gt;
from a private Digital Ocean Space named &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;example-bucket&lt;/span&gt;&lt;/tt&gt; for a short duration of &lt;tt class="docutils literal"&gt;30&lt;/tt&gt; seconds.&lt;/p&gt;
&lt;p&gt;Save this code into a file named &lt;tt class="docutils literal"&gt;pre_sign_get_test.py&lt;/tt&gt; and run it with &lt;tt class="docutils literal"&gt;python pre_sign_get_test.py&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;The result will be a URL that you may redirect a user to. For testing you should be able to load the URL into a web browser and download the file.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;botocore.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize an S3 session/client to talk to DigitalOcean Spaces.&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;s3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# configure the region you created the space in.&lt;/span&gt;
    &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;nyc3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# configure the region you created the space in.&lt;/span&gt;
    &lt;span class="n"&gt;endpoint_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://nyc3.digitaloceanspaces.com&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# configure your access key (generate this on the dashboard).&lt;/span&gt;
    &lt;span class="n"&gt;aws_access_key_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DOXXXXXXXXXXXXXXXXXX&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# configure your secret key (generate this on the dashboard).&lt;/span&gt;
    &lt;span class="n"&gt;aws_secret_access_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DOO/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;signed_get_object_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate_presigned_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ClientMethod&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;get_object&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;Bucket&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;example-bucket&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;Key&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;my-object.zip&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;ExpiresIn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# print the pre-signed GET request URL for downloading the object.&lt;/span&gt;
&lt;span class="c1"&gt;# You may redirect the user to this URL when they click a &amp;quot;download&amp;quot; button.&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signed_get_object_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="pre-signed-post-or-upload"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Pre-signed POST or Upload&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Allow a semi-trusted user the ability to upload a specific file named &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;my-object2.zip&lt;/span&gt;&lt;/tt&gt;
into a private Digital Ocean Space named &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;example-bucket&lt;/span&gt;&lt;/tt&gt; for a short duration of &lt;tt class="docutils literal"&gt;60&lt;/tt&gt; seconds.&lt;/p&gt;
&lt;p&gt;Save this code into a file named &lt;tt class="docutils literal"&gt;pre_sign_post_test.py&lt;/tt&gt; and run it with &lt;tt class="docutils literal"&gt;python pre_sign_get_test.py&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;The result will be a &lt;tt class="docutils literal"&gt;cURL&lt;/tt&gt; command that you may run on your console for testing.&lt;/p&gt;
&lt;p&gt;In a real situation, you would want to pass these parameters to the client and let it perform the authenticated upload.
The file never hits your server and is uploaded directly to the private Space.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; parameter order matters! The &lt;tt class="docutils literal"&gt;file&lt;/tt&gt; parameter must be last.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;botocore.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize an S3 session/client to talk to DigitalOcean Spaces.&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;s3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# configure the region you created the space in.&lt;/span&gt;
    &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;nyc3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# configure the region you created the space in.&lt;/span&gt;
    &lt;span class="n"&gt;endpoint_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;https://nyc3.digitaloceanspaces.com&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# configure your access key (generate this on the dashboard).&lt;/span&gt;
    &lt;span class="n"&gt;aws_access_key_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DOXXXXXXXXXXXXXXXXXX&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# configure your secret key (generate this on the dashboard).&lt;/span&gt;
    &lt;span class="n"&gt;aws_secret_access_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DOO/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;signed_post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate_presigned_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;example-bucket&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;my-object2.zip&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ExpiresIn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;signed_post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fields&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--form &amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;curl &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39; --form &amp;quot;file=@my-object2.zip;filename=my-object2.zip&amp;quot; &amp;#39;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;signed_post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#pre-signed-get-or-download" id="toc-entry-1"&gt;Pre-signed GET or Download&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#pre-signed-post-or-upload" id="toc-entry-2"&gt;Pre-signed POST or Upload&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/><category term="AWS"/></entry><entry><title>Hybrid Hot Water Heater Saves 69 Percent On Energy Consumption</title><link href="https://russell.ballestrini.net/hybrid-hot-water-heater-saves-69-percent-on-energy-consumption/" rel="alternate"/><published>2019-02-05T09:28:00-05:00</published><updated>2019-02-05T09:28:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2019-02-05:/hybrid-hot-water-heater-saves-69-percent-on-energy-consumption/</id><summary type="html">&lt;p&gt;&lt;em&gt;Disclosure: This post contains affiliate links.&lt;/em&gt; See &lt;a class="reference external" href="/disclosures-and-terms/"&gt;full disclosure page here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Exactly one year ago I fufilled a &lt;a class="reference external" href="/fulfilling-childhood-dreams-solar/"&gt;longtime childhood dream&lt;/a&gt; when I invested in roof top solar on my house in Eastern Connecticut (Zone 6b).&lt;/p&gt;
&lt;p&gt;Over the past 12 months, I have religiously tracked my families energy consumption using …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;em&gt;Disclosure: This post contains affiliate links.&lt;/em&gt; See &lt;a class="reference external" href="/disclosures-and-terms/"&gt;full disclosure page here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Exactly one year ago I fufilled a &lt;a class="reference external" href="/fulfilling-childhood-dreams-solar/"&gt;longtime childhood dream&lt;/a&gt; when I invested in roof top solar on my house in Eastern Connecticut (Zone 6b).&lt;/p&gt;
&lt;p&gt;Over the past 12 months, I have religiously tracked my families energy consumption using both a &amp;quot;&lt;a class="reference external" href="https://www.amazon.com/gp/product/B00009MDBU/ref=as_li_tl?ie=UTF8&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;creativeASIN=B00009MDBU&amp;amp;linkCode=as2&amp;amp;tag=russellball0b-20&amp;amp;linkId=b3410667dcccb96e343e7cda77ff46ff"&gt;Kill A Watt&lt;/a&gt;&amp;quot; and a &lt;a class="reference external" href="https://www.amazon.com/gp/product/B015IY0Z3E/ref=as_li_tl?ie=UTF8&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;creativeASIN=B015IY0Z3E&amp;amp;linkCode=as2&amp;amp;tag=russellball0b-20&amp;amp;linkId=727da547a2b0a22fa53016191c2cf313"&gt;Curb&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Along the way, I have found my largest energy consumer (abuser).&lt;/p&gt;
&lt;p&gt;Heating water.&lt;/p&gt;
&lt;p&gt;My traditional electric water heater had a monthly operating cost of $85. Worst yet, during some winter months, 100% of my solar production was soaked up by heating water!&lt;/p&gt;
&lt;p&gt;I demanded a better way!&lt;/p&gt;
&lt;p&gt;... and I found one ...&lt;/p&gt;
&lt;div class="section" id="hybrid-electric-hot-water-heat-pump"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Hybrid Electric Hot Water Heat Pump&lt;/a&gt;&lt;/h2&gt;
&lt;img alt="AO Smith Hybrid Hot Water Heater" class="align-right" src="/uploads/2019/ao-smith-hybrid-hot-water-heater.jpg" style="width: 300px;" /&gt;
&lt;p&gt;My new unit (&lt;a class="reference external" href="https://www.amazon.com/gp/product/B079RCGK12/ref=as_li_tl?ie=UTF8&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;creativeASIN=B079RCGK12&amp;amp;linkCode=as2&amp;amp;tag=russellball0b-20&amp;amp;linkId=7590d68023bc0d6b244587826aea587e"&gt;A.O. Smith 50 Gallon Voltex Hybrid Electric Heat Pump&lt;/a&gt;) transfers ambient heat from the air into water.&lt;/p&gt;
&lt;p&gt;The technology is relatively old, basically an air conditioner or refrigerator run in reverse.
Instead of dumping the cold into the tank, it dumps hot and as a by-product produces cold air.&lt;/p&gt;
&lt;p&gt;Yes that's right, this unit will suppliment my summer cooling because it will dump cold air while it produces hot water. The heat pump also serves as a dehumidifier, so I no longer run one of those!&lt;/p&gt;
&lt;p&gt;The install was mostly identical to a normal water heater, however there are a couple extra requirements:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;The unit needs at least 400 square feet to pull ambient air&lt;/li&gt;
&lt;li&gt;The unit produces condensate, similar to a dehumidifier or air conditioner, which needs to drain somewhere.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Additionally the heat pump fan produces a bit of sound, not terrible, and seems quiet to me.&lt;/p&gt;
&lt;p&gt;If you have a traditional electric water heater, you meet the extra requirements, and you consider yourself an environmentalist, you need to get a hybrid water heater.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="finances"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Finances&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;These units currently run between $1200 - $1500 but I'll break down the complete finances of my project.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;+ $1,149 purchase price
+    $65 delivery
+   $100 pipes and fittings
-   $300 instant rebate
-   $200 mail-in rebate
-   $200 craigslist 3 year old unit
===================================
+   $614
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I replaced the unit myself, with help from my father-in-law. I don't own a truck, so I decided to have it delivered for $65. I bought $100 worth of pipe and fittings, mostly because I don't want to learn how to sweat copper, so I use &lt;a class="reference external" href="https://www.amazon.com/gp/product/B01AS48PBS/ref=as_li_qf_asin_il_tl?ie=UTF8&amp;amp;tag=russellball0b-20&amp;amp;creative=9325&amp;amp;linkCode=as2&amp;amp;creativeASIN=B01AS48PBS&amp;amp;linkId=81ade3de2fc030c163112c53c7049885"&gt;sharkbite fittings&lt;/a&gt; which cost more but are fast and simple to use.&lt;/p&gt;
&lt;p&gt;My state offers an instant $300 in store rebate, plus a $200 mail in rebate.&lt;/p&gt;
&lt;p&gt;The old unit was only 3 years old (came with the new house purchase) so I sold it on craigslist for $200 (MSRP was $899).&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="return-on-investment"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Return on investment&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Ok, so $614 to replace a working unit, am I crazy? What is the expected ROI?&lt;/p&gt;
&lt;p&gt;Well, I've only had the unit for a month, however my energy consumption for heating hot water went from $85/mo to $30/mo.&lt;/p&gt;
&lt;p&gt;Let's just round down to $50/mo savings.&lt;/p&gt;
&lt;p&gt;Ok so in 12 months the unit will pay for itself. Even without the rebates you're still looking at only a 2 year ROI before the unit starts paying dividends.&lt;/p&gt;
&lt;p&gt;That's not all.&lt;/p&gt;
&lt;p&gt;I don't need to run a separate dehumidifier in my basement because the new unit removes humidity as a by-product!&lt;/p&gt;
&lt;p&gt;... but wait there's more!&lt;/p&gt;
&lt;p&gt;This unit also produces cold air as a by-product which means free supplimental cooling during the summer!&lt;/p&gt;
&lt;p&gt;A water heater, dehumidifier, and air conditioner all-in-one!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="what-is-the-catch"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;What is the catch?&lt;/a&gt;&lt;/h2&gt;
&lt;img alt="Curb Realtime Energy Consumption of AO Smith Hybrid Hot Water Heater" class="align-right" src="/uploads/2019/curb-ao-smith-hybrid-hot-water-heater-usage.png" /&gt;
&lt;p&gt;There is no catch, but you do have to agree on a small trade off, hot water recovery time.&lt;/p&gt;
&lt;p&gt;Instead of using 4,500 watts for 45 minutes (.75 hours), a hybrid hot water heat pump uses 350 watts for 3 hours.&lt;/p&gt;
&lt;p&gt;Trading the slightly longer hot water recovery times, gives you:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;a significant reduction on your energy bill&lt;/li&gt;
&lt;li&gt;dehumidification&lt;/li&gt;
&lt;li&gt;supplimental air conditioning / cooling&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;# traditional.
4500 * .75 # == 3375 watts or 3.375 kWhr to recover

# hybrid heatpump.
350 * 3 # == 1050 watts or 1.050 kWhr to recover
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The decrease in consumption means a huge savings of 69%!!!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;# percentage decrease.
(3375 - 1050) / 3375 # == 69% !!!
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So what are you waiting for? Honestly, if you are thinking about going solar, you should tackle this project first, right now!&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#hybrid-electric-hot-water-heat-pump" id="toc-entry-1"&gt;Hybrid Electric Hot Water Heat Pump&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#finances" id="toc-entry-2"&gt;Finances&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#return-on-investment" id="toc-entry-3"&gt;Return on investment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#what-is-the-catch" id="toc-entry-4"&gt;What is the catch?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Solar"/><category term="Project"/></entry><entry><title>AWS nvme to block mapping</title><link href="https://russell.ballestrini.net/aws-nvme-to-block-mapping/" rel="alternate"/><published>2019-01-19T14:50:00-05:00</published><updated>2019-01-19T14:50:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2019-01-19:/aws-nvme-to-block-mapping/</id><summary type="html">&lt;p&gt;Recently at work I transitioned our fleet from Ubuntu 14.04 LTS to Ubuntu 18.04 LTS. During the process I noticed an issue with our newer generation AWS EC2 &amp;quot;nitro&amp;quot; based instance types (specifically &lt;tt class="docutils literal"&gt;c5.2xlarge&lt;/tt&gt;).&lt;/p&gt;
&lt;p&gt;AWS was presenting my &lt;tt class="docutils literal"&gt;root&lt;/tt&gt; block device as &lt;tt class="docutils literal"&gt;/dev/nvme1n1&lt;/tt&gt; and my &lt;tt class="docutils literal"&gt;data …&lt;/tt&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;Recently at work I transitioned our fleet from Ubuntu 14.04 LTS to Ubuntu 18.04 LTS. During the process I noticed an issue with our newer generation AWS EC2 &amp;quot;nitro&amp;quot; based instance types (specifically &lt;tt class="docutils literal"&gt;c5.2xlarge&lt;/tt&gt;).&lt;/p&gt;
&lt;p&gt;AWS was presenting my &lt;tt class="docutils literal"&gt;root&lt;/tt&gt; block device as &lt;tt class="docutils literal"&gt;/dev/nvme1n1&lt;/tt&gt; and my &lt;tt class="docutils literal"&gt;data&lt;/tt&gt; device as &lt;tt class="docutils literal"&gt;/dev/nvme0n1&lt;/tt&gt;. For obvious reasons, this seemingly random and out-of-order assignment breaks my provisioning scripts.&lt;/p&gt;
&lt;p&gt;After much research and deep thought, I came up with a solid solution.&lt;/p&gt;
&lt;p&gt;I found a barely documented tool called &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;ebsnvme-id&lt;/span&gt;&lt;/tt&gt; on the official Amazon Linux AMI and wrote a wrapper (&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;nvme-to-block-mapping&lt;/span&gt;&lt;/tt&gt;) to iterate over all possible combinations of &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;/dev/nvme[0-26]n1&lt;/span&gt;&lt;/tt&gt; to create a symlink to the block mapping selected when we launch the EC2 instance.&lt;/p&gt;
&lt;p&gt;Since we have control over the block mapping, we end up with consistent and known symlink which we may confidently pass to our provisioning scripts when the time comes to partition, format, and use the block devices.&lt;/p&gt;
&lt;p&gt;To save you time, I have added these scripts to this blog post!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Please consider&lt;/strong&gt; &lt;a class="reference external" href="https://www.paypal.me/russellbal/5"&gt;donating here&lt;/a&gt; &lt;strong&gt;if my work has helped you!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Place the following two scripts into your AMI under &lt;tt class="docutils literal"&gt;/usr/sbin/&lt;/tt&gt; and trigger the wrapper just once prior to using the devices (don't worry the wrapper script is idempotent, running it more than once won't break anything).&lt;/p&gt;
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/_OYInsj7SYw" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen&gt;&lt;/iframe&gt;&lt;div class="section" id="nvme-to-block-mapping"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;nvme-to-block-mapping&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="reference external" href="/uploads/2019/nvme-to-block-mapping"&gt;/usr/sbin/nvme-to-block-mapping&lt;/a&gt; (click for file):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c1"&gt;# for details:&lt;/span&gt;
&lt;span class="c1"&gt;# https://russell.ballestrini.net/aws-nvme-to-block-mapping/&lt;/span&gt;

&lt;span class="c1"&gt;# this will create a symlinks like:&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;#     /dev/nvme1n1 -&amp;gt; /dev/xvdh&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# these ebs block device paths are set by stacker and assumed by ansible.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# if the device is non ebs, it will use the following mapping:&lt;/span&gt;
&lt;span class="nv"&gt;non_ebs_mapping&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/dev/sdb1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/dev/sdc1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/dev/sdd1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/dev/sde1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/dev/sdf1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/dev/sdg1&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# nvme0n1 uses ${non_ebs_mapping[0]} (the 0 index item in the array)&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;#     /dev/nvme0n1 -&amp;gt; /dev/sdb1&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# nvme3n1 uses ${non_ebs_mapping[3]} (the 3 index item in the array)&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;#     /dev/nvme3n1 -&amp;gt; /dev/sde1&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;

&lt;span class="c1"&gt;# why we only iterate from 0 to 26:&lt;/span&gt;
&lt;span class="c1"&gt;# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/device_naming.html&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;seq&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;26&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;nvme_block_device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/dev/nvme&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;n1&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# skip any nvme paths which don&amp;#39;t exist.&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;$nvme_block_device&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# get ebs block mapping device path set by stacker (or base AMI).&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nv"&gt;mapping_device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;/usr/sbin/ebsnvme-id&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;nvme_block_device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--block-dev&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# if the mapping_device is empty, it isn&amp;#39;t an EBS device so&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# we will use the non_ebs_mapping to translate the device.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$mapping_device&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nv"&gt;mapping_device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;non_ebs_mapping&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# if block mapping device path does not start with /dev/ fix it.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$mapping_device&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;/dev/*&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nv"&gt;mapping_device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/dev/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;mapping_device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# if the block mapping device path already exists, skip it.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$mapping_device&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;path exists: &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;mapping_device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# otherwise, create a symlink from nvme block device to mapping device.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;symlink created: &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;nvme_block_device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;mapping_device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$nvme_block_device&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$mapping_device&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="ebsnvme-id"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;ebsnvme-id&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="reference external" href="/uploads/2019/ebsnvme-id"&gt;/usr/sbin/ebsnvme-id&lt;/a&gt; (click for file):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="ch"&gt;#!/usr/bin/env python2.7&lt;/span&gt;

&lt;span class="c1"&gt;# Copyright (C) 2017 Amazon.com, Inc. or its affiliates.&lt;/span&gt;
&lt;span class="c1"&gt;# All Rights Reserved.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Licensed under the Apache License, Version 2.0 (the &amp;quot;License&amp;quot;).&lt;/span&gt;
&lt;span class="c1"&gt;# You may not use this file except in compliance with the License.&lt;/span&gt;
&lt;span class="c1"&gt;# A copy of the License is located at&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;#    http://aws.amazon.com/apache2.0/&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# or in the &amp;quot;license&amp;quot; file accompanying this file. This file is&lt;/span&gt;
&lt;span class="c1"&gt;# distributed on an &amp;quot;AS IS&amp;quot; BASIS, WITHOUT WARRANTIES OR CONDITIONS&lt;/span&gt;
&lt;span class="c1"&gt;# OF ANY KIND, either express or implied. See the License for the&lt;/span&gt;
&lt;span class="c1"&gt;# specific language governing permissions and limitations under the&lt;/span&gt;
&lt;span class="c1"&gt;# License.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# Reference:&lt;/span&gt;
&lt;span class="c1"&gt;# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/nvme-ebs-volumes.html&lt;/span&gt;

&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;Usage:&lt;/span&gt;
&lt;span class="sd"&gt;Read EBS device information and provide information about&lt;/span&gt;
&lt;span class="sd"&gt;the volume.&lt;/span&gt;
&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;ctypes&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;fcntl&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ioctl&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;NVME_ADMIN_IDENTIFY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x06&lt;/span&gt;
&lt;span class="n"&gt;NVME_IOCTL_ADMIN_CMD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0xC0484E41&lt;/span&gt;
&lt;span class="n"&gt;AMZN_NVME_VID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mh"&gt;0x1D0F&lt;/span&gt;
&lt;span class="n"&gt;AMZN_NVME_EBS_MN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Amazon Elastic Block Store&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;nvme_admin_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Structure&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;_pack_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;_fields_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;opcode&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;      &lt;span class="c1"&gt;# op code&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;flags&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="c1"&gt;# fused operation&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;        &lt;span class="c1"&gt;# command id&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;nsid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="c1"&gt;# namespace id&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;reserved0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mptr&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="c1"&gt;# metadata pointer&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;addr&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="c1"&gt;# data pointer&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mlen&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="c1"&gt;# metadata length&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;alen&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="c1"&gt;# data length&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cdw10&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cdw11&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cdw12&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cdw13&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cdw14&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cdw15&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;reserved1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint64&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;nvme_identify_controller_amzn_vs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Structure&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;_pack_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;_fields_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bdev&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# block device name&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;reserved0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;nvme_identify_controller_psd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Structure&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;_pack_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;_fields_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mp&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="c1"&gt;# maximum power&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;reserved0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;enlat&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;     &lt;span class="c1"&gt;# entry latency&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;exlat&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;     &lt;span class="c1"&gt;# exit latency&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;rrt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="c1"&gt;# relative read throughput&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;rrl&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="c1"&gt;# relative read latency&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;rwt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="c1"&gt;# relative write throughput&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;rwl&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="c1"&gt;# relative write latency&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;reserved1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;nvme_identify_controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Structure&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;_pack_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;_fields_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;# PCI Vendor ID&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ssvid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;        &lt;span class="c1"&gt;# PCI Subsystem Vendor ID&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sn&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;        &lt;span class="c1"&gt;# Serial Number&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mn&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;        &lt;span class="c1"&gt;# Module Number&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fr&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;         &lt;span class="c1"&gt;# Firmware Revision&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;rab&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;           &lt;span class="c1"&gt;# Recommend Arbitration Burst&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ieee&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;      &lt;span class="c1"&gt;# IEEE OUI Identifier&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mic&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;           &lt;span class="c1"&gt;# Multi-Interface Capabilities&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mdts&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;# Maximum Data Transfer Size&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;reserved0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;oacs&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;         &lt;span class="c1"&gt;# Optional Admin Command Support&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;acl&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;           &lt;span class="c1"&gt;# Abort Command Limit&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;aerl&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;# Asynchronous Event Request Limit&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;frmw&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;# Firmware Updates&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lpa&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;           &lt;span class="c1"&gt;# Log Page Attributes&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;elpe&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;# Error Log Page Entries&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;npss&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;# Number of Power States Support&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;avscc&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;         &lt;span class="c1"&gt;# Admin Vendor Specific Command Configuration&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;reserved1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;265&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sqes&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;# Submission Queue Entry Size&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cqes&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;# Completion Queue Entry Size&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;reserved2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;nn&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;            &lt;span class="c1"&gt;# Number of Namespaces&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;oncs&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;         &lt;span class="c1"&gt;# Optional NVM Command Support&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fuses&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;        &lt;span class="c1"&gt;# Fused Operation Support&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fna&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;           &lt;span class="c1"&gt;# Format NVM Attributes&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vwc&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;           &lt;span class="c1"&gt;# Volatile Write Cache&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;awun&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;         &lt;span class="c1"&gt;# Atomic Write Unit Normal&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;awupf&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;        &lt;span class="c1"&gt;# Atomic Write Unit Power Fail&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;nvscc&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;         &lt;span class="c1"&gt;# NVM Vendor Specific Command Configuration&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;reserved3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;704&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;531&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;reserved4&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c_uint8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;704&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;psd&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nvme_identify_controller_psd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;     &lt;span class="c1"&gt;# Power State Descriptor&lt;/span&gt;
                &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vs&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nvme_identify_controller_amzn_vs&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  &lt;span class="c1"&gt;# Vendor Specific&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ebs_nvme_device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ctrl_identify&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_nvme_ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id_response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id_len&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;admin_cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nvme_admin_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opcode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NVME_ADMIN_IDENTIFY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                       &lt;span class="n"&gt;addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id_response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                       &lt;span class="n"&gt;alen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id_len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                       &lt;span class="n"&gt;cdw10&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;rw&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;nvme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NVME_IOCTL_ADMIN_CMD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admin_cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ctrl_identify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_ctrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nvme_identify_controller&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_nvme_ioctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;addressof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_ctrl&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_ctrl&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vid&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;AMZN_NVME_VID&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;AMZN_NVME_EBS_MN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[ERROR] Not an EBS device: &amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{0}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_volume_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;vol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sn&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;vol&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;vol&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;vol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;vol-&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;vol&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;vol&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_block_device&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stripped&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;dev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id_ctrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bdev&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stripped&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/dev/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;dev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Reads EBS information from NVMe devices.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;device&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Device to query&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_argument_group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Display Options&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-v&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;--volume&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;store_true&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Return volume-id&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-b&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;--block-dev&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;store_true&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Return block device mapping&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-u&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;--udev&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;store_true&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Output data in format suitable for udev rules&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;print_help&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;get_all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;udev&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;volume&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block_dev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;dev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ebs_nvme_device&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ne"&gt;IOError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ne"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;get_all&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Volume ID: &lt;/span&gt;&lt;span class="si"&gt;{0}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_volume_id&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;get_all&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;block_dev&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;udev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_block_device&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;udev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#nvme-to-block-mapping" id="toc-entry-1"&gt;nvme-to-block-mapping&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#ebsnvme-id" id="toc-entry-2"&gt;ebsnvme-id&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/><category term="AWS"/></entry><entry><title>Yuletide Trains and Homegrown Video Games</title><link href="https://russell.ballestrini.net/yuletide-trains-and-homegrown-video-games/" rel="alternate"/><published>2018-12-25T17:15:00-05:00</published><updated>2018-12-25T17:15:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2018-12-25:/yuletide-trains-and-homegrown-video-games/</id><summary type="html">&lt;img alt="pixel art style seasonal wrapped gift" class="align-right" src="/uploads/2018/pixel-art-gift.png" /&gt;
&lt;p&gt;Each holiday season I find myself drawn to a side passion of mine.&lt;/p&gt;
&lt;p&gt;While some build model trains and others create Christmas light shows with synchronized music you'll find me on my sofa where I build, explore, and tinker on my own video game engine.&lt;/p&gt;
&lt;p&gt;Maybe the sound of wrapping …&lt;/p&gt;</summary><content type="html">&lt;img alt="pixel art style seasonal wrapped gift" class="align-right" src="/uploads/2018/pixel-art-gift.png" /&gt;
&lt;p&gt;Each holiday season I find myself drawn to a side passion of mine.&lt;/p&gt;
&lt;p&gt;While some build model trains and others create Christmas light shows with synchronized music you'll find me on my sofa where I build, explore, and tinker on my own video game engine.&lt;/p&gt;
&lt;p&gt;Maybe the sound of wrapping paper tearing acts as a trigger, but I only prioritize time on my game engine during the holiday season.&lt;/p&gt;
&lt;p&gt;Do you have a specific hobby that you do during the holiday season but not during other parts of the year?&lt;/p&gt;
&lt;blockquote&gt;
Why am I working on Christmas every year - Am I a work-a-holic?&lt;/blockquote&gt;
&lt;div class="section" id="work-or-play"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Work or Play.&lt;/a&gt;&lt;/h2&gt;
&lt;img alt="green pixel art style Christmas tree with animated blinking lights" class="align-right" src="/uploads/2018/pixel-art-yuletide-tree.gif" /&gt;
&lt;p&gt;Why do I absorb myself into this seasonal hobby? Am I escaping? Or am I making use of down time to work on a project that I am too occupied to even think about during the rest of the year?&lt;/p&gt;
&lt;p&gt;For me, building my game on Christmas is fun, not &lt;em&gt;work&lt;/em&gt;. Similar to how fishing while camping is fun, not &lt;em&gt;work&lt;/em&gt;.&lt;/p&gt;
&lt;blockquote&gt;
What is the last activity a commercial fisherman would do &amp;quot;for fun&amp;quot; on Christmas?
Fishing.&lt;/blockquote&gt;
&lt;p&gt;What is fun for one person might be work for another. It is relative and depends on context.&lt;/p&gt;
&lt;p&gt;I suspect that most people experience a tipping point, when an activity switches from fun to work.&lt;/p&gt;
&lt;p&gt;I think, for an average person, this moment occurs when the answer to the following question changes:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Do you rely on the activity for the majority of your income?&lt;ul&gt;
&lt;li&gt;if yes, the activity often feels like work&lt;/li&gt;
&lt;li&gt;if no, the activity often feels like fun&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="section" id="work-and-play"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Work and Play?&lt;/a&gt;&lt;/h2&gt;
&lt;img alt="red pixel art style santa hat" class="align-right" src="/uploads/2018/pixel-art-santa-hat.png" /&gt;
&lt;p&gt;Now that's not to say you cannot find fun while at work, and vice versa. Personally, I aim to land a perfect balance between the two with anything I do.&lt;/p&gt;
&lt;p&gt;In my situation, I use the computer &amp;quot;professionally&amp;quot; each work day, for 7-10 hours.&lt;/p&gt;
&lt;p&gt;If I was in the games industry, I doubt I would find much fun in building a video game on Christmas. But I'm not in the games industry and I have more fun building games than playing games.&lt;/p&gt;
&lt;p&gt;That is to say, for me building a game feels fun while playing a game feels like work. Crazy right?&lt;/p&gt;
&lt;img alt="animated gif of a pixel art style circular bomb with fuse burning down" class="align-right" src="/uploads/2018/pixel-art-bomb.gif" /&gt;
&lt;p&gt;Even though the activities of my &amp;quot;day job&amp;quot; overlap with the activities involved with building my game, there are enough differences. These differences make one feel like work while the other feel like fun.&lt;/p&gt;
&lt;p&gt;I'm not making an excuse for working during Christmas; I honestly have a blast coding and tinkering on my game.&lt;/p&gt;
&lt;p&gt;I love getting stumped and learning. I love showing different ideas to my boys and seeing their eyes light up. I love to see how fast I can implement a small suggestion one of them have. I love how there are no rules and only my own skills and understanding can block me.&lt;/p&gt;
&lt;p&gt;While my family is occupied with their new holiday gifts, I use my free time to explore creative ideas on a non-stressful personal project. A project where the stakes are low and the pleasure is high.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="show-me-the-game-already"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Show me the game already!&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This years game engine modifications:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;ability to save the game state to a &lt;cite&gt;json&lt;/cite&gt; file&lt;/li&gt;
&lt;li&gt;ability to load the game state from a &lt;cite&gt;json&lt;/cite&gt; file&lt;/li&gt;
&lt;li&gt;added a few algorithms for auto-generating maps&lt;/li&gt;
&lt;li&gt;refactored some common code (so I'm not reapeating myself)&lt;/li&gt;
&lt;li&gt;implemented an action charge / recharge system&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is a short video of the current state of the engine. Let me know what you think in the comments.&lt;/p&gt;
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/qeMPV6aXKzk" frameborder="0" allow="accelerometer; encrypted-media; gyroscope; picture-in-picture" allowfullscreen&gt;&lt;/iframe&gt;&lt;p&gt;I also took the time to learn how to make pixel art using &lt;cite&gt;GIMP&lt;/cite&gt;, a useful skill for making game assets.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="who-else-makes-games-during-christmas"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Who else makes games during Christmas?&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I reached out to a number of indie game developers while formulating this blog post and I had a solid conversation with &lt;a class="reference external" href="https://github.com/TheMaverickProgrammer"&gt;Maverick Peppers&lt;/a&gt; a developer who runs a &lt;a class="reference external" href="https://protocomplete.com/"&gt;software company called ProtoComplete&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;He was working during Christmas on his game while making pudding and drinking white russians (&lt;a class="reference external" href="/yuletide-trains-and-homegrown-video-games/#white-russian-recipe"&gt;recipe in the foot notes&lt;/a&gt;). He arrived at the conclusion that I was working from a passion-oriented mindset while he was from a goal-oriented mindset.&lt;/p&gt;
&lt;p&gt;Maverick doesn't rely on the income from his games, so I asked &amp;quot;why are you 'working' on Christmas?&amp;quot; He explained that because he often has a &lt;cite&gt;type-a&lt;/cite&gt; or &amp;quot;goal mindset&amp;quot;, he has trouble relaxing until he finishes whatever he is working on. In a sense, he was working Christmas &lt;em&gt;so that he could&lt;/em&gt; relax.&lt;/p&gt;
&lt;p&gt;As for myself, building games is a passion-oreiented process, I don't rely on the income and I use it to unwind.&lt;/p&gt;
&lt;p&gt;We talked about how people can approach activities with either:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;cite&gt;type-a&lt;/cite&gt; (goal-oriented mindset)&lt;/li&gt;
&lt;li&gt;&lt;cite&gt;type-b&lt;/cite&gt; (passion-oriented mindset)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fun part is people are often both, depending on the activity or project. For example Maverick has a passion mindset when making music and DJing, but always takes a goal mindset when it comes to business.&lt;/p&gt;
&lt;p&gt;Do you have a specific hobby that you do during the holiday season but not during other parts of the year?&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="footnotes"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;Footnotes&lt;/a&gt;&lt;/h2&gt;
&lt;div class="section" id="unwrapping-my-christmas-commit-history"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;Unwrapping My Christmas Commit History&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Looking at my commit history, the first iteration of my current game engine was saved on Jan 01, 2014 with a few commits until Jan 19, 2014, at which point nothing until Dec 25, 2014 (Christmas itself) when I sprinted until Jan 10, 2015.&lt;/p&gt;
&lt;p&gt;The next year, I must have hacked on something else, with no changes until Oct 09, 2016 where I had two commits.&lt;/p&gt;
&lt;p&gt;Like clockwork on Dec 25, 2016 (Christmas) I tried to fix a regression in the engine's collision and intersection code. I left myself some breadcrumb comments to help me debug in the future... Nothing in 2017.&lt;/p&gt;
&lt;p&gt;Today is Christmas 2018 and finally I have a work around for the regression I was looking into from Christmas 2016!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="porting-sfml-rect-from-c-to-python"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-7"&gt;Porting SFML Rect from C++ to Python&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;This work around ports the &lt;cite&gt;Rect&lt;/cite&gt; intersection logic of &lt;cite&gt;SFML&lt;/cite&gt; from C++ to pure Python and avoids the following error message:&lt;/p&gt;
&lt;blockquote&gt;
&lt;cite&gt;terminated by signal SIGSEGV (Address boundary error)&lt;/cite&gt;&lt;/blockquote&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_rect_intersection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Accept two sfml.graphics.Rect objects.&lt;/span&gt;
&lt;span class="sd"&gt;    Return a new sfml.graphics.Rect of the overlap or None.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="c1"&gt;# We allow Rects with negative dimensions, so handle them correctly.&lt;/span&gt;

    &lt;span class="c1"&gt;# Compute the min and max of the first Rect (r1).&lt;/span&gt;
    &lt;span class="n"&gt;r1_min_x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r1_max_x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r1_min_y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r1_max_y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Compute the min and max of the second Rect (r2).&lt;/span&gt;
    &lt;span class="n"&gt;r2_min_x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r2_max_x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r2_min_y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r2_max_y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# compute the intersection boundaries.&lt;/span&gt;
    &lt;span class="n"&gt;i_left&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r1_min_x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2_min_x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;i_top&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r1_min_y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2_min_y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;i_right&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r1_max_x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2_max_x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;i_bottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r1_max_y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2_max_y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# if the intersection is valid (positive non zero area),&lt;/span&gt;
    &lt;span class="c1"&gt;# then there is an intersection.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i_left&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;i_right&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;i_top&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;i_bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sfml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;graphics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rect&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;i_left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i_top&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i_right&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;i_left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i_bottom&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;i_top&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="white-russian-recipe"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-8"&gt;White Russian Recipe&lt;/a&gt;&lt;/h3&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;1/4 cup distilled водка&lt;/li&gt;
&lt;li&gt;1/4 cup Kahlua coffee rum&lt;/li&gt;
&lt;li&gt;1/2 cup cream&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="section" id="older-versions-of-the-game-engine"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-9"&gt;Older versions of the game engine&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Some &lt;a class="reference external" href="https://russell.ballestrini.net/test-game-engine-with-python-and-sfml/"&gt;videos of older versions&lt;/a&gt; of this game engine.&lt;/p&gt;
&lt;div class="contents topic" id="index"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;index&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#work-or-play" id="toc-entry-1"&gt;Work or Play.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#work-and-play" id="toc-entry-2"&gt;Work and Play?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#show-me-the-game-already" id="toc-entry-3"&gt;Show me the game already!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#who-else-makes-games-during-christmas" id="toc-entry-4"&gt;Who else makes games during Christmas?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#footnotes" id="toc-entry-5"&gt;Footnotes&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#unwrapping-my-christmas-commit-history" id="toc-entry-6"&gt;Unwrapping My Christmas Commit History&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#porting-sfml-rect-from-c-to-python" id="toc-entry-7"&gt;Porting SFML Rect from C++ to Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#white-russian-recipe" id="toc-entry-8"&gt;White Russian Recipe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#older-versions-of-the-game-engine" id="toc-entry-9"&gt;Older versions of the game engine&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Games"/><category term="Python"/></entry><entry><title>All Local Heros need a Gig Side Kick</title><link href="https://russell.ballestrini.net/all-local-heros-need-a-gig-side-kick/" rel="alternate"/><published>2018-08-03T11:20:00-04:00</published><updated>2018-08-03T11:20:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2018-08-03:/all-local-heros-need-a-gig-side-kick/</id><summary type="html">&lt;p&gt;Five months ago during my preparation and ramp up to gardening season, I started thinking about what it would be like to have a partner, a side kick, a secretary to keep me honest and on task.&lt;/p&gt;
&lt;p&gt;Somebody who could review old journal notes from previous years and give me …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Five months ago during my preparation and ramp up to gardening season, I started thinking about what it would be like to have a partner, a side kick, a secretary to keep me honest and on task.&lt;/p&gt;
&lt;p&gt;Somebody who could review old journal notes from previous years and give me hints about what seeds I should be starting, what worked well, what didn't, and why.&lt;/p&gt;
&lt;p&gt;I started thinking I'm likely not alone and that other hobby and market gardeners likely feel the same way. I then expanded this, likely anyone bootstrapping a small business &amp;quot;enterprise&amp;quot; could use a side kick. Small craft brewers, artists, sign makers, the list goes on and on.&lt;/p&gt;
&lt;p&gt;As you might know I'm deeply interested in systems like computer science and permaculture. Computer scientists often try to model complex natural systems. Often natural systems give us clues on how to make computers perform certain tasks.&lt;/p&gt;
&lt;p&gt;One reoccuring theme I find between computer science and permaculture is the idea of inputs and outputs.&lt;/p&gt;
&lt;p&gt;Brewing wine from berries requires the following inputs: boiling water, berries, sugar, yeast, and yeast nutrient. Combined all these inputs together in the right way, wait for a certain time, and the system returns an a tasty, alchoholic, product with a long shelf life as an output. The spent berries go straight into the compost for next years garden. Natural systems and unnatural systems model after natural ones, have no waste by-products (pollution). Every output of one process is always an input of another.&lt;/p&gt;
&lt;p&gt;Only when we mess up the natural order of organic systems do we end up with polution. Polution is simply a holistically bad output. For example, when we artifically produce outputs, like plastics there is no natural system to take it as an input, it becomes polution and makes the system sick.&lt;/p&gt;
&lt;p&gt;To fight off the production of unnatural outputs, like single use plastics, we need to create other human managed systems to use the pollution as inputs. For example, I'm insulating my shed with bags of single use polyethylene wrappers that wrap my families purchases. I'm keeping the valuable (although un-natural) resource out of the waste stream and using it to shrink the tempurature swing deltas in my shed. When we model the material world as inputs and outputs we can learn how to best use what we have on hand.&lt;/p&gt;
&lt;p&gt;We can boost each of our local economies by providing proper tools to the local makers.&lt;/p&gt;
&lt;p&gt;This is why today I'm announcing GigSideKick to flip the script on globalization and large enterprise giants.&lt;/p&gt;
&lt;p&gt;GigSideKick is a &amp;quot;field&amp;quot; journal app to track inputs and outputs. The primary goal is to collect the right amount of data with the least amount of friction, have an intuitive user interface, and a user experience which mostly stays out of the way. In addition to tracking inputs and outputs, the user will optionally set ETAs on processes set in motion which will power reminders.&lt;/p&gt;
&lt;blockquote&gt;
Each local hero deserves the unfair advantage that a smart and capabile Side Kick provides.&lt;/blockquote&gt;
&lt;p&gt;This type of input/output journal will not interfere with the task at hand, and would only require a few button presses. The data created from such a journal is important for the user's future self, and if properly aggregated and crowd sourced, the data will enable a powerful recommendation engine.&lt;/p&gt;
&lt;p&gt;For example, a person subscribing to a &amp;quot;garden&amp;quot; knowledge pack might get the following recommendation: &amp;quot;gardeners in your area are known to typically start on average of 24 zucchini seeds this month, which typically result in 18 viable zucchini seedlings in 6 days&amp;quot;, &amp;quot;How many will you start?&amp;quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;None&lt;/li&gt;
&lt;li&gt;12&lt;/li&gt;
&lt;li&gt;24&lt;/li&gt;
&lt;li&gt;48&lt;/li&gt;
&lt;li&gt;Other&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the background, the app would also track human labor (which itself is valuable input), track inventory of parts, work-in-process, and finished goods. Finally the app's data would power a local marketplace to help the maker trade and sell finished products. After all, if you produce a surplus of apples without a system in place to use them, the apples will end up as an input to the compost pile instead of the bellies of deserving humans. : )&lt;/p&gt;
&lt;p&gt;I believe we must give power back to local communities and economies. We, the side gig heros and local businesses operators, need all the help we can get to compete in today's global economy.&lt;/p&gt;
&lt;p&gt;Local makers have many often un-exploited unfair advantages. We do not need to ship our goods which means we should have lower margins. We need to figure out how to market our products (outputs) to local people looking for our superior goods.&lt;/p&gt;
&lt;p&gt;Another often untapped unfair advantage is the sale or trade of our by-products. We can even trade our waste! For example as a gardener, I would gladly trade a heaping bowl of same day cut, &amp;quot;beyond organic&amp;quot; salad greens, for your bucket of leaves, coffee grounds, sticks, sawdust, or grass clippings! It's honestly the timeless addage of &amp;quot;one mans trash is another mans treasure&amp;quot;. Your waste is a very useful input to the correct system and local people trading outputs is by far the best thing we can do for our planet.&lt;/p&gt;
&lt;p&gt;I plan to document my journy of building this vision, I hope you join me.&lt;/p&gt;
&lt;p&gt;Like always, please feel free to leave comments below, I always respond.&lt;/p&gt;
</content><category term="misc"/><category term="Project"/></entry><entry><title>Running DynamoDB Local service container on CircleCI 2.0</title><link href="https://russell.ballestrini.net/running-dynamodb-local-service-container-on-circleci-2/" rel="alternate"/><published>2018-07-18T11:56:00-04:00</published><updated>2018-07-18T11:56:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2018-07-18:/running-dynamodb-local-service-container-on-circleci-2/</id><summary type="html">&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; use a custom entrypoint in your CircleCI 2.0 config to limit Java memory to 1G.&lt;/p&gt;
&lt;p&gt;The new CircleCI 2.0 docker configuration supports a &amp;quot;primary image&amp;quot; (listed first) which runs all the &amp;quot;steps&amp;quot; as well as zero or many &amp;quot;service images&amp;quot; (listed subsequently). The &amp;quot;service images&amp;quot;, although …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; use a custom entrypoint in your CircleCI 2.0 config to limit Java memory to 1G.&lt;/p&gt;
&lt;p&gt;The new CircleCI 2.0 docker configuration supports a &amp;quot;primary image&amp;quot; (listed first) which runs all the &amp;quot;steps&amp;quot; as well as zero or many &amp;quot;service images&amp;quot; (listed subsequently). The &amp;quot;service images&amp;quot;, although not running in the same container as the primary present as if local to the primary.&lt;/p&gt;
&lt;p&gt;It sort of feels like mixing many docker containers together.&lt;/p&gt;
&lt;p&gt;Enough talk, in this example, I show how easy it is to run &lt;a class="reference external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html"&gt;DynamoDB Local&lt;/a&gt; in a separate container but at the same time present it to the primary as &lt;tt class="docutils literal"&gt;127.0.0.1:8000&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;2&lt;/span&gt;
&lt;span class="nt"&gt;workflows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;2&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;tests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;test&lt;/span&gt;

&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;working_directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;/go/src/github.com/remind101/r101-myapp&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;docker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;# Primary container image where all the steps run.&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;circleci/golang:1.8&lt;/span&gt;

&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;# Service container image made available to the primary container at `host: localhost`&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;dwmkerr/dynamodb:41&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# custom entrypoint to limit RAM to 1G to prevent OOM on CircleCI 2.0.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# https://circleci.com/docs/2.0/configuration-reference/#docker--machine--macosexecutor&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;entrypoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;java&amp;quot;&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;-Xmx1G&amp;quot;&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;-jar&amp;quot;&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;DynamoDBLocal.jar&amp;quot;&lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;checkout&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;make test_verbose&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="section" id="troubleshooting"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Troubleshooting&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When I first tried to use this image, any test which tried to reach out to DynamoDB via &lt;tt class="docutils literal"&gt;127.0.0.1:8000&lt;/tt&gt; had the following exception:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;Test&lt;span class="w"&gt; &lt;/span&gt;Panicked
Error&lt;span class="w"&gt; &lt;/span&gt;creating&lt;span class="w"&gt; &lt;/span&gt;table&lt;span class="w"&gt; &lt;/span&gt;group_associations:&lt;span class="w"&gt; &lt;/span&gt;RequestError:&lt;span class="w"&gt; &lt;/span&gt;send&lt;span class="w"&gt; &lt;/span&gt;request&lt;span class="w"&gt; &lt;/span&gt;failed
caused&lt;span class="w"&gt; &lt;/span&gt;by:&lt;span class="w"&gt; &lt;/span&gt;Post&lt;span class="w"&gt; &lt;/span&gt;http://127.0.0.1:8000/:&lt;span class="w"&gt; &lt;/span&gt;dial&lt;span class="w"&gt; &lt;/span&gt;tcp&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;127&lt;/span&gt;.0.0.1:8000:&lt;span class="w"&gt; &lt;/span&gt;getsockopt:&lt;span class="w"&gt; &lt;/span&gt;connection&lt;span class="w"&gt; &lt;/span&gt;refused
/usr/local/go/src/runtime/panic.go:489
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It seems the docker-dynamodb (DynamoDB Local) container failed to start and exited like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;Initializing&lt;span class="w"&gt; &lt;/span&gt;DynamoDB&lt;span class="w"&gt; &lt;/span&gt;Local&lt;span class="w"&gt; &lt;/span&gt;with&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;following&lt;span class="w"&gt; &lt;/span&gt;configuration:
Port:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8000&lt;/span&gt;
InMemory:&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
DbPath:&lt;span class="w"&gt;       &lt;/span&gt;null
SharedDb:&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
shouldDelayTransientStatuses:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
CorsParams:&lt;span class="w"&gt;   &lt;/span&gt;*


Exited&lt;span class="w"&gt; &lt;/span&gt;with&lt;span class="w"&gt; &lt;/span&gt;code&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;137&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Apparently &lt;tt class="docutils literal"&gt;Exit code 137&lt;/tt&gt; is a classic Docker + Java error code caused by an OOM (out of memory).&lt;/p&gt;
&lt;p&gt;By default, projects on CircleCI build in virtual environments with 4GB of RAM but this is shared and Java acts greedy when inside a container so it needs a limit by adding &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-Xmx1G&lt;/span&gt;&lt;/tt&gt; to the &lt;tt class="docutils literal"&gt;entrypoint&lt;/tt&gt;!&lt;/p&gt;
&lt;p&gt;References:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://circleci.com/blog/how-to-handle-java-oom-errors/"&gt;https://circleci.com/blog/how-to-handle-java-oom-errors/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://circleci.com/docs/2.0/postgres-config/"&gt;https://circleci.com/docs/2.0/postgres-config/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#troubleshooting" id="toc-entry-1"&gt;Troubleshooting&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="AWS"/><category term="Docker"/></entry><entry><title>Pyramid SQLAlchemy bootstrap console script with transaction.manager</title><link href="https://russell.ballestrini.net/pyramid-sqlalchemy-bootstrap-console-script-with-transaction-manager/" rel="alternate"/><published>2018-06-15T10:54:00-04:00</published><updated>2018-06-15T10:54:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2018-06-15:/pyramid-sqlalchemy-bootstrap-console-script-with-transaction-manager/</id><summary type="html">&lt;p&gt;So I've struggled for a while with the best way to properly setup a console script for my SQLAlchemy Pyramid apps.&lt;/p&gt;
&lt;p&gt;I use the &lt;a class="reference external" href="https://github.com/Pylons/pyramid-cookiecutter-alchemy"&gt;Pyramid Cookiecutter Alchemy&lt;/a&gt; to setup my projects and as such, I do not have a global and thus importable DBSession object. Instead my database session is …&lt;/p&gt;</summary><content type="html">&lt;p&gt;So I've struggled for a while with the best way to properly setup a console script for my SQLAlchemy Pyramid apps.&lt;/p&gt;
&lt;p&gt;I use the &lt;a class="reference external" href="https://github.com/Pylons/pyramid-cookiecutter-alchemy"&gt;Pyramid Cookiecutter Alchemy&lt;/a&gt; to setup my projects and as such, I do not have a global and thus importable DBSession object. Instead my database session is attached to the request on creation.&lt;/p&gt;
&lt;p&gt;Anyways, here is my recipe:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;argparse&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pyramid.paster&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;setup_logging&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;remarkbox.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;invalidate_all_node_cache_objects&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_arg_parser&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Invalidate all NodeCache objs to force recomputation.&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-c&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;--config&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;development.ini&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_arg_parser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;setup_logging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# use bootstrap context manager to prepare app and request,&lt;/span&gt;
    &lt;span class="c1"&gt;# next use the resulting request&amp;#39;s transaction manager!&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;request&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;request&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;invalidate_all_node_cache_objects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dbsession&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This pattern should help you solve this error:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;NoTransaction&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="n"&gt;when&lt;/span&gt; &lt;span class="n"&gt;using&lt;/span&gt; &lt;span class="n"&gt;bootstrap&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Python"/></entry><entry><title>Quickstart to DKIM Sign Email with Python</title><link href="https://russell.ballestrini.net/quickstart-to-dkim-sign-email-with-python/" rel="alternate"/><published>2018-06-04T09:03:00-04:00</published><updated>2018-06-04T09:03:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2018-06-04:/quickstart-to-dkim-sign-email-with-python/</id><summary type="html">&lt;p&gt;For a long time I have put off DKIM signing email sent from  my web services because I couldn't wrap my head around all the indirection Postfix requires to make it work.&lt;/p&gt;
&lt;p&gt;Honestly, I put it off for over 5 years...&lt;/p&gt;
&lt;p&gt;Today a thought sprung into my head:&lt;/p&gt;
&lt;blockquote&gt;
&amp;quot;Could I …&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;For a long time I have put off DKIM signing email sent from  my web services because I couldn't wrap my head around all the indirection Postfix requires to make it work.&lt;/p&gt;
&lt;p&gt;Honestly, I put it off for over 5 years...&lt;/p&gt;
&lt;p&gt;Today a thought sprung into my head:&lt;/p&gt;
&lt;blockquote&gt;
&amp;quot;Could I sign Email at the application level before passing to Postfix?&amp;quot;&lt;/blockquote&gt;
&lt;p&gt;As you may know, I primarily use the Python programming language, so I did some research and found a reference to a single library called &lt;tt class="docutils literal"&gt;dkimpy&lt;/tt&gt; (previously &lt;tt class="docutils literal"&gt;pydkim&lt;/tt&gt;). The codebase started over 10 years ago and appeared stable and mature.&lt;/p&gt;
&lt;p&gt;The part that sold me was that &lt;tt class="docutils literal"&gt;dkimpy&lt;/tt&gt; seemed compatible with the two Python standard library modules which I already use:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;tt class="docutils literal"&gt;email&lt;/tt&gt; which I use to prepare messages&lt;/li&gt;
&lt;li&gt;&lt;tt class="docutils literal"&gt;smtplib&lt;/tt&gt; which I use to transport messages to Postfix running on localhost&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One issue I did have with &lt;tt class="docutils literal"&gt;dkimpy&lt;/tt&gt; was the complete lack of examples or even a quickstart guide.&lt;/p&gt;
&lt;p&gt;For this reason, I have written this post!&lt;/p&gt;
&lt;div class="section" id="the-missing-dkimpy-quickstart-guide"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;The Missing dkimpy Quickstart Guide&lt;/a&gt;&lt;/h2&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;To install &lt;tt class="docutils literal"&gt;dkimpy&lt;/tt&gt; you may use &lt;tt class="docutils literal"&gt;pip&lt;/tt&gt; (&lt;tt class="docutils literal"&gt;requirements.txt&lt;/tt&gt;) or in my case I added it to my &lt;tt class="docutils literal"&gt;setup.py&lt;/tt&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Generate a public / private keypair. Don't let this step trip you up, the process easy. In a Unix -like environment you may run the following commands to create the keys.&lt;/p&gt;
&lt;p&gt;generate private key (I name my file after the domain and DKIM selector I plan to use).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;openssl&lt;span class="w"&gt; &lt;/span&gt;genrsa&lt;span class="w"&gt; &lt;/span&gt;-out&lt;span class="w"&gt; &lt;/span&gt;remarkbox.com.20180605.pem&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1024&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;generate public key from the private key.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;openssl&lt;span class="w"&gt; &lt;/span&gt;rsa&lt;span class="w"&gt; &lt;/span&gt;-in&lt;span class="w"&gt; &lt;/span&gt;remarkbox.com.20180605.pem&lt;span class="w"&gt; &lt;/span&gt;-out&lt;span class="w"&gt; &lt;/span&gt;remarkbox.com.20180605.pub&lt;span class="w"&gt; &lt;/span&gt;-pubout
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Install the public key (&lt;tt class="docutils literal"&gt;.pub&lt;/tt&gt;) as a DNS TXT record, where the record name (&amp;quot;selector&amp;quot;) is &lt;tt class="docutils literal"&gt;20180605._domainkey&lt;/tt&gt; and the value body is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nv"&gt;v&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DKIM1&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rsa&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcplYPRsqIFwXuggtH2XgQDMX+e+6sGnWdV8ld/FR9zgRAxB+DeiCEVooVvYt2JRZUEokgDFvys82Q+JTbN4qHNz19bdcBGrnTsnIFaQYpgeQYmPLdDtcWRKzTYMRNCnRmmEXyGv7WIDcaTapIq9NFgLmy1QT7ZTxuNjQtDB/2LwIDAQAB&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You may choose any selector, I happen to like to use YearMonthDay. Additionally you will substitute your public key in place of mine. Put each the line of the public key on a single line in the TXT record.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;On each of my application servers I store my private portion of my DKIM key in &lt;tt class="docutils literal"&gt;/etc/dkim/remarkbox.com.20180605.pem&lt;/tt&gt;. You may store your key any where on the filesystem that is accessible to the user or group running the application.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;This is how I used to send Email with Python:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;smtplib&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;email.mime.multipart&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MIMEMultipart&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;email.mime.text&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MIMEText&lt;/span&gt;

&lt;span class="c1"&gt;# catch socket errors when postfix isn&amp;#39;t running...&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;socket&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;socket_error&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;to_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sender_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;relay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;localhost&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MIMEMultipart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;alternative&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MIMEText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;plain&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MIMEText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;html&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Subject&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;From&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sender_email&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;To&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_email&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: react if connecting to postfix is a socket error.&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;smtplib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SMTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sendmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sender_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;to_email&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;This is how I now send DKIM signed Email with Python:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# ref: https://github.com/russellballestrini/miscutils/blob/master/miscutils/mail.py&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;dkim&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;smtplib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;email.mime.multipart&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MIMEMultipart&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;email.mime.text&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MIMEText&lt;/span&gt;
&lt;span class="c1"&gt;# catch socket errors when postfix isn&amp;#39;t running...&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;socket&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;socket_error&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;to_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sender_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;relay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;localhost&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dkim_private_key_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dkim_selector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="c1"&gt;# the `email` library assumes it is working with string objects.&lt;/span&gt;
    &lt;span class="c1"&gt;# the `dkim` library assumes it is working with byte objects.&lt;/span&gt;
    &lt;span class="c1"&gt;# this function performs the acrobatics to make them both happy.&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# needed for Python 3.&lt;/span&gt;
        &lt;span class="n"&gt;message_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message_text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# needed for Python 3.&lt;/span&gt;
        &lt;span class="n"&gt;message_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message_html&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;sender_domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sender_email&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;@&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MIMEMultipart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;alternative&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MIMEText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;plain&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MIMEText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;html&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;To&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_email&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;From&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sender_email&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Subject&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Python 3 libraries expect bytes.&lt;/span&gt;
        &lt;span class="n"&gt;msg_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Python 2 libraries expect strings.&lt;/span&gt;
        &lt;span class="n"&gt;msg_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dkim_private_key_path&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;dkim_selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# the dkim library uses regex on byte strings so everything&lt;/span&gt;
        &lt;span class="c1"&gt;# needs to be encoded from strings to bytes.&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dkim_private_key_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;dkim_private_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;To&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;From&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Subject&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dkim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;msg_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dkim_selector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sender_domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;privkey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dkim_private_key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;include_headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# add the dkim signature to the email message headers.&lt;/span&gt;
        &lt;span class="c1"&gt;# decode the signature back to string_type because later on&lt;/span&gt;
        &lt;span class="c1"&gt;# the call to msg.as_string() performs it&amp;#39;s own bytes encoding...&lt;/span&gt;
        &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DKIM-Signature&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DKIM-Signature: &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Python 3 libraries expect bytes.&lt;/span&gt;
            &lt;span class="n"&gt;msg_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Python 2 libraries expect strings.&lt;/span&gt;
            &lt;span class="n"&gt;msg_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# TODO: react if connecting to relay (localhost postfix) is a socket error.&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;smtplib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SMTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sendmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sender_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;to_email&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;msg_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;As always, leave a comment or contact me for questions or help.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-missing-dkimpy-quickstart-guide" id="toc-entry-1"&gt;The Missing dkimpy Quickstart Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Python"/></entry><entry><title>Fulfilling Childhood Dreams: Solar</title><link href="https://russell.ballestrini.net/fulfilling-childhood-dreams-solar/" rel="alternate"/><published>2018-03-16T08:03:00-04:00</published><updated>2018-03-16T08:03:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2018-03-16:/fulfilling-childhood-dreams-solar/</id><summary type="html">&lt;p&gt;Ever since I was an 8 year old boy I have wanted solar. I remember reading about the environment and alternative energy sources in a monthly &amp;quot;socal studies&amp;quot; flyer my school subscribed our classroom to. I questioned, even at my young age, why the world wasn't actively switching over to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Ever since I was an 8 year old boy I have wanted solar. I remember reading about the environment and alternative energy sources in a monthly &amp;quot;socal studies&amp;quot; flyer my school subscribed our classroom to. I questioned, even at my young age, why the world wasn't actively switching over to solar, water, and wind!&lt;/p&gt;
&lt;p&gt;25 years later I have finally fulfilled one of my childhood dreams: I installed a 11.375kWh solar system on my roof.&lt;/p&gt;
&lt;p&gt;This post will act as an overview of the stuff I learned about solar, I hope you learn something too!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Disclosure: This post contains affiliate links.&lt;/em&gt; See &lt;a class="reference external" href="/disclosures-and-terms/"&gt;full disclosure page here&lt;/a&gt;.&lt;/p&gt;
&lt;img alt="325 watt panasonic solar Panels on front of house" src="/uploads/2018/panasonic-325watt-panels-front.jpg" /&gt;
&lt;img alt="325 watt panasonic solar Panels on back of house" src="/uploads/2018/panasonic-325watt-panels-back.jpg" /&gt;
&lt;div class="section" id="solar-monitoring"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Solar Monitoring&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The system is composed 35 x Panasonic 325 watt panels (VBHN325SA16). The panels are isolated into 3 arrays (&amp;quot;strings&amp;quot;) of 8, 13, and 14.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;# 35 panels x 325 watts ~= 11.375kWh&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;35&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;.325&lt;/span&gt;
&lt;span class="mf"&gt;11.375&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each panel has an &amp;quot;optimizer&amp;quot; which communicates to the centralized SolarEdge inverter installed in my basement near the breaker box. The optimizers allow each array to work efficently even when a panel is shaded or broken.&lt;/p&gt;
&lt;p&gt;Here is the layout of the panels on my house:&lt;/p&gt;
&lt;img alt="My 11.375kWh Solar Panel Layout" src="/uploads/2018/11kWh-solar-panel-layout.png" /&gt;
&lt;p&gt;Additionally the optimizers emit energy production metrics to the inverter, which in turn forwards this data to &amp;quot;the cloud&amp;quot; (aka SolarEdge Monitoring System). From there, I can watch daily playbacks of the whole system.&lt;/p&gt;
&lt;p&gt;For example, this playback from March 19th, 2018 was a good &amp;quot;solar day&amp;quot; as my family calls:&lt;/p&gt;
&lt;img alt="My 11.375kWh Solar Playback of March 19th, 2018" src="/uploads/2018/solar-playback-2018-03-19.gif" /&gt;
&lt;p&gt;You can see how the sun rises to cover the 8 panels on the front of the house and then later moves to cover the panels on the back of the house.&lt;/p&gt;
&lt;img alt="325 watt panasonic solar Panels on back of house" src="/uploads/2018/solaredge-10k-central-inverter.jpg" /&gt;
&lt;p&gt;SolarEdge Monitoring Dashboard also keeps track of various high level metrics. For example, I input my electricity rates with date ranges and the dashboard calculates the &amp;quot;lifetime revenue&amp;quot; of my solar system. I plan to track my ROI on this number!&lt;/p&gt;
&lt;img alt="Solar panel installation overview" src="/uploads/2018/2018-03-16-solar-overview.png" /&gt;
&lt;/div&gt;
&lt;div class="section" id="home-energy-monitoring"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Home Energy Monitoring&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Most of my key insights into home energy actually come from another device I had installed called &lt;a class="reference external" href="https://www.amazon.com/gp/product/B015IY0Z3E/ref=as_li_tl?ie=UTF8&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;creativeASIN=B015IY0Z3E&amp;amp;linkCode=as2&amp;amp;tag=russellball0b-20&amp;amp;linkId=727da547a2b0a22fa53016191c2cf313"&gt;Curb Home Energy Monitor&lt;/a&gt;. Curb uses a bunch of non-invasive CT clamps on each breaker circut to gather consumption. Metrics are gathered and sent to &amp;quot;the cloud&amp;quot; and power some really cool realtime dashboards.&lt;/p&gt;
&lt;p&gt;For example, this chart shows solar production versus consumption in dollars for the last 15 days:&lt;/p&gt;
&lt;img alt="15 day solar production versus household breaker consumption" src="/uploads/2018/solar-15-day-production-consumption-in-dollars.png" /&gt;
&lt;p&gt;As you can see, about &lt;strong&gt;50% of my solar production is being consumed by heating hot water&lt;/strong&gt; for a family of 5!&lt;/p&gt;
&lt;p&gt;This feels absurd...&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; I switched to a &lt;a class="reference external" href="/hybrid-hot-water-heater-saves-69-percent-on-energy-consumption/"&gt;Hybrid Water Heater&lt;/a&gt; and wrote a post to show how I now use 69% less electricity when heating water!&lt;/p&gt;
&lt;p&gt;Anyways, here is a short video showing the Curb user interface.&lt;/p&gt;
&lt;center&gt;
&lt;video src="/uploads/2018/curb-user-interface.webm" width="620px" controls&gt;
Sorry, your browser doesn't support embedded videos,
but don't worry, you can &lt;a href="/uploads/2018/curb-user-interface.webm"&gt;download it&lt;/a&gt;
and watch it with your favorite video player!
&lt;/video&gt;
&lt;/center&gt;&lt;p&gt;The &lt;a class="reference external" href="https://www.amazon.com/gp/product/B015IY0Z3E/ref=as_li_tl?ie=UTF8&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;creativeASIN=B015IY0Z3E&amp;amp;linkCode=as2&amp;amp;tag=russellball0b-20&amp;amp;linkId=727da547a2b0a22fa53016191c2cf313"&gt;Curb&lt;/a&gt; is likely the most accurate home energy monitor on the market. The draw back is the cost of parts and labor; I used my solar installer's electrician and the total cost was $800.&lt;/p&gt;
&lt;p&gt;The competition has less accuracy but I could have likely installed a Sense myself and saved on the labor cost.&lt;/p&gt;
&lt;p&gt;Here are the two other brands I was looking at, for reference:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.amazon.com/gp/product/B075K6PHJ9/ref=as_li_tl?ie=UTF8&amp;amp;tag=russellball0b-20&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;linkCode=as2&amp;amp;creativeASIN=B075K6PHJ9&amp;amp;linkId=cc8e52d403b4b24da1f7b6a27a96ff74"&gt;Sense Home Energy Monitor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.amazon.com/gp/product/B0149EE5KS/ref=as_li_tl?ie=UTF8&amp;amp;tag=russellball0b-20&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;linkCode=as2&amp;amp;creativeASIN=B0149EE5KS&amp;amp;linkId=7e3e5d1063e980892649ea98351034bd"&gt;Neurio Home Electricity Monitor&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="section" id="solar-financing"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Solar Financing&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;When talking with Tesla, I fully expected to pay for my solar system in full with cash savings. It wasn't until I reached out to &lt;a class="reference external" href="http://sunlightsolar.com"&gt;SunLight Solar&lt;/a&gt; did I change my strategy. SunLight urged me to take advantage of the &amp;quot;.99% CT Green Bank&amp;quot; finance option. I took out a loan for the whole project and dumped my cash savings into my home mortgage as a bulk payment to the principle.&lt;/p&gt;
&lt;p&gt;Additionally, the CT Green Bank granted me $3,600 toward my project and the US federal government will grant 30% or $8,400 on my next tax return.&lt;/p&gt;
&lt;p&gt;After all the incentives, the parts and labor of my system came in just under $20,000:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$32,000 - $3,600 - 8,400 = $20,000
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Putting solar on my house actually opened up my financial options and diversified my portfolio!&lt;/p&gt;
&lt;p&gt;I now have:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;a power plant on my roof with an expected 9-10 year ROI; after 10 years I'll be generating wealth, capital, and positive &amp;quot;cash flow&amp;quot; in the form of energy&lt;/li&gt;
&lt;li&gt;paid down my 4.125% house mortgage by $35,000; saving tens of thousands over the life of the loan&lt;/li&gt;
&lt;li&gt;increased the value of my house by $20-30,000; this is an asset I can sell with or without my house&lt;/li&gt;
&lt;li&gt;shielded or insulated myself from electricity rate hikes; who knows what electricity will cost in 5 to 10 years&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="section" id="contact-me"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Contact me&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As always, please feel free to leave comments below. I live in New England so you may also &lt;a class="reference external" href="/contact/"&gt;contact me&lt;/a&gt; to setup a time to tour my setup and ask questions. I look forward to meeting you!&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#solar-monitoring" id="toc-entry-1"&gt;Solar Monitoring&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#home-energy-monitoring" id="toc-entry-2"&gt;Home Energy Monitoring&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#solar-financing" id="toc-entry-3"&gt;Solar Financing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#contact-me" id="toc-entry-4"&gt;Contact me&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Solar"/><category term="Project"/><category term="Python"/></entry><entry><title>How-to Work From Home</title><link href="https://russell.ballestrini.net/how-to-work-from-home-the-road-to-remote-chapter-1/" rel="alternate"/><published>2017-11-09T09:51:00-05:00</published><updated>2017-11-09T09:51:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2017-11-09:/how-to-work-from-home-the-road-to-remote-chapter-1/</id><summary type="html">&lt;p&gt;&amp;quot;I work from home&amp;quot; — a phrase I have uttered hundreds of times and is often met with instant amazement, envy, or jokes about pants. My goal for this book is to teach you the hacks I have learned in my career to help you land your dream job. If you …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&amp;quot;I work from home&amp;quot; — a phrase I have uttered hundreds of times and is often met with instant amazement, envy, or jokes about pants. My goal for this book is to teach you the hacks I have learned in my career to help you land your dream job. If you feel stuck in the job market and you don't want to relocate, this is the book for you.&lt;/p&gt;
&lt;div class="section" id="chapter-1-the-road-to-remote"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Chapter 1: The Road to Remote&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So you have made up your mind, you want to work from home.&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;The first step to attaining this goal is to make it a priority.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The suggestions and advice in this book come from my life experiences. Not all of my experiences will apply to you but I hope that we might find, together, at least a single thread of universal truth. In order to know, I think it makes sense to briefly review the anatomy of my life from childhood until now.&lt;/p&gt;
&lt;p&gt;I'm currently 32 years old, but I started working with computers at a young age. As an elementary schooler, I had a bright childhood friend named &lt;a class="reference external" href="https://www.youtube.com/watch?v=LlO2_GecWo8"&gt;Brian Brennan&lt;/a&gt; who although younger than I, was my first mentor regarding computers. At age 9 we played lots of video games, original playstation and Nintendo 64, as well as countless hours of computer games on his Gateway computer.&lt;/p&gt;
&lt;p&gt;At one point Brian learned how to make the computer work for him instead of other the other way around. Together we started learning HTML and Javascript. I didn't have the Internet on my family computer at the time, but my parents did have a WebTV so I started building HTML web pages for things I enjoyed and used the WebTV to upload my pages Geocities.&lt;/p&gt;
&lt;p&gt;Flash forward to age 13, I finally had dial-up. Now we didn't pay for dial-up, in fact my parents didn't know I was &amp;quot;connected&amp;quot; as we used to call it, but I was. I had the &amp;quot;setup&amp;quot; credentials for our regional phone company — I was living the dream of free Internet!&lt;/p&gt;
&lt;p&gt;I covertly routed speaker wire from our pantry closet to the computer room. Yes, I learned early on that in a pinch speaker wire can work as telephone wire, although I don't recommend it. This was one of many hacks I stumbled on. Hacks, the combination of science, trial &amp;amp; error, and creativity, are a common theme through out my life.&lt;/p&gt;
&lt;p&gt;I spent my highschool career building HTML pages instead of powerpoints slides for school projects. At home I built my own computers out of parts, as so many of us did back then and gamed all night during LAN parties where we traded &amp;quot;warez&amp;quot; and &amp;quot;pr0n&amp;quot;. I was 15 when we got an A-DSL connection (125kbps down / 15kbps up) and I started hosting my websites from my house on a Windows server hiding in my bedroom closet — later this server was reborn running FreeBSD 3.&lt;/p&gt;
&lt;p&gt;Most of my highschool education was spent figuring out how to get out of class and either into the computer lab or the library with friends. We basically met as an informal computer and tech club. For more details about this era, checkout my long time friend Charles Hooper's blog post titled &lt;a class="reference external" href="http://www.charleshooper.net/blog/how-i-hacked-my-high-school/"&gt;&amp;quot;How I hacked my high school&amp;quot;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When I was going to community college for Computer Science, I worked two part time jobs — I worked for my parents and separately at a large retail chain in the electronics department, where I slung &amp;quot;HDTVs&amp;quot;. I was working 40 hours a week, doing 4 courses a semester, and I didn't have time for anything.&lt;/p&gt;
&lt;p&gt;I started hosting my parent's small family business website which I coded by hand and I mostly resented my mall job. I hated how cruel companies were and how they actively shit on their employees. I spent my lunch breaks in Borders reading computer and business books and dreaming of my perfect job.&lt;/p&gt;
&lt;p&gt;In between Calculus and selling TVs, I also found time to build the best damn real estate search engine a local agency in South Eastern Connecticut could buy. I was taken advantage of; I worked at least 500 hours on that site which was commissioned for under $500. I did however learn a lot about business and tech. I was brave and dumb and took on more then I could chew but it was successful. Hands down it was the best solution on the market at the time, this was long before Zillow existed.&lt;/p&gt;
&lt;p&gt;While working in the retail store, I met George, an IT guy working for my region's pharmaceutical company. George referred me to a job making $18/hr migrating Windows 2000 computers to Windows XP. I accepted the contracting job thinking I was on the top of the world, I finally had a &amp;quot;real&amp;quot; computer job.&lt;/p&gt;
&lt;p&gt;I did awesome work but what I didn't know was this world is cruel to contractors. They don't get sick time, they don't get vacation, and they certainly don't get respect. Contractors are fed lies and false promises and they are the first to get cut. Managers treat contractors like physical resources. It's honestly sick.&lt;/p&gt;
&lt;p&gt;From there I moved to a technical call center helpdesk, also as a contractor, of a contractor, of a contractor of the DoD. If you follow the money, I was making $17/hr but at the top of the pyramid they were billing me out at $55/hr. After some time I managed to move onsite and successfully cut out one of the contractor layers.&lt;/p&gt;
&lt;p&gt;I still lacked negotiation skills and confidence so my increase was only a dollar, bringing me to $18/hr as a desktop support tech. I fixed computer problems whether it was hardware or software and I was fast and could basically solve anything handed to me. I quickly transitoned to a desktop support engineer role where I learned how to package and push software to small fleets of computers. A couple years later I transitioned to the Unix server team.&lt;/p&gt;
&lt;p&gt;Each step was a very big promotion as far as responsibility (I was one of two employees who designed and operated a multi million dollar Hitachi SAN) but my salary didn't increase proportionally. People were coming &amp;quot;off the street&amp;quot; for the same or lower position and making more then double I was. At the time I was working my ass off for $32k a year and hardly had enough for what my family needed. I was watching people get hired for the same job for $65k a year ... After being let down for so long, this was the tipping point, this is where I started to shift my thinking.&lt;/p&gt;
&lt;blockquote&gt;
The only way to move up is to move out.&lt;/blockquote&gt;
&lt;p&gt;I felt stuck. I worked for both the big industries for my region and they both failed me. I knew I did not want to relocate but I also didn't want to work for companies with obscene priorities and worse culture.&lt;/p&gt;
&lt;p&gt;I started moonlighting on side projects. I had so many flops and failures, to many to count. Finally I launched an idea with traction called &lt;a class="reference external" href="https://linkpeek.com"&gt;LinkPeek&lt;/a&gt;. During the code development phase of building LinkPeek I became active in the open source community for the framework I was using called &lt;a class="reference external" href="https://trypyramid.com/"&gt;Pyramid&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I was networking, even though I didn't know what it was called at the time. I taught myself some tricks to &lt;a class="reference external" href="/career-development-is-a-game-of-chutes-and-ladders/"&gt;career development&lt;/a&gt; and ended up meeting a hiring manager who appreciated my skill and was willing to take a chance with me working remote. I flew across the country to Santa Monica and had a bit of culture shock but I landed the position!&lt;/p&gt;
&lt;p&gt;This victory would be the first of 4 remote positions I have held since finally taking the leap to &amp;quot;work from home&amp;quot;.&lt;/p&gt;
&lt;p&gt;Tune in to the next chapter where I describe how I positioned myself to win the interview.&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#chapter-1-the-road-to-remote" id="toc-entry-1"&gt;Chapter 1: The Road to Remote&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Guide"/><category term="Python"/></entry><entry><title>webwords: a minimal viable web app with docker in as many languages as possible</title><link href="https://russell.ballestrini.net/webwords-is-a-minimal-viable-web-app-with-docker-in-as-many-languages-as-possible/" rel="alternate"/><published>2017-10-24T12:11:00-04:00</published><updated>2017-10-24T12:11:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2017-10-24:/webwords-is-a-minimal-viable-web-app-with-docker-in-as-many-languages-as-possible/</id><summary type="html">&lt;p&gt;&lt;strong&gt;The companion&lt;/strong&gt; &lt;a class="reference external" href="https://github.com/russellballestrini/webwords"&gt;webwords git repo&lt;/a&gt; &lt;strong&gt;lives here.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This project shows how to code the same minimal web app called &lt;tt class="docutils literal"&gt;webwords&lt;/tt&gt; in as many different programming languages as possible.
It also provides guides for building and running &lt;tt class="docutils literal"&gt;webwords&lt;/tt&gt; as a docker image.&lt;/p&gt;
&lt;div class="section" id="what-is-webwords"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;what is webwords&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A simple web application whose spec …&lt;/p&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;The companion&lt;/strong&gt; &lt;a class="reference external" href="https://github.com/russellballestrini/webwords"&gt;webwords git repo&lt;/a&gt; &lt;strong&gt;lives here.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This project shows how to code the same minimal web app called &lt;tt class="docutils literal"&gt;webwords&lt;/tt&gt; in as many different programming languages as possible.
It also provides guides for building and running &lt;tt class="docutils literal"&gt;webwords&lt;/tt&gt; as a docker image.&lt;/p&gt;
&lt;div class="section" id="what-is-webwords"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;what is webwords&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A simple web application whose spec accepts the following two query parameters —&lt;/p&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;keyword:&lt;/dt&gt;
&lt;dd&gt;The &lt;tt class="docutils literal"&gt;keyword&lt;/tt&gt; you want to search for.&lt;/dd&gt;
&lt;dt&gt;target:&lt;/dt&gt;
&lt;dd&gt;The URI &lt;tt class="docutils literal"&gt;target&lt;/tt&gt; that you want to search.&lt;/dd&gt;
&lt;/dl&gt;
&lt;p&gt;The application always returns an &lt;tt class="docutils literal"&gt;HTTP 200&lt;/tt&gt; response with the string &lt;tt class="docutils literal"&gt;true&lt;/tt&gt; or &lt;tt class="docutils literal"&gt;false&lt;/tt&gt; depending on if the keyword is found in the &lt;tt class="docutils literal"&gt;target&lt;/tt&gt; web page body.&lt;/p&gt;
&lt;p&gt;For example, to see if the word &lt;tt class="docutils literal"&gt;potato&lt;/tt&gt; exists on &lt;a class="reference external" href="https://www.remarkbox.com"&gt;Remarkbox&lt;/a&gt;, put the follwing in a browser:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;http://127.0.0.1:32779/?keyword=potato&amp;amp;target=https://www.remarkbox.com
&lt;/pre&gt;&lt;/div&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;Spoiler:&lt;/dt&gt;
&lt;dd&gt;potato does exist on Remarkbox, : )&lt;/dd&gt;
&lt;dt&gt;Note:&lt;/dt&gt;
&lt;dd&gt;You will need to replace the port of &lt;tt class="docutils literal"&gt;32779&lt;/tt&gt; with the port from the &lt;tt class="docutils literal"&gt;docker ps&lt;/tt&gt; output.&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class="section" id="why-is-webwords"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;why is webwords&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Webwords started as a programming Kata to practice writing code in different programming languages. Each port of webwords should behave the same to make comparing and functional testing simple.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;why did you choose this programming problem?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I think the spec of webwords is small enough for people new to any language to digest but complete in that it does something useful and demonstrates two common tasks: running an HTTP server and using an HTTP client.&lt;/p&gt;
&lt;p&gt;Also, I needed a way to verify if a user had possession of a domain name for the &lt;a class="reference external" href="https://www.remarkbox.com"&gt;comment service&lt;/a&gt; I'm building and chose to code this verification program as a micro service, first with Python and later with Go. The tiny end result was webwords.&lt;/p&gt;
&lt;p&gt;Shortly after, during a hackathon I used webwords to learn how to build Docker images for various languages and formalized the idea into a single project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What's next?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Webwords is for tinkering. If you want to add a version or touch up an existing version, send a PR.
Maybe a future fork will show a guide for adding a cache layer or teach how to add logging or gather metrics.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="go"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;go&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To build the docker image:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;go
docker&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;webwords-go&lt;span class="w"&gt; &lt;/span&gt;.
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To run a test container from the new image:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8888&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;webwords-go
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="python"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;python&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To build the docker image:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python
docker&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;webwords-python&lt;span class="w"&gt; &lt;/span&gt;.
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To run a test container from the new image:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8888&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;webwords-python
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="ruby"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;ruby&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To build the docker image:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ruby
docker&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;webwords-ruby&lt;span class="w"&gt; &lt;/span&gt;.
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To run a test container from the new image:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8888&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;webwords-ruby
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="js"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;js&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To build the docker image:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;js
docker&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;webwords-js&lt;span class="w"&gt; &lt;/span&gt;.
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To run a test container from the new image:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8888&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;webwords-js
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="debugging"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-7"&gt;debugging&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you're anything like me, your programs rarely compile or work properly on the first try.
Just like with programming, a docker image will rarely build correct the first time so you will need to learn how to debug.&lt;/p&gt;
&lt;p&gt;To debug, get the failed docker container's id:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;ps&lt;span class="w"&gt; &lt;/span&gt;--all
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Once you have the id, you can run the following to see the error:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;logs&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;container-id&amp;gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Debug the issue, fix your &lt;tt class="docutils literal"&gt;Dockerfile&lt;/tt&gt;, and retry the build process until you have it working.&lt;/p&gt;
&lt;p&gt;You can delete old attempts by running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;container-id&amp;gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#what-is-webwords" id="toc-entry-1"&gt;what is webwords&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#why-is-webwords" id="toc-entry-2"&gt;why is webwords&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#go" id="toc-entry-3"&gt;go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#python" id="toc-entry-4"&gt;python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#ruby" id="toc-entry-5"&gt;ruby&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#js" id="toc-entry-6"&gt;js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#debugging" id="toc-entry-7"&gt;debugging&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/><category term="Docker"/></entry><entry><title>So You're Planning a Beta Test</title><link href="https://russell.ballestrini.net/so-you-are-planning-a-beta-test/" rel="alternate"/><published>2017-10-22T13:20:00-04:00</published><updated>2017-10-22T13:20:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2017-10-22:/so-you-are-planning-a-beta-test/</id><summary type="html">&lt;p&gt;I'm running a beta for &lt;a class="reference external" href="https://www.remarkbox.com"&gt;Remarkbox&lt;/a&gt; and here is my biggest take away:&lt;/p&gt;
&lt;blockquote&gt;
Always collect information from potential customers as soon as possible.
Catch customers while they pursue a solution to their problems.&lt;/blockquote&gt;
&lt;p&gt;What do I mean?&lt;/p&gt;
&lt;p&gt;Well for starters, don't just ask for an email address, have them fill …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I'm running a beta for &lt;a class="reference external" href="https://www.remarkbox.com"&gt;Remarkbox&lt;/a&gt; and here is my biggest take away:&lt;/p&gt;
&lt;blockquote&gt;
Always collect information from potential customers as soon as possible.
Catch customers while they pursue a solution to their problems.&lt;/blockquote&gt;
&lt;p&gt;What do I mean?&lt;/p&gt;
&lt;p&gt;Well for starters, don't just ask for an email address, have them fill out a short survey right away.
If you only ask for an email, your out of luck if you need more information; people do not often respond later.&lt;/p&gt;
&lt;p&gt;I know, I know ... nobody likes to fill out surveys, so power-up their motivation with an incentive or discount. Use this survey to earn qualifed leads, remember quality over quantity.&lt;/p&gt;
&lt;p&gt;Come up with a few questions. Keep most of the questions open ended to allow them guide the dialog. Later, use the responses to break the ice when you send one-on-one emails. Each lead will present a unique situation, so take advantage of this and keep your on boarding emails personal!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; - Gather interest for the Beta as soon as possible and ask questions up front as part of the sign up.&lt;/p&gt;
</content><category term="misc"/><category term="Remarkbox"/></entry><entry><title>My Search for the Perfect Alternative Hosted Comment System</title><link href="https://russell.ballestrini.net/my-search-for-the-perfect-alternative-hosted-comment-system/" rel="alternate"/><published>2017-10-08T10:47:00-04:00</published><updated>2017-10-08T10:47:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2017-10-08:/my-search-for-the-perfect-alternative-hosted-comment-system/</id><summary type="html">&lt;p&gt;Nearly 6 years ago I launched the beta for &lt;a class="reference external" href="https://linkpeek.com"&gt;LinkPeek, a web page screenshot service&lt;/a&gt;. After all this time, I'm writing to share something new I've been passionately working on&amp;nbsp;called Remarkbox.&lt;/p&gt;
&lt;p&gt;I started working on &lt;a class="reference external" href="https://www.remarkbox.com"&gt;Remarkbox&lt;/a&gt; back in 2014 to solve a problem I had after moving my &lt;a class="reference external" href="/migrating-from-wordpress-to-pelican/"&gt;Wordpress …&lt;/a&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;Nearly 6 years ago I launched the beta for &lt;a class="reference external" href="https://linkpeek.com"&gt;LinkPeek, a web page screenshot service&lt;/a&gt;. After all this time, I'm writing to share something new I've been passionately working on&amp;nbsp;called Remarkbox.&lt;/p&gt;
&lt;p&gt;I started working on &lt;a class="reference external" href="https://www.remarkbox.com"&gt;Remarkbox&lt;/a&gt; back in 2014 to solve a problem I had after moving my &lt;a class="reference external" href="/migrating-from-wordpress-to-pelican/"&gt;Wordpress blog to a static site&lt;/a&gt;. I knew my readers would still want to communicate with me so I started the search for the &lt;strong&gt;perfect hosted comment system&lt;/strong&gt; to promote discussion.&lt;/p&gt;
&lt;p&gt;My research found solutions which would slow down my site, or worse, serve ads! That very day, I set out to build my own solution because -&lt;/p&gt;
&lt;blockquote&gt;
&amp;quot;How long could it&amp;nbsp;take to build a&amp;nbsp;comment system?&amp;quot; &amp;nbsp;...&lt;/blockquote&gt;
&lt;p&gt;3 years later, I'm finally ready to share Remarkbox: a hosted comment service that &lt;strong&gt;embeds in your pages&lt;/strong&gt; to keep the conversation in the same place as your content.&lt;/p&gt;
&lt;p&gt;Remarkbox &lt;strong&gt;increases user engagement&lt;/strong&gt; because it:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;allows a visitor to discuss your content right away without an account&lt;/li&gt;
&lt;li&gt;supports &lt;strong&gt;Markdown&lt;/strong&gt; with real-time comment previews&lt;/li&gt;
&lt;li&gt;supports deeply nested replies and has an orderly user interface&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... and the best part - fast page load speeds and&amp;nbsp;absolutely&amp;nbsp;&lt;strong&gt;NO ADS!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I'm in the process of hand selecting group of users to help me test. Would you be interested in joining a list to hear more about it?&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://www.remarkbox.com#beta"&gt;Yes! I would like to follow the Remarkbox&amp;nbsp;journey.&lt;/a&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;img alt="Remarkbox Logo" class="align-center" src="https://www.remarkbox.com/remarkbox-minified.png" style="width: 260px;" /&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Remarkbox"/></entry><entry><title>Selenium grid on Kubernetes</title><link href="https://russell.ballestrini.net/selenium-grid-on-kubernetes/" rel="alternate"/><published>2017-03-23T16:48:00-04:00</published><updated>2017-03-23T16:48:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2017-03-23:/selenium-grid-on-kubernetes/</id><summary type="html">&lt;p&gt;This post continues from where we left off on
&lt;a class="reference external" href="/minikube/"&gt;the Minikube guide&lt;/a&gt;.
If you do not already have a Kubernetes cluster, you should read that first.&lt;/p&gt;
&lt;p&gt;Selenium Grid allows you to build a cluster of Selenium nodes.
Today we will create a Selenium cluster with 1 hub and 4 nodes …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This post continues from where we left off on
&lt;a class="reference external" href="/minikube/"&gt;the Minikube guide&lt;/a&gt;.
If you do not already have a Kubernetes cluster, you should read that first.&lt;/p&gt;
&lt;p&gt;Selenium Grid allows you to build a cluster of Selenium nodes.
Today we will create a Selenium cluster with 1 hub and 4 nodes on Kubernetes.&lt;/p&gt;
&lt;div class="section" id="selenium-hub"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Selenium Hub&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Let's launch the hub:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;pods
kubectl&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;selenium-hub&lt;span class="w"&gt; &lt;/span&gt;--image&lt;span class="w"&gt; &lt;/span&gt;selenium/hub:2.53.1&lt;span class="w"&gt; &lt;/span&gt;--port&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4444&lt;/span&gt;
kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;pods
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To access the &lt;tt class="docutils literal"&gt;deployment&lt;/tt&gt; externally, we need to expose it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;services
kubectl&lt;span class="w"&gt; &lt;/span&gt;expose&lt;span class="w"&gt; &lt;/span&gt;deployment&lt;span class="w"&gt; &lt;/span&gt;selenium-hub&lt;span class="w"&gt; &lt;/span&gt;--type&lt;span class="o"&gt;=&lt;/span&gt;NodePort
kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;services
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;My &lt;tt class="docutils literal"&gt;deployment&lt;/tt&gt; was exposed here:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;http://192.168.99.100:31136/grid/console
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To find out where your &lt;tt class="docutils literal"&gt;deployment&lt;/tt&gt; was exposed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;minikube&lt;span class="w"&gt; &lt;/span&gt;service&lt;span class="w"&gt; &lt;/span&gt;selenium-hub&lt;span class="w"&gt; &lt;/span&gt;--url
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You may open this URI in a web browser.&lt;/p&gt;
&lt;div class="section" id="selenium-hub-overlearning"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Selenium Hub Overlearning&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;You can skip this section or try it for extra credit.&lt;/p&gt;
&lt;p&gt;We may use &lt;tt class="docutils literal"&gt;kubectl&lt;/tt&gt; exec for container introspection:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;selenium-hub-3216163580-7pqtx&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;ps&lt;span class="w"&gt; &lt;/span&gt;aux
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As you can see we have a &lt;tt class="docutils literal"&gt;java&lt;/tt&gt; process running&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;selenium-hub-3216163580-7pqtx&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;ps&lt;span class="w"&gt; &lt;/span&gt;aux
USER&lt;span class="w"&gt;       &lt;/span&gt;PID&lt;span class="w"&gt; &lt;/span&gt;%CPU&lt;span class="w"&gt; &lt;/span&gt;%MEM&lt;span class="w"&gt;    &lt;/span&gt;VSZ&lt;span class="w"&gt;   &lt;/span&gt;RSS&lt;span class="w"&gt; &lt;/span&gt;TTY&lt;span class="w"&gt;      &lt;/span&gt;STAT&lt;span class="w"&gt; &lt;/span&gt;START&lt;span class="w"&gt;   &lt;/span&gt;TIME&lt;span class="w"&gt; &lt;/span&gt;COMMAND
seluser&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.1&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;18044&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2688&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;?&lt;span class="w"&gt;        &lt;/span&gt;Ss&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;17&lt;/span&gt;:20&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:00&lt;span class="w"&gt; &lt;/span&gt;/bin/bash&lt;span class="w"&gt; &lt;/span&gt;/opt/bin/entry_point.sh
seluser&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.1&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2928868&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;64864&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;?&lt;span class="w"&gt;       &lt;/span&gt;Sl&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;17&lt;/span&gt;:20&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:13&lt;span class="w"&gt; &lt;/span&gt;java&lt;span class="w"&gt; &lt;/span&gt;-jar&lt;span class="w"&gt; &lt;/span&gt;/opt/selenium/selenium-server-standalone.jar&lt;span class="w"&gt; &lt;/span&gt;-role&lt;span class="w"&gt; &lt;/span&gt;hub&lt;span class="w"&gt; &lt;/span&gt;-hubConfig&lt;span class="w"&gt; &lt;/span&gt;/opt/selenium/config.json
seluser&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="m"&gt;87&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.1&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;34424&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2856&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;?&lt;span class="w"&gt;        &lt;/span&gt;Rs&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;:54&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:00&lt;span class="w"&gt; &lt;/span&gt;ps&lt;span class="w"&gt; &lt;/span&gt;aux
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We may also look at the config file and test the service internally:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# inspect the selenium config file.&lt;/span&gt;
kubectl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;selenium-hub-3216163580-7pqtx&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;/opt/selenium/config.json

&lt;span class="c1"&gt;# see if selenium is really listening on port 4444.&lt;/span&gt;
kubectl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;selenium-hub-3216163580-7pqtx&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;wget&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;127&lt;/span&gt;.0.0.1:4444&lt;span class="w"&gt; &lt;/span&gt;-O&lt;span class="w"&gt; &lt;/span&gt;-
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can even shell into the container:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-it&lt;span class="w"&gt; &lt;/span&gt;elenium-grid-3216163580-7pqtx&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;/bin/bash
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Exit out of the container and lets setup some Selenium Nodes!&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="selenium-node"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Selenium Node&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Lets spin up a Selenium Chrome node:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;pods
kubectl&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;selenium-node-chrome&lt;span class="w"&gt; &lt;/span&gt;--image&lt;span class="w"&gt; &lt;/span&gt;selenium/node-chrome:2.53.1&lt;span class="w"&gt; &lt;/span&gt;--env&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;HUB_PORT_4444_TCP_ADDR=selenium-hub&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--env&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;HUB_PORT_4444_TCP_PORT=4444&amp;quot;&lt;/span&gt;
kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;pods
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Kubernetes will use service discovery to resolve &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;selenium-hub&lt;/span&gt;&lt;/tt&gt; to the service (pods) running the hub!&lt;/p&gt;
&lt;p&gt;If you refresh the hub browser window, you should see a connected Chrome Node, like this:&lt;/p&gt;
&lt;img alt="Selenium Hub with one connected Chrome Node." src="/uploads/2017/selenium-grid-on-kubernetes.png" style="width: 500px;" /&gt;
&lt;div class="section" id="selenium-node-overlearning"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Selenium Node Overlearning&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;You can skip this section or try it for extra credit.&lt;/p&gt;
&lt;p&gt;The first time I tried to launch a Selenium node and I had trouble.&lt;/p&gt;
&lt;p&gt;I ran this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;pods
kubectl&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;selenium-node-chrome&lt;span class="w"&gt; &lt;/span&gt;--image&lt;span class="w"&gt; &lt;/span&gt;selenium/node-chrome:2.53.1
kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;pods
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The new &lt;tt class="docutils literal"&gt;pod&lt;/tt&gt; went into status &lt;tt class="docutils literal"&gt;CrashLoopBackOff&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;NAME&lt;span class="w"&gt;                                    &lt;/span&gt;READY&lt;span class="w"&gt;     &lt;/span&gt;STATUS&lt;span class="w"&gt;             &lt;/span&gt;RESTARTS&lt;span class="w"&gt;   &lt;/span&gt;AGE
selenium-grid-3216163580-7pqtx&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;/1&lt;span class="w"&gt;       &lt;/span&gt;Running&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;3d
selenium-node-chrome-4019562870-mcpfg&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;/1&lt;span class="w"&gt;       &lt;/span&gt;CrashLoopBackOff&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;6m
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To troubleshoot, I used the following commands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;describe&lt;span class="w"&gt; &lt;/span&gt;pod&lt;span class="w"&gt; &lt;/span&gt;selenium-node-chrome
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This command allowed me to review the Kubernetes level logs.
Everything seemed healthy so next I looked at the Docker level logs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;logs&lt;span class="w"&gt; &lt;/span&gt;selenium-node-chrome-4019562870-mcpfg
Not&lt;span class="w"&gt; &lt;/span&gt;linked&lt;span class="w"&gt; &lt;/span&gt;with&lt;span class="w"&gt; &lt;/span&gt;a&lt;span class="w"&gt; &lt;/span&gt;running&lt;span class="w"&gt; &lt;/span&gt;Hub&lt;span class="w"&gt; &lt;/span&gt;container
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Ok, the error &lt;tt class="docutils literal"&gt;Not linked with a running Hub container&lt;/tt&gt; appears when Selenium node cannot find the hub.&lt;/p&gt;
&lt;p&gt;Docker has a &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;--link&lt;/span&gt;&lt;/tt&gt; flag to link containers together, Kubernetes doesn't have this.
After some research, it seems &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;--link&lt;/span&gt;&lt;/tt&gt; manages ENV vars.&lt;/p&gt;
&lt;p&gt;You can see the environment vars of a &lt;tt class="docutils literal"&gt;pod&lt;/tt&gt; using this command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;selenium-hub-3216163580-7pqtx&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;printenv
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I learned that the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;selinum-node-chrome&lt;/span&gt;&lt;/tt&gt; docker image expects some ENV vars and if it doesn't get them, it goes into a crash loop.&lt;/p&gt;
&lt;p&gt;I reached out over IRC in the &lt;tt class="docutils literal"&gt;#kubernetes&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;#selenium&lt;/tt&gt; channels to ask about the ENV vars needed.
A really helpful user named &lt;cite&gt;smccarthy&lt;/cite&gt; linked me to this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;a class="reference external" href="https://github.com/kubernetes/kubernetes/tree/master/examples/selenium"&gt;https://github.com/kubernetes/kubernetes/tree/master/examples/selenium&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;Apparently one of the example Kubernetes clusters is a Selenium Grid setup!&lt;/p&gt;
&lt;p&gt;Looking over the example, I found the ENV vars that the selenium-node containers expect: &lt;tt class="docutils literal"&gt;HUB_PORT_4444_TCP_ADDR&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;HUB_PORT_4444_TCP_PORT&lt;/tt&gt;&lt;/p&gt;
&lt;p&gt;Man, why would they put the port (4444) in the key?&lt;/p&gt;
&lt;p&gt;Anyways, we pass these key/values when creating the container like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;selenium-node-chrome&lt;span class="w"&gt; &lt;/span&gt;--image&lt;span class="w"&gt; &lt;/span&gt;selenium/node-chrome:2.53.1&lt;span class="w"&gt; &lt;/span&gt;--env&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;HUB_PORT_4444_TCP_ADDR=selenium-hub&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--env&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;HUB_PORT_4444_TCP_PORT=4444&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="selenium-scale"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;Selenium Scale&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Now we can scale up and down the Selenium Grid cluster. Lets scale the &lt;tt class="docutils literal"&gt;deployment&lt;/tt&gt; to 4 &lt;tt class="docutils literal"&gt;replica&lt;/tt&gt; node-chrome &lt;tt class="docutils literal"&gt;pods&lt;/tt&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;pods
kubectl&lt;span class="w"&gt; &lt;/span&gt;scale&lt;span class="w"&gt; &lt;/span&gt;deployment&lt;span class="w"&gt; &lt;/span&gt;selenium-node-chrome&lt;span class="w"&gt; &lt;/span&gt;--replicas&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;
kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;pods
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Finally, if you refresh the hub browser window, you should see 4 connected Chrome nodes!&lt;/p&gt;
&lt;img alt="Selenium Hub with four connected Chrome Nodes." src="/uploads/2017/selenium-grid-on-kubernetes-scaled.png" style="width: 500px;" /&gt;
&lt;p&gt;If you liked this post, leave me a message in the comments!&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#selenium-hub" id="toc-entry-1"&gt;Selenium Hub&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#selenium-hub-overlearning" id="toc-entry-2"&gt;Selenium Hub Overlearning&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#selenium-node" id="toc-entry-3"&gt;Selenium Node&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#selenium-node-overlearning" id="toc-entry-4"&gt;Selenium Node Overlearning&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#selenium-scale" id="toc-entry-5"&gt;Selenium Scale&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Docker"/><category term="Kubernetes"/></entry><entry><title>Minikube</title><link href="https://russell.ballestrini.net/minikube/" rel="alternate"/><published>2017-03-21T12:38:00-04:00</published><updated>2017-03-21T12:38:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2017-03-21:/minikube/</id><summary type="html">&lt;p&gt;Minikube allows you to run a self contained, single node, Kubernetes cluster on your workstation.
Once installed and configured, you may use &lt;tt class="docutils literal"&gt;kubectl&lt;/tt&gt; to interact with it, just like a production Kubernetes cluster.&lt;/p&gt;
&lt;p&gt;Reference: &lt;a class="reference external" href="https://kubernetes.io/docs/getting-started-guides/minikube/"&gt;https://kubernetes.io/docs/getting-started-guides/minikube/&lt;/a&gt;&lt;/p&gt;
&lt;div class="section" id="install"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;install&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Requirements: Virtualbox&lt;/p&gt;
&lt;div class="section" id="install-kubectl"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;install kubectl&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Reference: &lt;a class="reference external" href="https://kubernetes.io/docs/tasks/kubectl/install/"&gt;https://kubernetes.io/docs …&lt;/a&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;Minikube allows you to run a self contained, single node, Kubernetes cluster on your workstation.
Once installed and configured, you may use &lt;tt class="docutils literal"&gt;kubectl&lt;/tt&gt; to interact with it, just like a production Kubernetes cluster.&lt;/p&gt;
&lt;p&gt;Reference: &lt;a class="reference external" href="https://kubernetes.io/docs/getting-started-guides/minikube/"&gt;https://kubernetes.io/docs/getting-started-guides/minikube/&lt;/a&gt;&lt;/p&gt;
&lt;div class="section" id="install"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;install&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Requirements: Virtualbox&lt;/p&gt;
&lt;div class="section" id="install-kubectl"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;install kubectl&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Reference: &lt;a class="reference external" href="https://kubernetes.io/docs/tasks/kubectl/install/"&gt;https://kubernetes.io/docs/tasks/kubectl/install/&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;-LO&lt;span class="w"&gt; &lt;/span&gt;https://storage.googleapis.com/kubernetes-release/release/&lt;span class="k"&gt;$(&lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;https://storage.googleapis.com/kubernetes-release/release/stable.txt&lt;span class="k"&gt;)&lt;/span&gt;/bin/darwin/amd64/kubectl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;+x&lt;span class="w"&gt; &lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="install-minikube"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;install minikube&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Reference: &lt;a class="reference external" href="https://github.com/kubernetes/minikube/releases"&gt;https://github.com/kubernetes/minikube/releases&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;-Lo&lt;span class="w"&gt; &lt;/span&gt;minikube&lt;span class="w"&gt; &lt;/span&gt;https://storage.googleapis.com/minikube/releases/v0.17.1/minikube-darwin-amd64&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;+x&lt;span class="w"&gt; &lt;/span&gt;minikube&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;minikube&lt;span class="w"&gt; &lt;/span&gt;/usr/local/bin/
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="start"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;start&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Supported hypervisors: virtualbox, vmwarefusion, kvm, xhyve&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;minikube&lt;span class="w"&gt; &lt;/span&gt;start&lt;span class="w"&gt; &lt;/span&gt;--vm-driver&lt;span class="o"&gt;=&lt;/span&gt;virtualbox
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If this is your first time starting minikube, it will perform the following:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;Starting&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Kubernetes&lt;span class="w"&gt; &lt;/span&gt;cluster...
Starting&lt;span class="w"&gt; &lt;/span&gt;VM...
Downloading&lt;span class="w"&gt; &lt;/span&gt;Minikube&lt;span class="w"&gt; &lt;/span&gt;ISO&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;89&lt;/span&gt;.24&lt;span class="w"&gt; &lt;/span&gt;MB&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;89&lt;/span&gt;.24&lt;span class="w"&gt; &lt;/span&gt;MB&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[==============================================]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;.00%&lt;span class="w"&gt; &lt;/span&gt;0s
SSH-ing&lt;span class="w"&gt; &lt;/span&gt;files&lt;span class="w"&gt; &lt;/span&gt;into&lt;span class="w"&gt; &lt;/span&gt;VM...
Setting&lt;span class="w"&gt; &lt;/span&gt;up&lt;span class="w"&gt; &lt;/span&gt;certs...
Starting&lt;span class="w"&gt; &lt;/span&gt;cluster&lt;span class="w"&gt; &lt;/span&gt;components...
Connecting&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;cluster...
Setting&lt;span class="w"&gt; &lt;/span&gt;up&lt;span class="w"&gt; &lt;/span&gt;kubeconfig...
Kubectl&lt;span class="w"&gt; &lt;/span&gt;is&lt;span class="w"&gt; &lt;/span&gt;now&lt;span class="w"&gt; &lt;/span&gt;configured&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;use&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;cluster
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Also, you should see a new VM running in VirtualBox, like this:&lt;/p&gt;
&lt;img alt="virtualbox running minikube." src="/uploads/2017/virtualbox-running-minikube.png" style="width: 350px;" /&gt;
&lt;p&gt;To verify that &lt;cite&gt;kubectl&lt;/cite&gt; is configured to use minikube look at the config file (&lt;cite&gt;~/.kube/config&lt;/cite&gt;).&lt;/p&gt;
&lt;p&gt;Also try running:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;cite&gt;kubectl get nodes&lt;/cite&gt;&lt;/li&gt;
&lt;li&gt;&lt;cite&gt;kubectl get services&lt;/cite&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also connect to the VirtualBox guest using SSH to have a look around.
In my case the Minikube VM was assigned &lt;tt class="docutils literal"&gt;192.168.99.100&lt;/tt&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;~/.minikube/machines/minikube/id_rsa&lt;span class="w"&gt; &lt;/span&gt;docker@192.168.99.100
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can see all the containers running with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;ps
ps&lt;span class="w"&gt; &lt;/span&gt;aux
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Exit out, you really don't need to interact at this level&lt;/p&gt;
&lt;p&gt;Instead we will treat Minikube as a &amp;quot;real&amp;quot; Kubernetes cluster and only use the &lt;tt class="docutils literal"&gt;kubectl&lt;/tt&gt; tool.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="demo"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;demo&lt;/a&gt;&lt;/h2&gt;
&lt;div class="section" id="create-a-deployment"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;create a deployment&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;In this example we create an echoserver cluster.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;hello-minikube&lt;span class="w"&gt; &lt;/span&gt;--image&lt;span class="o"&gt;=&lt;/span&gt;gcr.io/google_containers/echoserver:1.4&lt;span class="w"&gt; &lt;/span&gt;--port&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;this command will create -&lt;/p&gt;
&lt;p&gt;1 &lt;tt class="docutils literal"&gt;deployment&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;deployments
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;1 &lt;tt class="docutils literal"&gt;replicaset&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;replicasets
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;1 &lt;tt class="docutils literal"&gt;pod&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;pods
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To make the echoserver accessible externally, you need to &lt;tt class="docutils literal"&gt;expose&lt;/tt&gt; the &lt;tt class="docutils literal"&gt;deployment&lt;/tt&gt;, like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;expose&lt;span class="w"&gt; &lt;/span&gt;deployment&lt;span class="w"&gt; &lt;/span&gt;hello-minikube&lt;span class="w"&gt; &lt;/span&gt;--type&lt;span class="o"&gt;=&lt;/span&gt;NodePort
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The expose command creates -&lt;/p&gt;
&lt;p&gt;1 &lt;tt class="docutils literal"&gt;service&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;services
NAME&lt;span class="w"&gt;            &lt;/span&gt;CLUSTER-IP&lt;span class="w"&gt;   &lt;/span&gt;EXTERNAL-IP&lt;span class="w"&gt;   &lt;/span&gt;PORT&lt;span class="o"&gt;(&lt;/span&gt;S&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;AGE
kubernetes&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.0.0.1&lt;span class="w"&gt;     &lt;/span&gt;&amp;lt;none&amp;gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="m"&gt;443&lt;/span&gt;/TCP&lt;span class="w"&gt;          &lt;/span&gt;2d
hello-minikube&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;.0.0.225&lt;span class="w"&gt;   &lt;/span&gt;&amp;lt;nodes&amp;gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="m"&gt;8080&lt;/span&gt;:31136/TCP&lt;span class="w"&gt;   &lt;/span&gt;58m
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To access the service, you connect to the Minikube's IP address on the exposed port.&lt;/p&gt;
&lt;p&gt;In my case the Minikube VirtualBox IP is &lt;tt class="docutils literal"&gt;192.168.99.100&lt;/tt&gt; and the exposed port is &lt;tt class="docutils literal"&gt;31136&lt;/tt&gt; as listed above.&lt;/p&gt;
&lt;p&gt;The minikube tool has a shortcut for this info, try:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;minikube&lt;span class="w"&gt; &lt;/span&gt;service&lt;span class="w"&gt; &lt;/span&gt;hello-minikube&lt;span class="w"&gt; &lt;/span&gt;--url
http://192.168.99.100:31136
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Toss this into a web browser on your local machine and it should echo back!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="scale-a-deployment"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-7"&gt;scale a deployment&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Scale up the &lt;tt class="docutils literal"&gt;deployment&lt;/tt&gt; named &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;hello-minikube&lt;/span&gt;&lt;/tt&gt; by setting the number of &lt;tt class="docutils literal"&gt;replicas&lt;/tt&gt; to &lt;tt class="docutils literal"&gt;3&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;scale&lt;span class="w"&gt; &lt;/span&gt;deployment&lt;span class="w"&gt; &lt;/span&gt;hello-minikube&lt;span class="w"&gt; &lt;/span&gt;--replicas&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;verify:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;deployments
kubectl&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;pods
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="delete-a-deployment"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-8"&gt;delete a deployment&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;trash this demo (delete the &lt;tt class="docutils literal"&gt;deployment&lt;/tt&gt;, &lt;tt class="docutils literal"&gt;replicaset&lt;/tt&gt;, &lt;tt class="docutils literal"&gt;pods&lt;/tt&gt;, and &lt;tt class="docutils literal"&gt;service&lt;/tt&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;kubectl&lt;span class="w"&gt; &lt;/span&gt;delete&lt;span class="w"&gt; &lt;/span&gt;deployment&lt;span class="w"&gt; &lt;/span&gt;hello-minikube
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#install" id="toc-entry-1"&gt;install&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#install-kubectl" id="toc-entry-2"&gt;install kubectl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#install-minikube" id="toc-entry-3"&gt;install minikube&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#start" id="toc-entry-4"&gt;start&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#demo" id="toc-entry-5"&gt;demo&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#create-a-deployment" id="toc-entry-6"&gt;create a deployment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#scale-a-deployment" id="toc-entry-7"&gt;scale a deployment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#delete-a-deployment" id="toc-entry-8"&gt;delete a deployment&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Guide"/><category term="Kubernetes"/><category term="Docker"/></entry><entry><title>Nginx throw HTTP 503 maintenance JSON for all requests</title><link href="https://russell.ballestrini.net/nginx-throw-503-maintenance-json-for-all-requests/" rel="alternate"/><published>2016-12-21T15:09:00-05:00</published><updated>2016-12-21T15:09:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2016-12-21:/nginx-throw-503-maintenance-json-for-all-requests/</id><summary type="html">&lt;p&gt;I found this technique after stumbling on Aaron Parecki's blog.  You can read his post here:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://aaronparecki.com/2014/09/03/28/custom-json-503-error-page-nginx-http-content-type-headers"&gt;Setting a custom json 503 error page in nginx with proper http and content-type headers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lets pretend you have an API and you need to turn on maintenance for a major change.
All your …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I found this technique after stumbling on Aaron Parecki's blog.  You can read his post here:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://aaronparecki.com/2014/09/03/28/custom-json-503-error-page-nginx-http-content-type-headers"&gt;Setting a custom json 503 error page in nginx with proper http and content-type headers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lets pretend you have an API and you need to turn on maintenance for a major change.
All your client's (phones, web frontends) know what to do when they get an HTTP 503 Error code with JSON body.&lt;/p&gt;
&lt;p&gt;Now you want to cause all requests to get the following JSON -&lt;/p&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;/opt/maint/maint.json&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;errorCode&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;maintenance&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;errorDesc&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;We are currently performing scheduled maintenance. Please try again later.&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The nginx configuration looks like this -&lt;/p&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;maint.conf&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;listen&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="s"&gt;[::]:80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;default_server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;server_name&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/opt/maint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;add_header&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;error_page&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/maint.json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kn"&gt;location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;# all API calls will miss try_files on purpose and return custom 503.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="kn"&gt;try_files&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;All API calls will miss &lt;tt class="docutils literal"&gt;try_files&lt;/tt&gt; on purpose and return our custom 503 &lt;tt class="docutils literal"&gt;maint.json&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;Proof that this method rocks, it supports &lt;tt class="docutils literal"&gt;GET&lt;/tt&gt;, &lt;tt class="docutils literal"&gt;POST&lt;/tt&gt;, &lt;tt class="docutils literal"&gt;PUT&lt;/tt&gt;, &lt;tt class="docutils literal"&gt;UPDATE&lt;/tt&gt;, and &lt;tt class="docutils literal"&gt;DELETE&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;It also automatically serves the proper &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;Content-Type&lt;/span&gt;&lt;/tt&gt; header, in this case &lt;tt class="docutils literal"&gt;application/json&lt;/tt&gt; -&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;curl -v -X POST --data &amp;quot;param1=value1&amp;amp;param2=value2&amp;quot; http://127.0.0.1:80/my-very-favorite-api-call

&amp;gt; POST /my-very-favorite-api-call HTTP/1.1
&amp;gt; User-Agent: curl/7.29.0
&amp;gt; Host: 127.0.0.1
&amp;gt; Accept: */*
&amp;gt; Content-Length: 27
&amp;gt; Content-Type: application/x-www-form-urlencoded
&amp;gt;
* upload completely sent off: 27 out of 27 bytes
&amp;lt; HTTP/1.1 503 Service Temporarily Unavailable
&amp;lt; Server: nginx/1.10.2
&amp;lt; Date: Wed, 21 Dec 2016 20:32:44 GMT
&amp;lt; Content-Type: application/json
&amp;lt; Content-Length: 150
&amp;lt; Connection: keep-alive
&amp;lt; ETag: &amp;quot;585addfe-96&amp;quot;
&amp;lt; Retry-After: 30
&amp;lt;
{
  &amp;quot;errorCode&amp;quot;: &amp;quot;maintenance&amp;quot;,
  &amp;quot;errorDesc&amp;quot;: &amp;quot;We are currently performing scheduled maintenance. Please try again later.&amp;quot;
}
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Need to make sure you always set a custom header? Don't forget the &lt;tt class="docutils literal"&gt;always&lt;/tt&gt; directive:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
add_header Retry-After 30 always;
&lt;/pre&gt;
&lt;p&gt;Leave a comment if this helps you!&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/></entry><entry><title>Capability driven Presentation</title><link href="https://russell.ballestrini.net/capability-driven-presentation/" rel="alternate"/><published>2016-12-18T18:21:00-05:00</published><updated>2016-12-18T18:21:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2016-12-18:/capability-driven-presentation/</id><summary type="html">&lt;p&gt;A web page does not need to look the same on every browser or device.
We cannot control the capabilities of a user's browser or device.
As web designers, we have the duty to give the viewer the best experience possible.
A user will come with what they have and …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A web page does not need to look the same on every browser or device.
We cannot control the capabilities of a user's browser or device.
As web designers, we have the duty to give the viewer the best experience possible.
A user will come with what they have and we should do our best to accommodate.&lt;/p&gt;
&lt;p&gt;TL;DR: Capability drives presentation.&lt;/p&gt;
&lt;div class="section" id="resilient-web-design"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Resilient Web Design&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A resilient web page should:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;have a single canonical URI&lt;/li&gt;
&lt;li&gt;serve the same content, regardless of a viewer's capibilities&lt;/li&gt;
&lt;li&gt;use the viewer's capibilities to gracefully enhance and/or downgrade the presentation of the content&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For more background and history please read this free online book: &lt;a class="reference external" href="https://resilientwebdesign.com/"&gt;https://resilientwebdesign.com/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="techniques"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Techniques&lt;/a&gt;&lt;/h2&gt;
&lt;div class="section" id="the-js-only-class"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;The js-only class&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Purpose: hide HTML elements when Javascript does not work.&lt;/p&gt;
&lt;p&gt;In this technique we:&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;use a &lt;tt class="docutils literal"&gt;&amp;lt;noscript&amp;gt;&lt;/tt&gt; tag to nest a class named &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;js-only&lt;/span&gt;&lt;/tt&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;declare &lt;tt class="docutils literal"&gt;display: none&lt;/tt&gt; in the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;js-only&lt;/span&gt;&lt;/tt&gt; class to hide elements&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;apply the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;js-only&lt;/span&gt;&lt;/tt&gt; class to any element which should hide without js&lt;/p&gt;
&lt;p&gt;Note: The &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;js-only&lt;/span&gt;&lt;/tt&gt; class will only exists when Javascript does not work.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;noscript&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;style&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;js-only&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;style&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;noscript&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, suppose we have a notification message which clears when a user clicks 'x'.
To clear the notification, we must use a Javascript &lt;tt class="docutils literal"&gt;onclick&lt;/tt&gt; event handler.&lt;/p&gt;
&lt;p&gt;Without the Javascript capability the notification cannot clear.
Therefore, to prevent confusion we should remove the 'x' from the presentation.&lt;/p&gt;
&lt;p&gt;For example, with Javascript the notification will look like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;img alt="notification with javascript has an x" src="/uploads/2016/12/notification-with-javascript.png" /&gt;
&lt;/blockquote&gt;
&lt;p&gt;But without Javascript the 'x' is removed and the notification will look this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;img alt="notification without javascript does not have an x" src="/uploads/2016/12/notification-without-javascript.png" /&gt;
&lt;/blockquote&gt;
&lt;p&gt;To do this we assign the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;js-only&lt;/span&gt;&lt;/tt&gt; class to our label holding 'x':&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;alerts&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
{%- for message, level in request.session.pop_flash() %}
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;alert alert-{{level}}&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;onclick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;this.style.display=&amp;#39;none&amp;#39;&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;alert&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;close js-only&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;dismiss&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;dismiss&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;x&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    {{message}}
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
{%- endfor %}
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The content is the same, but we hide the interface based on the user's capability!
This small change has a huge impact on the overall user experience.&lt;/p&gt;
&lt;p&gt;We no longer present a broken button to the user, and I think that is worth the effort.&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#resilient-web-design" id="toc-entry-1"&gt;Resilient Web Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#techniques" id="toc-entry-2"&gt;Techniques&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#the-js-only-class" id="toc-entry-3"&gt;The js-only class&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/></entry><entry><title>Build RPM or DEB packages for Node.js using Jenkins and FPM</title><link href="https://russell.ballestrini.net/rpm-deb-packages-for-nodejs-using-jenkins-and-fpm/" rel="alternate"/><published>2016-12-01T15:30:00-05:00</published><updated>2016-12-01T15:30:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2016-12-01:/rpm-deb-packages-for-nodejs-using-jenkins-and-fpm/</id><summary type="html">&lt;p&gt;This blog post assumes you already have:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;a Jenkins master and none or many build servers&lt;/li&gt;
&lt;li&gt;FPM installed on the build servers&lt;/li&gt;
&lt;li&gt;Node.js installed on the build servers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I add &lt;em&gt;jenkins-build.sh&lt;/em&gt; in the root of the Node.js code repo:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# example usage: JOB_NAME=example-api BUILD_NUMBER=101 bash jenkins-build …&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;This blog post assumes you already have:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;a Jenkins master and none or many build servers&lt;/li&gt;
&lt;li&gt;FPM installed on the build servers&lt;/li&gt;
&lt;li&gt;Node.js installed on the build servers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I add &lt;em&gt;jenkins-build.sh&lt;/em&gt; in the root of the Node.js code repo:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# example usage: JOB_NAME=example-api BUILD_NUMBER=101 bash jenkins-build.sh&lt;/span&gt;

&lt;span class="c1"&gt;# download and build nodejs application and 3rd party modules.&lt;/span&gt;
npm&lt;span class="w"&gt; &lt;/span&gt;rebuild
npm&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-l

&lt;span class="nv"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;package.json&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;import sys, json; print json.load(sys.stdin)[&amp;quot;version&amp;quot;]&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# change directory down below checkout directory&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;..

&lt;span class="c1"&gt;# create an RPM using fpm.&lt;/span&gt;
&lt;span class="c1"&gt;#   JOB_NAME     = jenkins job name&lt;/span&gt;
&lt;span class="c1"&gt;#   BUILD_NUMBER = jenkins auto incremented build number&lt;/span&gt;
/usr/local/bin/fpm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;dir&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;rpm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$JOB_NAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--iteration&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$BUILD_NUMBER&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--version&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$version&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--vendor&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Example, Inc&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--rpm-user&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;node&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;--deb-user&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;node&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--rpm-group&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;node&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--deb-group&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;node&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--directories&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/opt/&lt;/span&gt;&lt;span class="nv"&gt;$JOB_NAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$JOB_NAME&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$JOB_NAME&lt;/span&gt;&lt;span class="s2"&gt;.service=/lib/systemd/system/&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$JOB_NAME&lt;/span&gt;&lt;span class="s2"&gt;=/opt/&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# make a copy of the rpm with generic name (without version or build).&lt;/span&gt;
cp&lt;span class="w"&gt; &lt;/span&gt;*.rpm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$JOB_NAME&lt;/span&gt;&lt;span class="s2"&gt;.rpm&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# mv both the rpm and genericly named rpm to the workdir so jenkins can archive it.&lt;/span&gt;
mv&lt;span class="w"&gt; &lt;/span&gt;*.rpm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$JOB_NAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then in the Jenkins job, I have the following &lt;cite&gt;execute shell&lt;/cite&gt; tasks:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# clean out old rpms.&lt;/span&gt;
rm&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;*.rpm

&lt;span class="c1"&gt;# download all 3rd party Node.js modules and package into an installable RPM.&lt;/span&gt;
bash&lt;span class="w"&gt; &lt;/span&gt;jenkins-build.sh
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I then simply upload the rpm to an S3 repo which is accessible to my API hosts.&lt;/p&gt;
&lt;p&gt;The same basic strategy may be used for other languages with subtle differences to the build script.&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/><category term="AWS"/></entry><entry><title>Register Super Powers with Pyramid add_request_method</title><link href="https://russell.ballestrini.net/register-super-powers-with-pyramid-add-request-method/" rel="alternate"/><published>2016-11-24T15:30:00-05:00</published><updated>2016-11-24T15:30:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2016-11-24:/register-super-powers-with-pyramid-add-request-method/</id><summary type="html">&lt;p&gt;The Pyramid web application framework uses a request object to hold state regarding an inbound HTTP connection.
A view must accept a request object as the first argument which makes it always available to our views and templates.&lt;/p&gt;
&lt;p&gt;This behavior rocks, but Pyramid makes it even better by allowing us …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The Pyramid web application framework uses a request object to hold state regarding an inbound HTTP connection.
A view must accept a request object as the first argument which makes it always available to our views and templates.&lt;/p&gt;
&lt;p&gt;This behavior rocks, but Pyramid makes it even better by allowing us to enrich the request object!&lt;/p&gt;
&lt;p&gt;As an example, lets pretend we want to randomly generate an integer from 1 to 999 and attach it to each request:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pyramid.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Configurator&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;random&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;randint&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;global_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot; This function returns a Pyramid WSGI application.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="c1"&gt;# build app config object from ini.&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Configurator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_random_number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;return a random number.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

   &lt;span class="c1"&gt;# register request methods.&lt;/span&gt;
   &lt;span class="c1"&gt;# each request instance will run these functions.&lt;/span&gt;
   &lt;span class="c1"&gt;# the result attaches to the request as an attribute.&lt;/span&gt;
   &lt;span class="c1"&gt;# cache the result with `reify=True` to prevent multiple computations.&lt;/span&gt;
   &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_request_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;add_random_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;random_number&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reify&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now we have access to this attribute in our views and templates:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random_number&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This strategy has a number of uses. For example, I use it to:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;attach configuration settings and secrets like API keys&lt;/li&gt;
&lt;li&gt;attach a user object, by querying the database for the user_id sourced from the cookie session&lt;/li&gt;
&lt;li&gt;analyze requests for misuse like spam or flooding&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What super powers do you register to your request? You should let the world know in the comments.&lt;/p&gt;
&lt;p&gt;Another Pyramid related post: &lt;a class="reference external" href="/sharing-a-pyramid-cookie-with-flask-or-tornado/"&gt;Sharing a Pyramid cookie with Flask or Tornado&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/></entry><entry><title>Sharing a Pyramid cookie with Flask or Tornado</title><link href="https://russell.ballestrini.net/sharing-a-pyramid-cookie-with-flask-or-tornado/" rel="alternate"/><published>2016-11-21T18:35:00-05:00</published><updated>2016-11-21T18:35:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2016-11-21:/sharing-a-pyramid-cookie-with-flask-or-tornado/</id><summary type="html">&lt;p&gt;Do you have a Pyramid application which authenticates users and uses a signed cookie as a session?
Do you want to build a microservice using another framework and allow it to use the same cookie and session? Me too!&lt;/p&gt;
&lt;p&gt;First we will review a bit of Pyramid code which describes …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Do you have a Pyramid application which authenticates users and uses a signed cookie as a session?
Do you want to build a microservice using another framework and allow it to use the same cookie and session? Me too!&lt;/p&gt;
&lt;p&gt;First we will review a bit of Pyramid code which describes the cookie session.&lt;/p&gt;
&lt;div class="section" id="setup-pyramid-signed-cookie-session"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Setup Pyramid Signed Cookie Session&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;At the top of your &lt;tt class="docutils literal"&gt;__init__.py&lt;/tt&gt; you will have the following import:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# cookie only session, not encrypted but signed to prevent tampering!&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pyramid.session&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SignedCookieSessionFactory&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In the &lt;tt class="docutils literal"&gt;main()&lt;/tt&gt; function of &lt;tt class="docutils literal"&gt;__init__.py&lt;/tt&gt; you will create a &lt;tt class="docutils literal"&gt;Configurator&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;global_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot; This function returns a Pyramid WSGI application.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="c1"&gt;# setup session factory to use unencrypted but signed cookies.&lt;/span&gt;
    &lt;span class="n"&gt;session_factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SignedCookieSessionFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;test-secret&amp;#39;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# build app config object from ini.&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Configurator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;session_factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session_factory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As you can see for this test example the &lt;tt class="docutils literal"&gt;secret&lt;/tt&gt; is hardcoded. A real application would define the secret in the .ini and access it using the &lt;tt class="docutils literal"&gt;settings&lt;/tt&gt; object.&lt;/p&gt;
&lt;p&gt;Now all new requests will have a &lt;tt class="docutils literal"&gt;request.session&lt;/tt&gt; attribute.&lt;/p&gt;
&lt;p&gt;You use it like a dictionary, for example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# set some data into the cookie. for me, this logs the user in.&lt;/span&gt;
&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;authenticated_user_id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;

&lt;span class="c1"&gt;# get some data from the cookie. for me this gets the user_id or None.&lt;/span&gt;
&lt;span class="n"&gt;authenticated_user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;authenticated_user_id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# delete a key / value from cookie. for me this will log out the user.&lt;/span&gt;
&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;authenticated_user_id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Reference: &lt;a class="reference external" href="https://docs.pylonsproject.org/projects/pyramid/en/latest/api/session.html#pyramid.session.SignedCookieSessionFactory"&gt;SignedCookieSessionFactory&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="teach-tornado-how-to-read-pyramid-cookies"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Teach Tornado how to read Pyramid cookies&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In this section I'll show you how to access and deserialize the Pyramid cookie from a Tornado application.&lt;/p&gt;
&lt;p&gt;To do this, I'm going to extend the Tornado &lt;tt class="docutils literal"&gt;Hello, world&lt;/tt&gt; application:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;tornado.ioloop&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;tornado.web&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tornado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Hello, world&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_app&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tornado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MainHandler&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_app&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8888&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tornado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ioloop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IOLoop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is a simple application which listens to port 8888 and serves the text &lt;tt class="docutils literal"&gt;Hello, world&lt;/tt&gt; when &lt;tt class="docutils literal"&gt;/&lt;/tt&gt; is requested.&lt;/p&gt;
&lt;p&gt;Add the following imports:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# accessing Pyramid cookies.&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;webob.cookies&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SignedSerializer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pyramid.session&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PickleSerializer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pyramid.compat&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bytes_&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For testing purposes, create a global &lt;tt class="docutils literal"&gt;serializer&lt;/tt&gt; object:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# https://docs.webob.org/en/stable/api/cookies.html#webob.cookies.SignedSerializer&lt;/span&gt;
&lt;span class="n"&gt;serializer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SignedSerializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;test-secret&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;pyramid.session.&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PickleSerializer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Adjust the &lt;tt class="docutils literal"&gt;get&lt;/tt&gt; method in the &lt;tt class="docutils literal"&gt;MainHandler&lt;/tt&gt; to look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;raw_cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;session&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;raw_cookie&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;session_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_cookie&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;lt;br/&amp;gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Hello, world&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The complete program follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;tornado.ioloop&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;tornado.web&lt;/span&gt;

&lt;span class="c1"&gt;# accessing Pyramid cookies.&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;webob.cookies&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SignedSerializer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pyramid.session&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PickleSerializer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pyramid.compat&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bytes_&lt;/span&gt;

&lt;span class="c1"&gt;# https://docs.webob.org/en/stable/api/cookies.html#webob.cookies.SignedSerializer&lt;/span&gt;
&lt;span class="n"&gt;serializer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SignedSerializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;test-secret&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;pyramid.session.&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PickleSerializer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tornado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;raw_cookie&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_cookie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;session&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;raw_cookie&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;session_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serializer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_cookie&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;lt;br/&amp;gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Hello, world&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_app&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tornado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MainHandler&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_app&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8888&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tornado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ioloop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IOLoop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Again, we are hardcoding the same &lt;tt class="docutils literal"&gt;secret&lt;/tt&gt;. If you set everything up properly, loading &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;http://127.0.0.1:8888&lt;/span&gt;&lt;/tt&gt; in a web browser should print the cookie session_data in plain-text.&lt;/p&gt;
&lt;p&gt;In my testing, I saw my cookie and it looked like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1479520270&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1479516714.062414&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;authenticated_user_id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;nodes_pending_verify&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]})&lt;/span&gt;
&lt;span class="n"&gt;Hello&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Thats all for now, let me know what you think in the comments!&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#setup-pyramid-signed-cookie-session" id="toc-entry-1"&gt;Setup Pyramid Signed Cookie Session&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#teach-tornado-how-to-read-pyramid-cookies" id="toc-entry-2"&gt;Teach Tornado how to read Pyramid cookies&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Python"/><category term="Salt"/></entry><entry><title>My first Systemd Service Script and Override</title><link href="https://russell.ballestrini.net/my-first-systemd-service-script-and-override/" rel="alternate"/><published>2016-10-28T15:54:00-04:00</published><updated>2016-10-28T15:54:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2016-10-28:/my-first-systemd-service-script-and-override/</id><summary type="html">&lt;p&gt;At work we mostly run Centos and I have some NodeJS services to deploy.
I feel most familiar with Ubuntu / Upstart so this post serves as my notes on systemd.&lt;/p&gt;
&lt;p&gt;In this contrived example, we define a service for our &lt;cite&gt;taco-api&lt;/cite&gt; application.
The &lt;cite&gt;taco-api&lt;/cite&gt; source code lives in &lt;cite&gt;/opt/taco-api …&lt;/cite&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;At work we mostly run Centos and I have some NodeJS services to deploy.
I feel most familiar with Ubuntu / Upstart so this post serves as my notes on systemd.&lt;/p&gt;
&lt;p&gt;In this contrived example, we define a service for our &lt;cite&gt;taco-api&lt;/cite&gt; application.
The &lt;cite&gt;taco-api&lt;/cite&gt; source code lives in &lt;cite&gt;/opt/taco-api&lt;/cite&gt;.&lt;/p&gt;
&lt;p&gt;We manage a static &lt;cite&gt;.service&lt;/cite&gt; file using a package or config management.&lt;/p&gt;
&lt;p&gt;&lt;cite&gt;[/usr]/lib/systemd/system/taco-api.service&lt;/cite&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;[Unit]&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Node.js service for taco API&lt;/span&gt;

&lt;span class="k"&gt;[Service]&lt;/span&gt;

&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;NODE_ENV=development&lt;/span&gt;

&lt;span class="na"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/node /opt/taco-api/app.js&lt;/span&gt;

&lt;span class="c1"&gt;# Restart service after all crashes but wait 10 seconds between restarts.&lt;/span&gt;
&lt;span class="na"&gt;Restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="na"&gt;RestartSec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;10&lt;/span&gt;

&lt;span class="c1"&gt;# output stdout and stderr to syslog. (/var/log/[syslog|messages])&lt;/span&gt;
&lt;span class="na"&gt;StandardOutput&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;syslog&lt;/span&gt;
&lt;span class="na"&gt;StandardError&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;syslog&lt;/span&gt;
&lt;span class="na"&gt;SyslogIdentifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;%N&lt;/span&gt;

&lt;span class="c1"&gt;# define the user and group to own the process.&lt;/span&gt;
&lt;span class="c1"&gt;#User=node&lt;/span&gt;
&lt;span class="c1"&gt;#Group=node&lt;/span&gt;

&lt;span class="c1"&gt;# change directory before running ExecStart command.&lt;/span&gt;
&lt;span class="na"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/opt/taco-api&lt;/span&gt;

&lt;span class="k"&gt;[Install]&lt;/span&gt;
&lt;span class="na"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We also manage a static &lt;cite&gt;override.conf&lt;/cite&gt; file using config management.
In this file, we customize the environment variables present.&lt;/p&gt;
&lt;p&gt;&lt;cite&gt;/etc/systemd/system/taco-api.service.d/override.conf&lt;/cite&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;[Service]&lt;/span&gt;
&lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;NODE_ENV=production&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This allows us to only override and keep track of the deltas.&lt;/p&gt;
&lt;p&gt;You can test, like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# emit the status of the service.&lt;/span&gt;
service&lt;span class="w"&gt; &lt;/span&gt;taco-api&lt;span class="w"&gt; &lt;/span&gt;status

&lt;span class="c1"&gt;# start the service.&lt;/span&gt;
service&lt;span class="w"&gt; &lt;/span&gt;taco-api&lt;span class="w"&gt; &lt;/span&gt;start

&lt;span class="c1"&gt;# look at the process list, note that node is running.&lt;/span&gt;
ps&lt;span class="w"&gt; &lt;/span&gt;aux&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;node

&lt;span class="c1"&gt;# emit the status of the service.&lt;/span&gt;
service&lt;span class="w"&gt; &lt;/span&gt;taco-api&lt;span class="w"&gt; &lt;/span&gt;status

&lt;span class="c1"&gt;# start the service.&lt;/span&gt;
service&lt;span class="w"&gt; &lt;/span&gt;taco-api&lt;span class="w"&gt; &lt;/span&gt;stop

&lt;span class="c1"&gt;# look at the process list, note that node is not running.&lt;/span&gt;
ps&lt;span class="w"&gt; &lt;/span&gt;aux&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;node
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Thank you!&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/></entry><entry><title>Set static hostname for RHEL Centos 7 on AWS</title><link href="https://russell.ballestrini.net/set-static-hostname-for-rhel-centos-7-on-aws/" rel="alternate"/><published>2016-09-06T11:18:00-04:00</published><updated>2016-09-06T11:18:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2016-09-06:/set-static-hostname-for-rhel-centos-7-on-aws/</id><content type="html">&lt;p&gt;This took me about 2 hours to figure out, hopefully it saves you time.&lt;/p&gt;
&lt;p&gt;/etc/sysconfig/network:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
HOSTNAME=desired-hostname.example.com
&lt;/pre&gt;
&lt;p&gt;/etc/hostname:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
desired-hostname.example.com
&lt;/pre&gt;
&lt;p&gt;/etc/cloud/cloud.cfg:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
preserve_hostname: true
&lt;/pre&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="AWS"/></entry><entry><title>If I swallow lots of air will I be lighter?</title><link href="https://russell.ballestrini.net/if-i-swallow-lots-of-air-will-i-be-lighter/" rel="alternate"/><published>2016-06-22T19:17:00-04:00</published><updated>2016-06-22T19:17:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2016-06-22:/if-i-swallow-lots-of-air-will-i-be-lighter/</id><summary type="html">&lt;p&gt;After dinner tonight, Carter, my four year old asked me, &amp;quot;If I swallow lots of air will I be lighter?&amp;quot;. I thought about the question for a moment and then told him it depends on what surrounds you.&lt;/p&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;If you are surrounded by:&lt;/dt&gt;
&lt;dd&gt;&lt;ul class="first last simple"&gt;
&lt;li&gt;air and you swallow lots of air …&lt;/li&gt;&lt;/ul&gt;&lt;/dd&gt;&lt;/dl&gt;</summary><content type="html">&lt;p&gt;After dinner tonight, Carter, my four year old asked me, &amp;quot;If I swallow lots of air will I be lighter?&amp;quot;. I thought about the question for a moment and then told him it depends on what surrounds you.&lt;/p&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;If you are surrounded by:&lt;/dt&gt;
&lt;dd&gt;&lt;ul class="first last simple"&gt;
&lt;li&gt;air and you swallow lots of air you will not be lighter.&lt;/li&gt;
&lt;li&gt;water and and swallow lots of air you will be lighter, relative to the water, and you will float better.&lt;/li&gt;
&lt;li&gt;helium and you swallow lots of air you will become heavier, relative to the surrounding helium.&lt;/li&gt;
&lt;/ul&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;p&gt;Whaaaaaat???!!?!1!&lt;/p&gt;
&lt;p&gt;: )&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/></entry><entry><title>Output all instance identifiers of an AWS VPC to JSON</title><link href="https://russell.ballestrini.net/output-all-instance-identifiers-of-an-aws-vpc-to-json/" rel="alternate"/><published>2016-06-21T10:25:00-04:00</published><updated>2016-06-21T10:25:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2016-06-21:/output-all-instance-identifiers-of-an-aws-vpc-to-json/</id><summary type="html">&lt;p&gt;At work today I needed an easy way to collect private IP addresses of every instance in one of our production VPCs.&lt;/p&gt;
&lt;p&gt;I ended up adding a tool to &lt;a class="reference external" href="https://botoform.com"&gt;https://botoform.com&lt;/a&gt; to perform this task.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;bf&lt;span class="w"&gt; &lt;/span&gt;--profile&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;aws_profile&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;dump&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;vpc_name_tag&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;instances&lt;span class="w"&gt; &lt;/span&gt;--output-format&lt;span class="w"&gt; &lt;/span&gt;json
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;bf&lt;span class="w"&gt; &lt;/span&gt;--profile&lt;span class="w"&gt; &lt;/span&gt;customer3&lt;span class="w"&gt; &lt;/span&gt;dump …&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;At work today I needed an easy way to collect private IP addresses of every instance in one of our production VPCs.&lt;/p&gt;
&lt;p&gt;I ended up adding a tool to &lt;a class="reference external" href="https://botoform.com"&gt;https://botoform.com&lt;/a&gt; to perform this task.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;bf&lt;span class="w"&gt; &lt;/span&gt;--profile&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;aws_profile&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;dump&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;vpc_name_tag&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;instances&lt;span class="w"&gt; &lt;/span&gt;--output-format&lt;span class="w"&gt; &lt;/span&gt;json
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;bf&lt;span class="w"&gt; &lt;/span&gt;--profile&lt;span class="w"&gt; &lt;/span&gt;customer3&lt;span class="w"&gt; &lt;/span&gt;dump&lt;span class="w"&gt; &lt;/span&gt;prd&lt;span class="w"&gt; &lt;/span&gt;instances&lt;span class="w"&gt; &lt;/span&gt;--output-format&lt;span class="w"&gt; &lt;/span&gt;json&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;~/customer3-prd-instances.json
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Checkout the &lt;a class="reference external" href="https://botoform.readthedocs.io/en/latest/guides/quickstart.html"&gt;Botoform Quickstart&lt;/a&gt;
for installation instructions.&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="AWS"/></entry><entry><title>Set DNS resolver options</title><link href="https://russell.ballestrini.net/set-dns-resolver-options/" rel="alternate"/><published>2015-12-16T10:17:00-05:00</published><updated>2015-12-16T10:17:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-12-16:/set-dns-resolver-options/</id><summary type="html">&lt;p class="first last"&gt;The right way to persist resolver options across many Linux distributions.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;I desire the following options in &lt;cite&gt;/etc/resolv.conf&lt;/cite&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;rotate&lt;span class="w"&gt; &lt;/span&gt;timeout:1&lt;span class="w"&gt; &lt;/span&gt;attempts:1
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="section" id="ubuntu-or-debian"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Ubuntu or Debian&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For Ubuntu or Debian based systems, place the options in
&lt;cite&gt;/etc/resolvconf/resolv.conf.d/base&lt;/cite&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;rotate&lt;span class="w"&gt; &lt;/span&gt;timeout:1&lt;span class="w"&gt; &lt;/span&gt;attempts:1
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;These options merge with the options from DHCP and end up in &lt;cite&gt;/etc/resolv.conf&lt;/cite&gt;.&lt;/p&gt;
&lt;p&gt;Then restart the host, or at least networking.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="redhat-or-centos-or-fedora"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Redhat or Centos or Fedora&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;For Redhat, Centos or Fedora, add the following to &lt;cite&gt;/etc/sysconfig/network&lt;/cite&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nv"&gt;RES_OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;rotate timeout:1 attempts:1&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then restart the host, or at least networking.&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#ubuntu-or-debian" id="toc-entry-1"&gt;Ubuntu or Debian&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#redhat-or-centos-or-fedora" id="toc-entry-2"&gt;Redhat or Centos or Fedora&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/></entry><entry><title>Bind9 on Joyent Triton</title><link href="https://russell.ballestrini.net/bind9-on-joyent-triton/" rel="alternate"/><published>2015-11-29T18:47:00-05:00</published><updated>2015-11-29T18:47:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-11-29:/bind9-on-joyent-triton/</id><summary type="html">&lt;p class="first last"&gt;Tricks to reduce Bind9's large memory footprint on Joyent Triton.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;I have a single DNS server running on a KVM at DigitalOcean for $5/mo. I might move to Joyent and use 2 x 128M Ubuntu containers for $2.23/mo each.&lt;/p&gt;
&lt;p&gt;In my first test Bind9 (named RNDC) ended up using 111M of memory on a 256M Ubuntu Triton container.&lt;/p&gt;
&lt;p&gt;The large 111M memory footprint correlated with the amount of worker threads running. Bind9 determines the number of worker threads to manage by the number of CPUs detected by the OS.&lt;/p&gt;
&lt;p&gt;In my case, Ubuntu detected 48 CPUs because Joyent containers run on bare-metal.&lt;/p&gt;
&lt;p&gt;We can see this by running the following commands:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;/proc/cpuinfo&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;processor&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;wc&lt;span class="w"&gt; &lt;/span&gt;-l
&lt;span class="m"&gt;48&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;rndc&lt;span class="w"&gt; &lt;/span&gt;status
version:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;9&lt;/span&gt;.9.5-3ubuntu0.5-Ubuntu&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;id:f9b8a50e&amp;gt;
CPUs&lt;span class="w"&gt; &lt;/span&gt;found:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;48&lt;/span&gt;
worker&lt;span class="w"&gt; &lt;/span&gt;threads:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;48&lt;/span&gt;
UDP&lt;span class="w"&gt; &lt;/span&gt;listeners&lt;span class="w"&gt; &lt;/span&gt;per&lt;span class="w"&gt; &lt;/span&gt;interface:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
number&lt;span class="w"&gt; &lt;/span&gt;of&lt;span class="w"&gt; &lt;/span&gt;zones:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;216&lt;/span&gt;
debug&lt;span class="w"&gt; &lt;/span&gt;level:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
xfers&lt;span class="w"&gt; &lt;/span&gt;running:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
xfers&lt;span class="w"&gt; &lt;/span&gt;deferred:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
soa&lt;span class="w"&gt; &lt;/span&gt;queries&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;progress:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
query&lt;span class="w"&gt; &lt;/span&gt;logging&lt;span class="w"&gt; &lt;/span&gt;is&lt;span class="w"&gt; &lt;/span&gt;OFF
recursive&lt;span class="w"&gt; &lt;/span&gt;clients:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;/0/1000
tcp&lt;span class="w"&gt; &lt;/span&gt;clients:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;/100
server&lt;span class="w"&gt; &lt;/span&gt;is&lt;span class="w"&gt; &lt;/span&gt;up&lt;span class="w"&gt; &lt;/span&gt;and&lt;span class="w"&gt; &lt;/span&gt;running
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To fix this, at least on Ubuntu, we need to pass &lt;cite&gt;-n 2&lt;/cite&gt; to limit the &lt;cite&gt;worker threads&lt;/cite&gt; to 2.&lt;/p&gt;
&lt;p&gt;For Ubuntu, edit &lt;cite&gt;/etc/default/bind9&lt;/cite&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nv"&gt;OPTIONS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;-u bind -n 2&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then restart the &lt;cite&gt;bind9&lt;/cite&gt; service and verify using &lt;cite&gt;sudo rndc status&lt;/cite&gt; and &lt;cite&gt;free -m&lt;/cite&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;service&lt;span class="w"&gt; &lt;/span&gt;bind9&lt;span class="w"&gt; &lt;/span&gt;restart
sudo&lt;span class="w"&gt; &lt;/span&gt;rndc&lt;span class="w"&gt; &lt;/span&gt;status
free&lt;span class="w"&gt; &lt;/span&gt;-m
&lt;/pre&gt;&lt;/div&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Docker"/></entry><entry><title>Migrating from WordPress to Pelican</title><link href="https://russell.ballestrini.net/migrating-from-wordpress-to-pelican/" rel="alternate"/><published>2015-11-15T15:39:00-05:00</published><updated>2015-11-15T15:39:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-11-15:/migrating-from-wordpress-to-pelican/</id><summary type="html">&lt;p class="first last"&gt;Five hints to save time during your migration from WordPress.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;Its finally happening. I'm moving this blog from WordPress to Pelican.
This task has persisted on my TODO list for over two years.&lt;/p&gt;
&lt;p&gt;During the process of the move, I'm going to use this post to dump hints:&lt;/p&gt;
&lt;div class="section" id="wordpress-xml-to-json"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;WordPress XML to JSON&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I wrote &lt;a class="reference external" href="https://github.com/russellballestrini/wordpress-xml-to-json"&gt;this tool to convert Wordpress XML dumps to JSON&lt;/a&gt;.
The tool is opinionated and removes lots of data.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="pelican-import"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Pelican-import&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A tool to convert WordPress .xml into .rst or .md files (ReStructuredText or MarkDown) is
&lt;a class="reference external" href="https://docs.getpelican.com/en/latest/importer.html"&gt;pelican-import&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I suggest checking it out, even if you do not plan to use Pelican as your static site generator.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="add-date-to-post-filenames"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Add date to post filenames&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;After using &lt;a class="reference external" href="https://docs.getpelican.com/en/latest/importer.html"&gt;pelican-import&lt;/a&gt; I had about 150 &lt;cite&gt;.rst&lt;/cite&gt; files and I decided to put the date in the filename, so I wrote this short bash script tool to do the renames:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;ls&lt;span class="w"&gt; &lt;/span&gt;*.rst&lt;span class="sb"&gt;`&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;file&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$files&lt;/span&gt;:
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;the_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:date:&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;awk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{ print $2; }&amp;#39;&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$the_date&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="alter-category-to-tags"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Alter category to tags&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;category and tags have different meanings and assumptions between wordpress and pelican.  As a result I decided to change all my categories to tags using this command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s/:category:/:tags:/g&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;*.rst
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="alter-attachment-and-image-paths"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;Alter attachment and image paths&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;fix paths to images / uploads to remove wp-content:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s/\/wp-content//g&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;*.rst
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#wordpress-xml-to-json" id="toc-entry-1"&gt;WordPress XML to JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#pelican-import" id="toc-entry-2"&gt;Pelican-import&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#add-date-to-post-filenames" id="toc-entry-3"&gt;Add date to post filenames&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#alter-category-to-tags" id="toc-entry-4"&gt;Alter category to tags&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#alter-attachment-and-image-paths" id="toc-entry-5"&gt;Alter attachment and image paths&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/></entry><entry><title>Boto3 get main route table</title><link href="https://russell.ballestrini.net/boto3-get-main-route-table/" rel="alternate"/><published>2015-10-16T12:15:00-04:00</published><updated>2015-10-16T12:15:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-10-16:/boto3-get-main-route-table/</id><summary type="html">&lt;p class="first last"&gt;Library work around.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;While developing Botoform I ran into an issue with Boto3 where I
couldn't easily get the &amp;quot;main&amp;quot; route table of a VPC. I ended up adding a
&lt;a class="reference external" href="https://github.com/russellballestrini/botoform/blob/master/botoform/enriched/vpc.py"&gt;get_main_route_table&lt;/a&gt;
method to do the duty.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_main_route_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Return the main (default) route table for VPC.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;main_route_table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;route_table&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;route_tables&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;association&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route_table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;associations&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;association&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;main_route_table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route_table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main_route_table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;cannot get main route table! &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main_route_table&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;main_route_table&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That all for now!&lt;/p&gt;
&lt;p&gt;You should read my other Boto related posts for tricks to impress your friends.  : )&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="/setting-region-programmatically-in-boto3/"&gt;Setting region programmatically in Boto3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="/filtering-aws-resources-with-boto3/"&gt;Filtering AWS resources with Boto3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/><category term="AWS"/></entry><entry><title>List all installed package names in Python</title><link href="https://russell.ballestrini.net/list-all-installed-package-names-in-python/" rel="alternate"/><published>2015-07-04T22:15:00-04:00</published><updated>2015-07-04T22:15:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-07-04:/list-all-installed-package-names-in-python/</id><summary type="html">&lt;p class="first last"&gt;You only need a two lines of code to access package names and versions.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;Here we define a function called &lt;cite&gt;pkgs&lt;/cite&gt;.
This function returns a list of package resource objects.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;pkgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;__import__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;pkg_resources&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;working_set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We then define two more functions: &lt;cite&gt;pkg_names&lt;/cite&gt; &amp;amp; &lt;cite&gt;pkg_versions&lt;/cite&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;pkg_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;

&lt;span class="n"&gt;pkg_versions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;==&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pkgs&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Last we show how to use invoke these functions:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pkg_names&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ansible&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;pycrypto&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;PyYAML&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Jinja2&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;...truncated...&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;virt-back&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Werkzeug&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;xmltodict&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pkg_versions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ansible==1.7&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;pycrypto==2.6.1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;...truncated...&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;virt-back==0.1.0&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;xmltodict==0.9.2&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/><category term="AWS"/></entry><entry><title>Filtering AWS resources with Boto3</title><link href="https://russell.ballestrini.net/filtering-aws-resources-with-boto3/" rel="alternate"/><published>2015-07-02T17:03:00-04:00</published><updated>2015-07-02T17:03:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-07-02:/filtering-aws-resources-with-boto3/</id><summary type="html">&lt;p class="first last"&gt;References to take you from filtering novice to expert.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;This post will be updated frequently when as I learn more about how to
filter AWS resources using Boto3 library.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Filtering VPCs by tags&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In this example we want to filter a particular VPC by the &amp;quot;Name&amp;quot; tag
with the value of 'webapp01'.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; import boto3
&amp;gt;&amp;gt;&amp;gt; boto3.setup_default_session(profile_name='project1')
&amp;gt;&amp;gt;&amp;gt; ec2 = boto3.resource('ec2', region_name='us-west-2')
&amp;gt;&amp;gt;&amp;gt; filters = [{'Name':'tag:Name', 'Values':['webapp01']}]
&amp;gt;&amp;gt;&amp;gt; webapp01 = list(ec2.vpcs.filter(Filters=filters))[0]
&amp;gt;&amp;gt;&amp;gt; webapp01.vpc_id
'vpc-11111111'
&lt;/pre&gt;
&lt;p&gt;You can also filter on the value of the 'tag-key' or the 'tag-value'
like so:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; taco_key_filter = [{'Name':'tag-key', 'Values':['taco']}]
&amp;gt;&amp;gt;&amp;gt; nacho_value_filter = [{'Name':'tag-value', 'Values':['nacho']}]
&lt;/pre&gt;
&lt;p&gt;You can also filter on multiple 'Values'. In this example want 2 VPCs
named 'webapp01' and 'webapp02':&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; filters = [{'Name':'tag:Name', 'Values':['webapp01','webapp02']}]
&amp;gt;&amp;gt;&amp;gt; list(ec2.vpcs.filter(Filters=filters))
[ec2.Vpc(id='vpc-11111111'), ec2.Vpc(id='vpc-22222222')]
&lt;/pre&gt;
&lt;p&gt;You can also use the '*' wildcard to glob up results in your filter. In
this example we want all 3 VPCs named 'webapp01', 'webapp02' and
'webapp03':&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; filters = [{'Name':'tag:Name', 'Values':['webapp*']}]
&amp;gt;&amp;gt;&amp;gt; list(ec2.vpcs.filter(Filters=filters))
[ec2.Vpc(id='vpc-11111111'), ec2.Vpc(id='vpc-22222222'), ec2.Vpc(id='vpc-33333333')]
&lt;/pre&gt;
&lt;p&gt;Thats all for now!&lt;/p&gt;
&lt;p&gt;You should read my other Boto related posts for tricks to impress your friends.  : )&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="/setting-region-programmatically-in-boto3/"&gt;Setting region programmatically in Boto3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="/working-with-botocores-awsconfig/"&gt;Working with botocores AWS config&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="AWS"/></entry><entry><title>Working with botocore's ~/.aws/config</title><link href="https://russell.ballestrini.net/working-with-botocores-awsconfig/" rel="alternate"/><published>2015-07-01T18:14:00-04:00</published><updated>2015-07-01T18:14:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-07-01:/working-with-botocores-awsconfig/</id><summary type="html">&lt;p class="first last"&gt;Don't reinvent the wheel, use Botocores Config facilities for work with AWS.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;I ran into a &lt;a class="reference external" href="https://github.com/boto/botocore/issues/435"&gt;bug&lt;/a&gt; in
botocore and this post will serve to document a work around as well as
show how to use botocore session object to work with the values stored
in ~/.aws/config.&lt;/p&gt;
&lt;p&gt;Pretend you have an aws config with two accounts for two separate
projects, like so:&lt;/p&gt;
&lt;p&gt;*~/.aws/config:*&lt;/p&gt;
&lt;pre class="literal-block"&gt;
[profile project1]
account_id = 111111111111
aws_access_key_id=THISISNOTMYACCESSKEY1
aws_secret_access_key=THISISNOTMYSECRETKEY1
# Optional, to define default region for this profile.
region=us-west-1

[profile project2]
account_id = 222222222222
aws_access_key_id=THISISNOTMYACCESSKEY2
aws_secret_access_key=THISISNOTMYSECRETKEY2
# Optional, to define default region for this profile.
region=us-west-2
&lt;/pre&gt;
&lt;p&gt;Now instead of using a single object, we create multiple objects, one
for each profile we intend to use.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; import botocore.session
&amp;gt;&amp;gt;&amp;gt; session1 = botocore.session.Session(profile='project1')
&amp;gt;&amp;gt;&amp;gt; session2 = botocore.session.Session(profile='project2')
&amp;gt;&amp;gt;&amp;gt; session1.get_credentials().access_key
'THISISNOTMYACCESSKEY1'
&amp;gt;&amp;gt;&amp;gt; session2.get_credentials().access_key
'THISISNOTMYACCESSKEY2'
&lt;/pre&gt;
&lt;p&gt;Also figured out how to get at the `account_id` integer:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; session1.get_scoped_config()['account_id']
'111111111111'
&lt;/pre&gt;
&lt;p&gt;Here is another algorithm that returns a list of sessions objects, one
for each profile listed in the config.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; import botocore.session
&amp;gt;&amp;gt;&amp;gt; sessions = []
&amp;gt;&amp;gt;&amp;gt; aws_config = botocore.session.get_session().full_config
&amp;gt;&amp;gt;&amp;gt; for profile_name in aws_config['profiles']:
...     session = botocore.session.Session(profile=profile_name)
...     sessions.append(session)
&lt;/pre&gt;
&lt;p&gt;Thats all for now!&lt;/p&gt;
&lt;p&gt;You should read my other Boto related posts for tricks to impress your friends.  : )&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="/setting-region-programmatically-in-boto3/"&gt;Setting region programmatically in Boto3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="/filtering-aws-resources-with-boto3/"&gt;Filtering AWS resources with Boto3&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="AWS"/></entry><entry><title>My Mentor</title><link href="https://russell.ballestrini.net/my-mentor/" rel="alternate"/><published>2015-06-21T13:36:00-04:00</published><updated>2015-06-21T13:36:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-06-21:/my-mentor/</id><summary type="html"/><content type="html">&lt;p&gt;The original purpose of this post was to recognize the teachers and
mentors which have helped shape me. I planned to write about how Mr.
Cassidy, my high school drafting teacher, taught me the importance
precision. I prepared to explain how Mr. Mercuri, my college computer
science professor, ignited my desire to engineer software. But I could
not. I knew deep down that one person in particular affected me more
then the rest.&lt;/p&gt;
&lt;p&gt;This person could fix anything and could even talk through a broken
heart. He didn't know the internals of computers but the hacker spirit
sweats from his veins. He taught me how to problem solve, many times in
unconventional ways. I once watched him clear brush with an old snow
plow and cut our work day in half. If he didn't have access to a tool,
he would furnish one out of raw materials and spare parts, like a real
life MacGyver. With a little bit of thought and ingenuity he built
anything he imagined.&lt;/p&gt;
&lt;p&gt;Sometimes he would use tough love to prove a point. For example, when I
had a sun burn in middle school, he taught me to push through pain and
not let my team down even though my jersey was scratching my raw skin.
He showed me how to work hard and how to have great work ethic. He
taught me the importance of &amp;quot;saving for a rainy day&amp;quot; but also knew how
to have fun and loved parties. He taught me self control and how to make
sacrifices for payouts in the future.&lt;/p&gt;
&lt;p&gt;He taught me the fundamentals of kindness and guided me on the righteous
path. He gave great advice, and even though I didn't always listen, he
respected my thoughts and wishes. If he ever caused an accident he would
always admit fault. He emits honor, honesty, and courage and never lies
to or manipulates the people around him. Although we have had our
differences, I think about you each day.&lt;/p&gt;
&lt;p&gt;I love you. Thank you and happy father's day, dad.&lt;/p&gt;
</content><category term="misc"/><category term="Opinion"/></entry><entry><title>Change default gateway on all SmartOS Zones and KVM guests</title><link href="https://russell.ballestrini.net/change-default-gateway-on-all-smartos-zones-and-kvm-guests/" rel="alternate"/><published>2015-06-01T20:34:00-04:00</published><updated>2015-06-01T20:34:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-06-01:/change-default-gateway-on-all-smartos-zones-and-kvm-guests/</id><summary type="html"/><content type="html">&lt;p&gt;Today I needed to change the default gateway on every Zone and KVM guest
on my SmartOS hypervisor because I switched my ISP and as a result my
gateway changed from 192.168.1.254 to 192.168.1.1. After changing one
guest I got lazy and put together this script.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;update-all-gateways.sh&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
for VM in `vmadm list -p -o alias,uuid`

  do
    # create an array called VM_PARTS splitting on ':'
    IFS=':' VM_PARTS=($VM)

    # create some helper varibles for alias and uuid
    alias=${VM_PARTS[0]}
    uuid=${VM_PARTS[1]}

    mac=`vmadm get $uuid | json nics | json -a mac`

    echo &amp;quot;
{
   \&amp;quot;update_nics\&amp;quot;: [
      {
        \&amp;quot;mac\&amp;quot;: \&amp;quot;$mac\&amp;quot;,
        \&amp;quot;gateway\&amp;quot;: \&amp;quot;192.168.1.1\&amp;quot;
      }
   ]
}
&amp;quot; | vmadm update $uuid
done
&lt;/pre&gt;
&lt;/p&gt;&lt;pre class="literal-block"&gt;
[root&amp;#64;hypervisor /opt/setup-jsons/updates]# bash update-all-gateways.sh
Successfully updated VM 211b992b-a448-40b4-94c9-xxxxxxxxxxxx
Successfully updated VM 31baa6a5-aa98-4750-80df-xxxxxxxxxxxx
Successfully updated VM 65d176b4-c36d-4cbf-b6ed-xxxxxxxxxxxx
Successfully updated VM aa0f603c-9572-4cb0-b96f-xxxxxxxxxxxx
Successfully updated VM ad928301-f3e1-4fe8-a1c1-xxxxxxxxxxxx
Successfully updated VM b82a257e-5628-46db-aee4-xxxxxxxxxxxx
Successfully updated VM da72b638-51de-4d7d-9853-xxxxxxxxxxxx
Successfully updated VM ee42bf30-51ce-4ae2-915b-xxxxxxxxxxxx
&lt;/pre&gt;
&lt;p&gt;You are welcome!&lt;/p&gt;
&lt;p&gt;Also to change the global zone (head node) default gateway, edit
/usbkey/config and then run the following commands:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
[root&amp;#64;hypervisor /]# route delete default 192.168.1.254
delete net default: gateway 192.168.1.254

[root&amp;#64;hypervisor /]# route add default 192.168.1.1
add net default: gateway 192.168.1.1

[root&amp;#64;hypervisor /]# netstat -r
&lt;/pre&gt;
</content><category term="misc"/><category term="Uncategorized"/></entry><entry><title>SmartOS Ubuntu guest, apt-get not working because IPv6</title><link href="https://russell.ballestrini.net/smartos-ubuntu-guest-apt-get-not-working-because-ipv6/" rel="alternate"/><published>2015-05-09T20:29:00-04:00</published><updated>2015-05-09T20:29:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-05-09:/smartos-ubuntu-guest-apt-get-not-working-because-ipv6/</id><summary type="html">&lt;p class="first last"&gt;My brutal yet simple work around.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;Turns out I don't have IPv6 setup properly in my network so when apt
attempts to connect to the Internet it tries IPv6 and fails.&lt;/p&gt;
&lt;p&gt;To disable IPv6 on the ubuntu guest, add this to end of /etc/sysctl.conf
and restart the guest:&lt;/p&gt;
&lt;p&gt;sudo vim /etc/sysctl.conf:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;This was a hacky work around mostly because although I desire to have
IPv6 working, I desire to get this VM running more...&lt;/p&gt;
&lt;p&gt;Thanks for reading, leave comments please.&lt;/p&gt;
</content><category term="misc"/><category term="Guide"/></entry><entry><title>A Python script which searches for available interpreters</title><link href="https://russell.ballestrini.net/a-python-script-which-searches-for-available-interpreters/" rel="alternate"/><published>2015-05-08T10:34:00-04:00</published><updated>2015-05-08T10:34:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-05-08:/a-python-script-which-searches-for-available-interpreters/</id><summary type="html"/><content type="html">&lt;p&gt;This post describes how to write a polyglot -- in this case a script
which runs as valid Bash or Python, to search for available Python
interpreters.&lt;/p&gt;
&lt;p&gt;The script initially runs as Bash but upon finding a first match, the
script will call itself again this time using the expected Python
interpreter in interactive mode!&lt;/p&gt;
&lt;p&gt;And now, for the polyglot code:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
#!/bin/sh

# reinvoke this script with bpython -i, ipython -i, or python -i
# reference: https://unix.stackexchange.com/a/66242
''':'
if type bpython &amp;gt;/dev/null 2&amp;gt;/dev/null; then
  exec bpython -i &amp;quot;$0&amp;quot; &amp;quot;$&amp;#64;&amp;quot;
elif type ipython &amp;gt;/dev/null 2&amp;gt;/dev/null; then
  exec ipython -i &amp;quot;$0&amp;quot; &amp;quot;$&amp;#64;&amp;quot;
else
  exec python -i &amp;quot;$0&amp;quot; &amp;quot;$&amp;#64;&amp;quot;
fi
'''
from helpers import (
  base_parser,
  get_hvpc_from_args,
)
parser = base_parser('Interactive Python interpreter &amp;amp; connection to hvpc')
args = parser.parse_args()
hvpc = get_hvpc_from_args(args)
print('\nYou now have access to the hvpc object, for example: hvpc.roles\n')
&lt;/pre&gt;
&lt;p&gt;In this case you can see that we setup the interactive interpreter's
environment to create an hvpc (Husky VPC) object for exploration.&lt;/p&gt;
&lt;p&gt;If you want a pure python version that doesn't use a bash/python
polygot, checkout this code I wrote:
&lt;a class="reference external" href="https://github.com/russellballestrini/botoform/blob/master/botoform/plugins/repl.py"&gt;https://github.com/russellballestrini/botoform/blob/master/botoform/plugins/repl.py&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/></entry><entry><title>Migrating libvirt KVM guest to SmartOS KVM guest</title><link href="https://russell.ballestrini.net/migrating-libvirt-kvm-guest-to-smartos-kvm-guest/" rel="alternate"/><published>2015-04-28T22:17:00-04:00</published><updated>2015-04-28T22:17:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-04-28:/migrating-libvirt-kvm-guest-to-smartos-kvm-guest/</id><summary type="html">&lt;p class="first last"&gt;Stop worrying and replace your Linux hypervisor with SmartOS.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;The following tutorial documents how to migrate a libvirt/KVM guest from
Ubuntu to SmartOS.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;akuma:&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Ubuntu Hypervisor&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;guy:&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;SmartOS Hypervisor&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;sagat:&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;guest to migrate&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;These commands were run on akuma:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
WORKDIR=/KVMROOT/migrate
sudo mkdir $WORKDIR
cd $WORKDIR

# this tool simply stops and tarballs up the qcow and xml for libvirt KVM guest.
sudo /usr/local/bin/virt-back --path=$WORKDIR --backup sagat

sudo tar -xzvf sagat.tar.gz

sudo qemu-img convert -O raw sagat.qcow2 sagat.raw
sudo qemu-img convert -O raw sagat-var.qcow2 sagat-var.raw

scp sagat*.raw root&amp;#64;guy:/opt/tmp
&lt;/pre&gt;
&lt;p&gt;These commands were run on guy:&lt;/p&gt;
&lt;p&gt;Create the following file on guy - setup-ubuntu-sagat.json:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
{
  &amp;quot;brand&amp;quot;: &amp;quot;kvm&amp;quot;,
  &amp;quot;resolvers&amp;quot;: [
    &amp;quot;192.168.1.22&amp;quot;,
    &amp;quot;8.8.4.4&amp;quot;
  ],
  &amp;quot;ram&amp;quot;: &amp;quot;4096&amp;quot;,
  &amp;quot;vcpus&amp;quot;: &amp;quot;4&amp;quot;,
  &amp;quot;alias&amp;quot;: &amp;quot;sagat&amp;quot;,
  &amp;quot;hostname&amp;quot;: &amp;quot;sagat&amp;quot;,
  &amp;quot;nics&amp;quot;: [
    {
      &amp;quot;nic_tag&amp;quot;: &amp;quot;admin&amp;quot;,
      &amp;quot;ip&amp;quot;: &amp;quot;192.168.1.50&amp;quot;,
      &amp;quot;netmask&amp;quot;: &amp;quot;255.255.255.0&amp;quot;,
      &amp;quot;gateway&amp;quot;: &amp;quot;192.168.1.254&amp;quot;,
      &amp;quot;model&amp;quot;: &amp;quot;virtio&amp;quot;,
      &amp;quot;primary&amp;quot;: true
    }
  ],
  &amp;quot;disks&amp;quot;: [
    {
      &amp;quot;boot&amp;quot;: true,
      &amp;quot;model&amp;quot;: &amp;quot;virtio&amp;quot;,
      &amp;quot;size&amp;quot;: 10240
    },
    {
      &amp;quot;model&amp;quot;: &amp;quot;virtio&amp;quot;,
      &amp;quot;size&amp;quot;: 20480
    }
  ]
}
&lt;/pre&gt;
&lt;p&gt;We then create a new KVM guest on guy:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
[root&amp;#64;guy /opt/setup-jsons]# vmadm create -f setup-ubuntu-sagat.json
Successfully created VM aa0f603c-9572-4cb0-b96f-4c79eb431223
&lt;/pre&gt;
&lt;p&gt;List out the virtual block devices on the new KVM guest:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
[root&amp;#64;guy /opt/setup-jsons]# vmadm info aa0f603c-9572-4cb0-b96f-4c79eb431223 block
{
  &amp;quot;block&amp;quot;: [
    {
      &amp;quot;device&amp;quot;: &amp;quot;virtio0&amp;quot;,
      &amp;quot;locked&amp;quot;: false,
      &amp;quot;removable&amp;quot;: false,
      &amp;quot;inserted&amp;quot;: {
        &amp;quot;ro&amp;quot;: false,
        &amp;quot;drv&amp;quot;: &amp;quot;raw&amp;quot;,
        &amp;quot;encrypted&amp;quot;: false,
        &amp;quot;file&amp;quot;: &amp;quot;/dev/zvol/rdsk/zones/aa0f603c-9572-4cb0-b96f-4c79eb431223-disk0&amp;quot;
      },
      &amp;quot;type&amp;quot;: &amp;quot;hd&amp;quot;
    },
    {
      &amp;quot;device&amp;quot;: &amp;quot;virtio1&amp;quot;,
      &amp;quot;locked&amp;quot;: false,
      &amp;quot;removable&amp;quot;: false,
      &amp;quot;inserted&amp;quot;: {
        &amp;quot;ro&amp;quot;: false,
        &amp;quot;drv&amp;quot;: &amp;quot;raw&amp;quot;,
        &amp;quot;encrypted&amp;quot;: false,
        &amp;quot;file&amp;quot;: &amp;quot;/dev/zvol/rdsk/zones/aa0f603c-9572-4cb0-b96f-4c79eb431223-disk1&amp;quot;
      },
      &amp;quot;type&amp;quot;: &amp;quot;hd&amp;quot;
    }
  ]
}
&lt;/pre&gt;
&lt;p&gt;Stop the new guest, it needs to be off for the restore.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
[root&amp;#64;guy /opt/setup-jsons]# vmadm stop aa0f603c-9572-4cb0-b96f-4c79eb431223
Successfully completed stop for VM aa0f603c-9572-4cb0-b96f-4c79eb431223
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Use dd to:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;write sagat.raw to disk0&lt;/li&gt;
&lt;li&gt;write sagat-var.raw to disk1&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="literal-block"&gt;
dd if=sagat.raw of=/dev/zvol/rdsk/zones/aa0f603c-9572-4cb0-b96f-4c79eb431223-disk0 bs=1M
dd if=sagat-var.raw of=/dev/zvol/rdsk/zones/aa0f603c-9572-4cb0-b96f-4c79eb431223-disk1 bs=1M
&lt;/pre&gt;
&lt;p&gt;Lastly start the new guest up, and check it out:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
vmadm start aa0f603c-9572-4cb0-b96f-4c79eb431223
&lt;/pre&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/></entry><entry><title>Setting region programmatically in Boto3</title><link href="https://russell.ballestrini.net/setting-region-programmatically-in-boto3/" rel="alternate"/><published>2015-04-24T14:07:00-04:00</published><updated>2015-04-24T14:07:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-04-24:/setting-region-programmatically-in-boto3/</id><summary type="html"/><content type="html">&lt;p&gt;At work I'm looking into the possibility of porting parts of our AWS
automation codebase from Boto2 to Boto3. We desire to perform this port
because Boto2's record and result pagination appears defective.&lt;/p&gt;
&lt;p&gt;I started to familiarize myself with Boto3 by using the Interactive Python interpreter.&lt;/p&gt;
&lt;p&gt;Here I show myself trying to connect to the RDS AWS endpoint following the docs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;boto3&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rds&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Traceback&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;NoRegionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;You&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt; &lt;span class="n"&gt;specify&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What, No region? OK - how do I set a region?&lt;/p&gt;
&lt;p&gt;Well it turns out the docs want you to configure a region in a config file.
This will not work for me, I need to set the region programatically...&lt;/p&gt;
&lt;p&gt;So after stumbling around in the botocore source code I found the
following solutions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution 1 - Set region_name when creating client:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;boto3&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rds&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;us-west-2&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Solution 2 - Set default region_name on the session:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;boto3&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setup_default_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;us-west-2&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;rds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rds&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It seems Boto3 has two types of interfaces, clients and resources.&lt;/p&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;Clients:&lt;/dt&gt;
&lt;dd&gt;return description objects and appear lower level.
Description objects seem like AWS XML responses transformed into Python Dicts/Lists.&lt;/dd&gt;
&lt;dt&gt;Resources:&lt;/dt&gt;
&lt;dd&gt;return higher level Python objects and like Instances with stop/start methods.&lt;/dd&gt;
&lt;/dl&gt;
&lt;p&gt;At a quick glance, both clients and resources seem to properly implement
pagination automatically!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;# client interface.&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ec2&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;us-west-2&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;idesc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;describe_instances&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idesc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Reservations&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="mi"&gt;273&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;idesc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ResponseMetadata&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;HTTPStatusCode&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;RequestId&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;7e431ff7-xxxx-xxxx-xxxx-xxxxxxxxxxxxx&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;# resource interface.&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ec2&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;us-west-2&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instances&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;     &lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That all for now!&lt;/p&gt;
&lt;p&gt;You should read my other Boto related posts for tricks to impress your friends.  : )&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="/filtering-aws-resources-with-boto3/"&gt;Filtering AWS resources with Boto3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="/working-with-botocores-awsconfig/"&gt;Working with botocores AWS config&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/><category term="AWS"/></entry><entry><title>Securely publish Jenkins build artifacts on Salt Master</title><link href="https://russell.ballestrini.net/securely-publish-jenkins-build-artifacts-on-salt-master/" rel="alternate"/><published>2015-02-22T12:39:00-05:00</published><updated>2015-02-22T12:39:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-02-22:/securely-publish-jenkins-build-artifacts-on-salt-master/</id><summary type="html">&lt;p class="first last"&gt;Your project deserves an asset pipeline.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;Do you want a secure setup for publishing and staging build artifacts
from a Jenkins build server to a Salt Master? This guide describes my
fully automated pipeline to transport binaries using Salt's encrypted
&amp;quot;bus&amp;quot;.&lt;/p&gt;
&lt;p&gt;We start off with some Salt States to stand up a Jenkins build server
&amp;quot;client&amp;quot;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;jenkins/client.sls:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# https://russell.ballestrini.net/securely-publish-jenkins-build-artifacts-on-salt-master/
# manage jenkins user, home dir, and Jenkins &amp;quot;master&amp;quot; public SSH key.
jenkins:
  user.present:
    - fullname: jenkins butler
    - shell: /bin/bash
    - home: /home/jenkins

  file.directory:
    - name: /home/jenkins
    - user: jenkins
    - group: jenkins
    - require:
      - user: jenkins

  ssh_auth.present:
    - user: jenkins
    - name: {{ pillar.get('jenkins-public-key') }}
    - require:
      - user: jenkins

# Manage a script to push artifacts to Salt Master.
# Note: jenkins user should _not_ have ability to change this file.
/opt/salt-call-put-artifacts-onto-salt-master.sh:
  file.managed:
    - user: root
    - group: jenkins
    - mode: 755
    - contents: |
        #!/bin/bash
        set -x
        salt-call cp.push_dir &amp;quot;$PWD&amp;quot; glob='*.tar.gz'
        salt-call cp.push &amp;quot;$PWD/commit-hash.txt&amp;quot;
    - require:
      - file: jenkins

# Allow jenkins to run push script as root via sudo.
jenkins-sudoers:
  file.append:
    - name: /etc/sudoers
    - text:
      - &amp;quot;jenkins    ALL = NOPASSWD: /opt/salt-call-put-artifacts-onto-salt-master.sh&amp;quot;
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;On the Salt Master we must enable MinionFS and restart Salt Master
process:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
fileserver_backend:
  - roots
  - minion

file_recv: True
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;We then use this command as the last build task in every Jenkins build
job:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo /home/jenkins/salt-call-put-artifacts-onto-salt-master.sh
&lt;/pre&gt;
&lt;p&gt;This causes the file to be staged on the Salt Master on a successful
build.&lt;/p&gt;
&lt;p&gt;The Salt States to deploy the software to production, look something
like this:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# extract tarball from Salt Master using MinionFS.
extract-tarball-for-mysite:
  archive.extracted:
    - name: /www/mysite
    - archive_format: tar
    - source: salt://ubuntu-jenkins.foxhop.net/home/jenkins/workspace/job-name/env.tar.gz
    - user: uwsgi
    - group: uwsgi
    # I want to always extract, not sure a better way.
    - if_missing: /dev/taco
    - require:
      - service: make-mysite-dead-for-release
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;I also have build triggers which monitor remote git/hg repos for
changes. Pushing code triggers a build which tests my code base and
securely publishes to my Salt Master. When the time comes to perform a
release, all I have to do is run highstate, because the pipeline did all
the other work for me!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Special thanks to an &lt;a class="reference external" href="https://my.remarkbox.com/r/efbc6dc7-02e3-11e9-b440-040140774501"&gt;anonymous commentor&lt;/a&gt; regarding how to increase security. I have updated the scripts accordingly.&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/><category term="Salt"/></entry><entry><title>Set postgres user password on PostgreSQL SmartOS Zone</title><link href="https://russell.ballestrini.net/set-postgres-user-password-on-postgresql-smartos-zone/" rel="alternate"/><published>2015-01-21T22:28:00-05:00</published><updated>2015-01-21T22:28:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-01-21:/set-postgres-user-password-on-postgresql-smartos-zone/</id><summary type="html"/><content type="html">&lt;p&gt;Connect to zone and determine the auto generated password for postgres
user:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
cat /var/svc/log/system-zoneinit\:default.log | grep PGSQL_PW
&lt;/pre&gt;
&lt;p&gt;document the result and log into postgres with the following command,
entering the password when prompted:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
[root&amp;#64;psql ~]# psql --user postgres
&lt;/pre&gt;
&lt;p&gt;Alter the postgres role's password:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
postgres=# ALTER ROLE postgres UNENCRYPTED PASSWORD 'new-password';
&lt;/pre&gt;
&lt;p&gt;Now exit (&lt;tt class="docutils literal"&gt;\q&lt;/tt&gt;) then try to log in with the new password.&lt;/p&gt;
&lt;p&gt;In my case I was setting up PostgreSQL for testing out Zabbix monitoring
system. So I did the following as the postgres user:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
postgres=# CREATE USER zab UNENCRYPTED PASSWORD 'zabby'
postgres=# CREATE DATABASE zab OWNER zab;
&lt;/pre&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/></entry><entry><title>Risk, Process, and Balance</title><link href="https://russell.ballestrini.net/risk-process-and-balance/" rel="alternate"/><published>2015-01-10T22:35:00-05:00</published><updated>2015-01-10T22:35:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2015-01-10:/risk-process-and-balance/</id><summary type="html">&lt;p&gt;The operations of a company will have intrinsic risk. Risk occurs each
time we decide to take an action or an inaction. This means that
anything we choose to do, or not do, has associated risk.&lt;/p&gt;
&lt;p&gt;An organization which has an unhealthy aversion to risk has a much
higher chance …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The operations of a company will have intrinsic risk. Risk occurs each
time we decide to take an action or an inaction. This means that
anything we choose to do, or not do, has associated risk.&lt;/p&gt;
&lt;p&gt;An organization which has an unhealthy aversion to risk has a much
higher chance of failure. As time goes on the intolerance of taking on
additional risk to accomplish goals, will render a company petrified.
Stuck in the fear of change.&lt;/p&gt;
&lt;p&gt;In order to become unstuck, a company and it's employees must become
great at calculating risk and assessment. In order to make a choice on
what to work on, and what to not work on, they will need to research and
collect data and continuously track progress and feedback. The company
must use this data and feedback to move uncalculated bad risk into
calculated good risk. Simply thinking through the problem and change
before hand can uncover ways to accomplish the end result in the least
risky way.&lt;/p&gt;
&lt;p&gt;The biggest objection to the risk assessment phase is typically time
constraints. A great company will learn how to rapidly assess and reduce
the risk of action. They reduce risk by: gathering requirements,
planning, building process, using process to drive automation, and
iterating on automation to speed up feedback loops. These fast feedback
loops will allow the company and it's employees to fail smaller and more
frequently and facilitate reflection to optimize the pipeline. This
means even more calculated and low risk actions!&lt;/p&gt;
&lt;blockquote&gt;
So, a company should add more process to reduce risk? Not quite.&lt;/blockquote&gt;
&lt;p&gt;A process, when left unchecked or added for the wrong reasons, will
become more detrimental then blindly performing an action without
calculating the risk. Below I outline the key differences between a good
process and a bad process.&lt;/p&gt;
&lt;p&gt;A good process will:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;reduce risk but not at the expense of blocking or preventing progress&lt;/li&gt;
&lt;li&gt;be fast to facilitate quick feedback loops&lt;/li&gt;
&lt;li&gt;have potential for automation and must not be tedious or manual
(think approvals)&lt;/li&gt;
&lt;li&gt;promote small and frequent changes&lt;/li&gt;
&lt;li&gt;support both fail forward and fail backward&lt;/li&gt;
&lt;li&gt;only fail backward to reduce time-to-recover&lt;/li&gt;
&lt;li&gt;not be pushed down from upper management, rather upper management
should help set goals and requirements, while the team builds and
decides on process&lt;/li&gt;
&lt;li&gt;be questioned and reviewed often to promote iteration to find and fix
inefficiencies&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A bad process will:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;block real work from getting done&lt;/li&gt;
&lt;li&gt;prevent creative and innovative solutions&lt;/li&gt;
&lt;li&gt;have a higher chance of being ignored, skipped, or held in contempt&lt;/li&gt;
&lt;li&gt;punish people who take risks (make change) and promote people who do
nothing&lt;/li&gt;
&lt;li&gt;cause more issues then it solves&lt;/li&gt;
&lt;li&gt;become a crutch or a security blanket (a false sense of security)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I feel strongly that the essence behind the Agile and DevOps movements
comes from the constant battle of balancing risk with process. This
constant desire for balance in the organization's environment will lead
to innovative ideas, tools, and workflows. These ideas, tools and
workflows can not function properly if transplanted into a company who
does not have the desire to find the optimal balance. Put simply, a
company cannot buy Agile or DevOps, that culture needs to grow from with
in.&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Opinion"/></entry><entry><title>autofs /net automount stopped working</title><link href="https://russell.ballestrini.net/autofs-net-automount-stopped-working/" rel="alternate"/><published>2014-12-21T22:38:00-05:00</published><updated>2014-12-21T22:38:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-12-21:/autofs-net-automount-stopped-working/</id><summary type="html">&lt;p&gt;So autofs randomly stopped working on one of my Ubuntu hosts (this issue
has been found on Arch as well so its most likely a change upstream). I
found this error in the logs:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
attempting to mount entry /net/freenas.example.net
get_exports: lookup(hosts): exports lookup failed for freenas …&lt;/pre&gt;</summary><content type="html">&lt;p&gt;So autofs randomly stopped working on one of my Ubuntu hosts (this issue
has been found on Arch as well so its most likely a change upstream). I
found this error in the logs:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
attempting to mount entry /net/freenas.example.net
get_exports: lookup(hosts): exports lookup failed for freenas.example.net
key &amp;quot;freenas.example.net&amp;quot; not found in map source(s).
failed to mount /net/freenas.example.net
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;The fix was to replace the following line in /etc/auto.master from&lt;/p&gt;
&lt;pre class="literal-block"&gt;
/net   -hosts
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;to this&lt;/p&gt;
&lt;pre class="literal-block"&gt;
/net /etc/auto.net --timeout=60
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;and then restart autofs (sudo service autofs restart).&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/></entry><entry><title>Custom Rundeck HipChat notification templates</title><link href="https://russell.ballestrini.net/custom-rundeck-hipchat-notification-templates/" rel="alternate"/><published>2014-12-03T01:37:00-05:00</published><updated>2014-12-03T01:37:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-12-03:/custom-rundeck-hipchat-notification-templates/</id><summary type="html">&lt;p&gt;Today I built a GUI and workflow around Ansible using Rundeck. Tonight I
started diving into sending HipChat notifications and after a bit of
research, I managed to create a custom notification template for each
Rundeck project.&lt;/p&gt;
&lt;p&gt;Modify your project's configuration file, on Ubuntu it was in
&lt;tt class="docutils literal"&gt;/var/rundeck/projects …&lt;/tt&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;Today I built a GUI and workflow around Ansible using Rundeck. Tonight I
started diving into sending HipChat notifications and after a bit of
research, I managed to create a custom notification template for each
Rundeck project.&lt;/p&gt;
&lt;p&gt;Modify your project's configuration file, on Ubuntu it was in
&lt;tt class="docutils literal"&gt;/var/rundeck/projects/pname/etc/project.properties&lt;/tt&gt;, and add the
following line to the bottom:&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre class="literal-block"&gt;
framework.plugin.Notification.HipChatNotification.messageTemplateLocation=/var/rundeck/projects/pname/etc/custom-hipchat-template.ftl
&lt;/pre&gt;
&lt;p&gt;Note: replace &lt;tt class="docutils literal"&gt;pname&lt;/tt&gt; with your project name.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I ended up producing a single line chat notification template.
I uploaded it here to act as an example:&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://pad.yohdah.com/311/rundeck-hipchat-custom-notification-template"&gt;Custom Rundeck HipChat Notification Template&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The template syntax and renderer is called FreeMarker. Rundeck and the
HipChat plugin pass many different context Map hash objects to
FreeMarker for use in the templates. In this case I display &amp;quot;VPC name&amp;quot;
selected by the user when starting the job.&lt;/p&gt;
&lt;p&gt;References:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://rundeck.org/plugins/2013/06/24/hipchat-notification.html"&gt;Rundeck HipChat Notification
Plugin&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a class="reference external" href="https://github.com/hbakkum/rundeck-hipchat-plugin"&gt;Rundeck HipChat Notification
Plugin&lt;/a&gt; (User
Guide and Default Template)&lt;/p&gt;
&lt;/li&gt;</content><category term="misc"/><category term="Code"/><category term="DevOps"/></entry><entry><title>Build release pipelines on S3 with s3p</title><link href="https://russell.ballestrini.net/build-release-pipelines-on-s3-with-s3p/" rel="alternate"/><published>2014-11-24T14:39:00-05:00</published><updated>2014-11-24T14:39:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-11-24:/build-release-pipelines-on-s3-with-s3p/</id><summary type="html">&lt;p&gt;This weekend I finished my first sprint on s3p which is a Python library
and CLI application that manages release pipelines on AWS S3. I put a
lot of effort into the
&lt;a class="reference external" href="https://github.com/russellballestrini/s3p/blob/master/readme.rst"&gt;readme.rst&lt;/a&gt;
file, so look there for usage and examples.&lt;/p&gt;
&lt;p&gt;The main purpose of s3p is to use …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This weekend I finished my first sprint on s3p which is a Python library
and CLI application that manages release pipelines on AWS S3. I put a
lot of effort into the
&lt;a class="reference external" href="https://github.com/russellballestrini/s3p/blob/master/readme.rst"&gt;readme.rst&lt;/a&gt;
file, so look there for usage and examples.&lt;/p&gt;
&lt;p&gt;The main purpose of s3p is to use code to enforce process when promoting
releases in the pipeline. Another goal was to make the CLI tool dead
simple to use. As a side effect, I ended up using composition to extend
boto.s3's Key and Bucket classes to produce
&lt;a class="reference external" href="https://github.com/russellballestrini/s3p/blob/master/s3p/release.py"&gt;S3Release&lt;/a&gt;
and
&lt;a class="reference external" href="https://github.com/russellballestrini/s3p/blob/master/s3p/pipeline.py"&gt;S3Pipeline&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I plan to incorporate s3p into Teamcity build jobs. At work I would like
to use s3p to replace bash+s3cmd in pipeline management. Once s3p has a
bit more production use, I will likely sprint on it again.&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/><category term="AWS"/></entry><entry><title>Dealing with pagination in Python</title><link href="https://russell.ballestrini.net/dealing-with-pagination-in-python/" rel="alternate"/><published>2014-11-13T21:06:00-05:00</published><updated>2014-11-13T21:06:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-11-13:/dealing-with-pagination-in-python/</id><summary type="html">&lt;p&gt;So I'm working with an API (AWS ElastiCache) that offers mandatory
pagination of results. I need to get all results, so I took some time to
work out this logic.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
def combine_results(function, key, marker=0, **kwargs):
    &amp;quot;&amp;quot;&amp;quot;deal with manditory pagination of AWS result descriptions&amp;quot;&amp;quot;&amp;quot;
    results = []
    while marker != None:
        result …&lt;/pre&gt;</summary><content type="html">&lt;p&gt;So I'm working with an API (AWS ElastiCache) that offers mandatory
pagination of results. I need to get all results, so I took some time to
work out this logic.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
def combine_results(function, key, marker=0, **kwargs):
    &amp;quot;&amp;quot;&amp;quot;deal with manditory pagination of AWS result descriptions&amp;quot;&amp;quot;&amp;quot;
    results = []
    while marker != None:
        result = function(marker = marker, **kwargs)
        marker = nested_lookup('Marker', result)[0]
        results += nested_lookup(key, result)
    return results
&lt;/pre&gt;
&lt;/p&gt;&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Not only is the AWS ElastiCache API paginated but it also appears
deeply nested in lists and dicts.&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;I use this to burn it with fire:&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre class="literal-block"&gt;
def nested_lookup(key, dictionary):
    &amp;quot;&amp;quot;&amp;quot;Lookup a key in a nested dictionary, return a list of values&amp;quot;&amp;quot;&amp;quot;
    return list(_nested_lookup(key, dictionary))

def _nested_lookup(key, dictionary):
    &amp;quot;&amp;quot;&amp;quot;
    Lookup a key in a nested dictionary, return value

    Authors: Dougles Miranda and Russell Ballestrini
    &amp;quot;&amp;quot;&amp;quot;
    if isinstance(dictionary, list):
        for d in dictionary:
            for result in _nested_lookup(key, d):
                yield result

    if isinstance(dictionary, dict):
        for k, v in dictionary.iteritems():
            if k == key:
                yield v
            elif isinstance(v, dict):
                for result in _nested_lookup(key, v):
                    yield result
            elif isinstance(v, list):
                for d in v:
                    for result in _nested_lookup(key, d):
                        yield result
&lt;/pre&gt;
&lt;p&gt;The end result is we have access to paginated and deeply nested data
with a simple to use function:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; from lib import combine_results, nested_lookup
&amp;gt;&amp;gt;&amp;gt; d = elasticache_connection.describe_cache_clusters()
&amp;gt;&amp;gt;&amp;gt; nested_lookup('CacheClusterId', d)
[u'demo04-a-redis', u'demo04-b-redis', u'demo06-a-redis', u'demo06-b-redis', u'test-a-memcached', u'test-b-redis', u'ops01-redis', u'qa01-redis', u'ops02-redis', u'qa02-redis', u'int01-a-redis', u'int01-b-redis', u'ops03-redis', u'ops04-redis']
&lt;/pre&gt;
&lt;p&gt;Here are some unit tests to prove these functions work like expected:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
from unittest import TestCase

from lib.util import (
  combine_results,
  nested_lookup,
  _nested_lookup,
)

def my_func_that_paginates(max_results=3, marker=0):
    &amp;quot;&amp;quot;&amp;quot;this function sort of mocks the paginated AWS description results&amp;quot;&amp;quot;&amp;quot;
    data = [
      {'desired_key' : 0},
      {'desired_key' : 1},
      {'desired_key' : 2},
      {'desired_key' : 3},
      {'desired_key' : 4},
      {'desired_key' : 5},
      {'desired_key' : 6},
      {'desired_key' : 7},
      {'desired_key' : 8},
      {'desired_key' : 9},
    ]
    new_marker = marker + max_results
    if new_marker &amp;gt; len(data):
        # last page!
        page = data[marker:]
        return {'results' : page, 'Marker' : None}
    page = data[marker:new_marker]
    return {'results' : page, 'Marker' : new_marker}

class TestCombineResults(TestCase):

    def test_combine_results_returns_all_results(self):
        expected_set = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
        f = my_func_that_paginates
        result_set = set(combine_results(f, 'desired_key'))
        self.assertSetEqual(expected_set, result_set)

class TestNestedLookup(TestCase):

    def setUp(self):
        self.subject_dict = {'a':1,'b':{'d':100},'c':{'d':200}}

    def test_nested_lookup(self):
        results = nested_lookup('d', self.subject_dict)
        self.assertEqual(2, len(results))
        self.assertIn(100, results)
        self.assertIn(200, results)
        self.assertSetEqual({100,200}, set(results))

    def test_nested_lookup_wrapped_in_list(self):
        results = nested_lookup('d', [{}, self.subject_dict, {}])
        self.assertEqual(2, len(results))
        self.assertIn(100, results)
        self.assertIn(200, results)
        self.assertSetEqual({100,200}, set(results))

    def test_nested_lookup_wrapped_in_list_in_dict_in_list(self):
        results = nested_lookup('d', [{}, {'H' : [self.subject_dict]} ])
        self.assertEqual(2, len(results))
        self.assertIn(100, results)
        self.assertIn(200, results)
        self.assertSetEqual({100,200}, set(results))

    def test_nested_lookup_wrapped_in_list_in_list(self):
        results = nested_lookup('d', [ {}, [self.subject_dict, {}] ])
        self.assertEqual(2, len(results))
        self.assertIn(100, results)
        self.assertIn(200, results)
        self.assertSetEqual({100,200}, set(results))
&lt;/pre&gt;
&lt;p&gt;With this test, the steps of the algorithm looks like this:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
{'Marker': 3, 'results': [{'desired_key': 0}, {'desired_key': 1}, {'desired_key': 2}]}
3
[0, 1, 2]
[0, 1, 2]
{'Marker': 6, 'results': [{'desired_key': 3}, {'desired_key': 4}, {'desired_key': 5}]}
6
[3, 4, 5]
[0, 1, 2, 3, 4, 5]
{'Marker': 9, 'results': [{'desired_key': 6}, {'desired_key': 7}, {'desired_key': 8}]}
9
[6, 7, 8]
[0, 1, 2, 3, 4, 5, 6, 7, 8]
{'Marker': None, 'results': [{'desired_key': 9}]}
None
[9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
ok
&lt;/pre&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/><category term="AWS"/></entry><entry><title>Turn python dict into a key=value string and back again</title><link href="https://russell.ballestrini.net/turn-python-dict-into-a-keyvalue-string/" rel="alternate"/><published>2014-11-09T22:02:00-05:00</published><updated>2014-11-09T22:02:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-11-09:/turn-python-dict-into-a-keyvalue-string/</id><summary type="html">&lt;p&gt;I'm currently refactoring a script that tags AWS resources and I came up
with this one liner to generate pretty output. It basically turns
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;{'tag1':'value1','tag2':'value2'}&lt;/span&gt;&lt;/tt&gt; into &lt;tt class="docutils literal"&gt;tag1=value1, tag2=value2&lt;/tt&gt;.
Here is the code:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
', '.join(['='.join(key_value) for key_value in {'a':'1','b':'2'}.items() ])
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Oh, and …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I'm currently refactoring a script that tags AWS resources and I came up
with this one liner to generate pretty output. It basically turns
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;{'tag1':'value1','tag2':'value2'}&lt;/span&gt;&lt;/tt&gt; into &lt;tt class="docutils literal"&gt;tag1=value1, tag2=value2&lt;/tt&gt;.
Here is the code:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
', '.join(['='.join(key_value) for key_value in {'a':'1','b':'2'}.items() ])
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Oh, and if you like this, here is a function with additional functionality and protection:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
def dict_to_key_value(data, sep='=', pair_sep=', '):
    &amp;quot;&amp;quot;&amp;quot;turns {'tag1':'value1','tag2':'value2'} into tag1=value1, tag2=value2&amp;quot;&amp;quot;&amp;quot;
    return pair_sep.join([sep.join((unicode(key), unicode(value))) for key, value in data.items()])
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Careful, this might blow up on dictionaries that nest other objects.
Also here is a test:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
def test_dict_to_key_value():
    data = {'tag1':'value1','tag2':'value2'}
    pretty_str = dict_to_key_value(data)
    assert('tag1=value1' in pretty_str)
    assert('tag2=value2' in pretty_str)
    assert('tag1=value1, tag2=value2' in pretty_str)
    not_as_pretty = dict_to_key_value(data,'x','x')
    assert('tag1xvalue1xtag2xvalue2' in not_as_pretty)
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Here is the inverse, taking a list of key_value strings and returning a
dictionary of the data:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
def key_value_to_dict(key_value_list, sep='=', pair_sep=',' ):
    &amp;quot;&amp;quot;&amp;quot;
    Accept a key_value_list, like::

      key_value_list = ['a=1,b=2', 'c=3, d=4', 'e=5']

    Return a dict, like::

      {'a':'1', 'b':'2', 'c':'3', 'd':'4', 'e':'5'}
    &amp;quot;&amp;quot;&amp;quot;
    d = {}
    for speclist in key_value_list:
        for spec in speclist.strip().split(','):
            key, value = spec.strip().split('=')
            d[key] = value
    return d
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;And of course a test to prove it works how we expect:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
def test_key_value_to_dict():
    key_value_list = ['a=1,b=2', 'c=3, d=4', 'e=5']
    desired_result = {'a':'1', 'b':'2', 'c':'3', 'd':'4', 'e':'5'}
    assert(key_value_to_dict(key_value_list) == desired_result)
&lt;/pre&gt;
&lt;/p&gt;</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/><category term="AWS"/></entry><entry><title>Migrating MongoDB from Ubuntu to SmartOS</title><link href="https://russell.ballestrini.net/migrating-mongodb-from-ubuntu-to-smartos/" rel="alternate"/><published>2014-10-11T20:52:00-04:00</published><updated>2014-10-11T20:52:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-10-11:/migrating-mongodb-from-ubuntu-to-smartos/</id><summary type="html">&lt;p&gt;First, I installed the mongodb 14.2.0
(&lt;tt class="docutils literal"&gt;uuid &lt;span class="pre"&gt;a5775e36-2a02-11e4-942a-67ae7a242985&lt;/span&gt;&lt;/tt&gt;) dataset:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
imgadm avail | grep mongo
imgadm import a5775e36-2a02-11e4-942a-67ae7a242985
&lt;/pre&gt;
&lt;p&gt;Next, I launched a new zone with this image.&lt;/p&gt;
&lt;p&gt;Then I grabbed the uuid of the zone (&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;211b992b-a448-40b4-94c9-00fa82615cec&lt;/span&gt;&lt;/tt&gt;) and I connected into the zone&lt;/p&gt;
&lt;pre class="literal-block"&gt;
zlogin 211b992b-a448-40b4-94c9-00fa82615cec
&lt;/pre&gt;
&lt;p&gt;The zone automatically creates a username …&lt;/p&gt;</summary><content type="html">&lt;p&gt;First, I installed the mongodb 14.2.0
(&lt;tt class="docutils literal"&gt;uuid &lt;span class="pre"&gt;a5775e36-2a02-11e4-942a-67ae7a242985&lt;/span&gt;&lt;/tt&gt;) dataset:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
imgadm avail | grep mongo
imgadm import a5775e36-2a02-11e4-942a-67ae7a242985
&lt;/pre&gt;
&lt;p&gt;Next, I launched a new zone with this image.&lt;/p&gt;
&lt;p&gt;Then I grabbed the uuid of the zone (&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;211b992b-a448-40b4-94c9-00fa82615cec&lt;/span&gt;&lt;/tt&gt;) and I connected into the zone&lt;/p&gt;
&lt;pre class="literal-block"&gt;
zlogin 211b992b-a448-40b4-94c9-00fa82615cec
&lt;/pre&gt;
&lt;p&gt;The zone automatically creates a username and password for
admin and &amp;quot;quickbackup&amp;quot;. You can find these passwords by running the
following command inside the zone:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
cat /var/svc/log/system-zoneinit\:default.log | grep -i mon
&lt;/pre&gt;
&lt;p&gt;First thing I did was disable authentication by modifying
&lt;tt class="docutils literal"&gt;/opt/local/etc/mongod.conf&lt;/tt&gt;:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
#auth = true
noauth = true
&lt;/pre&gt;
&lt;p&gt;Then I restarted MongoDB to re-read its configuration:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
svcadm restart mongodb
&lt;/pre&gt;
&lt;p&gt;Next I attempted to restore the database BSON files, with the following
command:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
mongorestore --db=taco taco/taco.bson
&lt;/pre&gt;
&lt;p&gt;But I got the following error:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
connected to: 127.0.0.1
terminate called after throwing an instance of 'std::runtime_error'
  what():  locale::facet::_S_create_c_locale name not valid
Abort (core dumped)
&lt;/pre&gt;
&lt;p&gt;After some research I learned that I needed to export the following
variable before running the restore:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
export LC_ALL=C
&lt;/pre&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/></entry><entry><title>Set Root Password SmartOS Percona MySQL Zone</title><link href="https://russell.ballestrini.net/set-root-password-smartos-percona-mysql-zone/" rel="alternate"/><published>2014-10-11T00:12:00-04:00</published><updated>2014-10-11T00:12:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-10-11:/set-root-password-smartos-percona-mysql-zone/</id><summary type="html">&lt;p&gt;I used project-fifo to launch the &lt;tt class="docutils literal"&gt;percona (14.2.0)&lt;/tt&gt; MySQL dataset. I
couldn't get into the MySQL instance so I reached out on IRC.
Johngrasty, a friendly guy in the &lt;tt class="docutils literal"&gt;#smartos&lt;/tt&gt; IRC channel, provided a
command to display the randomly generated MySQL password emitted to the
zone-init log:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
cat …&lt;/pre&gt;</summary><content type="html">&lt;p&gt;I used project-fifo to launch the &lt;tt class="docutils literal"&gt;percona (14.2.0)&lt;/tt&gt; MySQL dataset. I
couldn't get into the MySQL instance so I reached out on IRC.
Johngrasty, a friendly guy in the &lt;tt class="docutils literal"&gt;#smartos&lt;/tt&gt; IRC channel, provided a
command to display the randomly generated MySQL password emitted to the
zone-init log:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
cat /var/svc/log/system-zoneinit\:default.log | grep MYSQL_PW
&lt;/pre&gt;
&lt;p&gt;I used this initial password to get into the &lt;tt class="docutils literal"&gt;mysql&amp;gt;&lt;/tt&gt; shell and
changed it with this SQL statement:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
mysql&amp;gt; SET PASSWORD = PASSWORD('clear-text-password');
&lt;/pre&gt;
&lt;p&gt;Johngrasty also supplied a snippet of JSON which shows how to declare
root MySQL password:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
{
  ... truncated ...
  &amp;quot;customer_metadata&amp;quot;: {
    &amp;quot;salt-master&amp;quot;: &amp;quot;10.0.0.101&amp;quot;,
    &amp;quot;salt-id&amp;quot;: &amp;quot;mysql1&amp;quot;
  },
  &amp;quot;internal_metadata&amp;quot;: {
    &amp;quot;mysql_pw&amp;quot;: &amp;quot;mypassword&amp;quot;
  }
}
&lt;/pre&gt;
&lt;p&gt;I looked for help in the #project-fifo channel as to why the GUI does
not work for assigning initial MySQL password. MerlinDMC, the author and
operator of &lt;a class="reference external" href="https://datasets.at"&gt;datasets.at&lt;/a&gt;, gave me the following
command to run in the Global Zone to look at a particular zone's
metadata:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
cat /zones/uuid-of-zone-goes-here/config/metadata.json
&lt;/pre&gt;
&lt;p&gt;Turns out the GUI is placing the &lt;tt class="docutils literal"&gt;&amp;quot;mysql_pw&amp;quot;&lt;/tt&gt; parameter into
&lt;tt class="docutils literal"&gt;&amp;quot;customer_metadata&amp;quot;&lt;/tt&gt; instead of &lt;tt class="docutils literal"&gt;&amp;quot;internal_metadata&amp;quot;&lt;/tt&gt; which is
invalid.&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/><category term="Salt"/></entry><entry><title>rabbits</title><link href="https://russell.ballestrini.net/rabbits/" rel="alternate"/><published>2014-09-18T21:11:00-04:00</published><updated>2014-09-18T21:11:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-09-18:/rabbits/</id><summary type="html">&lt;p class="first last"&gt;Digital Rabbit Drawing.&lt;/p&gt;
</summary><content type="html">&lt;img alt="original rabbits" src="/uploads/2014/09/2014-09-18-rabbits.png" /&gt;
</content><category term="misc"/><category term="Art"/></entry><entry><title>reality shattered</title><link href="https://russell.ballestrini.net/reality-shattered/" rel="alternate"/><published>2014-09-18T21:11:00-04:00</published><updated>2014-09-18T21:11:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-09-18:/reality-shattered/</id><summary type="html">&lt;p class="first last"&gt;Emotional overload&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;img alt="image0" src="/uploads/2014/09/2014-09-17-reality-shattered.png" /&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Art"/></entry><entry><title>Heka, World2!</title><link href="https://russell.ballestrini.net/heka-world2/" rel="alternate"/><published>2014-08-23T22:54:00-04:00</published><updated>2014-08-23T22:54:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-08-23:/heka-world2/</id><summary type="html">&lt;p&gt;This article expands on my &lt;a class="reference external" href="/heka-world/"&gt;“Hello World” for Heka&lt;/a&gt; blog post.
Check that one out first if you are new to Heka.&lt;/p&gt;
&lt;p&gt;In this guide we introduce using Heka over the network by utilizing two
Hekad processes on localhost. For discussion purposes we name one of the
Hekad processes &amp;quot;sender …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This article expands on my &lt;a class="reference external" href="/heka-world/"&gt;“Hello World” for Heka&lt;/a&gt; blog post.
Check that one out first if you are new to Heka.&lt;/p&gt;
&lt;p&gt;In this guide we introduce using Heka over the network by utilizing two
Hekad processes on localhost. For discussion purposes we name one of the
Hekad processes &amp;quot;sender&amp;quot; and the other &amp;quot;receiver&amp;quot;.&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;The &amp;quot;sender&amp;quot; will watch a log file and emit messages to localhost TCP
port 9612.&lt;/li&gt;
&lt;li&gt;The &amp;quot;receiver&amp;quot; will listen on localhost TCP port 9612 and emit
message payloads to file.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;hello-heka-file-in-tcp-out.toml (sender):&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# watch /tmp/input.log and output to TCP port 9612 on localhost
[hello_heka_input_log]
type = &amp;quot;LogstreamerInput&amp;quot;
log_directory = &amp;quot;/tmp&amp;quot;
file_match = 'input\.log'

[tcp_out:9612]
type = &amp;quot;TcpOutput&amp;quot;
message_matcher = &amp;quot;TRUE&amp;quot;
address = &amp;quot;127.0.0.1:9612&amp;quot;
#encoder = &amp;quot;hello_heka_output_encoder&amp;quot;
#
#[hello_heka_output_encoder]
#type = &amp;quot;PayloadEncoder&amp;quot;
#append_newlines = false
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;hello-heka-tcp-in-file-out.toml (receiver):&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# listen to TCP port 9612 and emit to /tmp/output.log
[tcp_in:9612]
type = &amp;quot;TcpInput&amp;quot;
parser_type = &amp;quot;message.proto&amp;quot;
decoder = &amp;quot;ProtobufDecoder&amp;quot;
address = &amp;quot;:9612&amp;quot;

[hello_heka_output_log]
type = &amp;quot;FileOutput&amp;quot;
message_matcher = &amp;quot;TRUE&amp;quot;
path = &amp;quot;/tmp/output.log&amp;quot;
perm = &amp;quot;664&amp;quot;
encoder = &amp;quot;hello_heka_output_encoder&amp;quot;

[hello_heka_output_encoder]
type = &amp;quot;PayloadEncoder&amp;quot;
append_newlines = false
&lt;/pre&gt;
&lt;p&gt;Now that we have config files, let us start our hekad processes, Open
three terminals.&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;in terminal 1:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo hekad -config=hello-heka-file-in-tcp-out.toml
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;in terminal 2:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo hekad -config=/tmp/hello-heka-tcp-in-file-out.toml
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;in terminal 3:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
echo 'Heka, World2!' &amp;gt;&amp;gt; /tmp/input.log
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;in terminal 3:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
cat /tmp/output.log
&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Again, like magic, the data echoed into input.log shows up in
output.log. This time the data traveled over TCP between to separate
Hekad processes. I leave changing the configuration to support separate
hosts to the reader.&lt;/p&gt;
&lt;p&gt;By default TCP sender encodes the message with Protobuf
(ProtobufEncoder) and the TCP reciever decodes the message with Protobuf
(ProtobufDecoder).&lt;/p&gt;
&lt;p&gt;In my testing I decided to make the TCP sender use the PayloadEncoder
and then instead of using a second hekad process, I used &lt;tt class="docutils literal"&gt;nc &lt;span class="pre"&gt;-l&lt;/span&gt; 9612&lt;/tt&gt;
to listen on the port. When data was added to &lt;tt class="docutils literal"&gt;/tmp/input.log&lt;/tt&gt; it
showed up in the netcat terminal because hekad was watching the file and
emiting just payload portion of the message to TCP 9612 which netcat was
listening on. I left this configuration in the examples above, simply
uncomment to reproduce.&lt;/p&gt;
&lt;p&gt;Read this for &lt;a class="reference external" href="https://www.foxhop.net/linux-nc-and-python-sockets"&gt;more fun with
netcat&lt;/a&gt;.&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/><category term="Python"/></entry><entry><title>Mailpile Salt States for Ubuntu or Debian</title><link href="https://russell.ballestrini.net/mailpile-salt-states-for-ubuntu-or-debian/" rel="alternate"/><published>2014-08-15T20:32:00-04:00</published><updated>2014-08-15T20:32:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-08-15:/mailpile-salt-states-for-ubuntu-or-debian/</id><summary type="html">&lt;p&gt;I wrote these Salt States to install Mailpile on an Ubuntu host. Fun
fact, it took me 20 minutes to write these states and they worked the
first time I ran them. Disclaimer - I used a throw away server and
wasn't concerned that buckets of packages were installed to the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I wrote these Salt States to install Mailpile on an Ubuntu host. Fun
fact, it took me 20 minutes to write these states and they worked the
first time I ran them. Disclaimer - I used a throw away server and
wasn't concerned that buckets of packages were installed to the system
instead of using a virtualenv.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
cd /opt/Mailpile
./mp --set sys.http_host=0.0.0.0
./mp
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Then open a web browser the IP address of the host running the mp
command and follow the prompts to setup the server/client/app.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;mailpile/init.sls:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# Clone the source repository
mailpile-git-latest:
  git.latest:
    - name: https://github.com/pagekite/Mailpile.git
    - target: /opt/Mailpile

# install the system requirements
mailpile-system-packages:
  pkg.installed:
    - names:
      - make
      - python-imaging
      - python-lxml
      - python-jinja2
      - pep8
      - ruby-dev
      - yui-compressor
      - python-nose
      - spambayes
      - phantomjs
      - python-pip
      - python-mock
      - python-pexpect
      {% if grains['lsb_distrib_release']|float &amp;gt;= 14.04 %}
      - rubygems-integration
      {% else %}
      - rubygems
      {% endif %}

# install some python requirements with pip
mailpile-pip-packages:
  pip.installed:
    - names:
      - pgpdump
      - selenium &amp;gt;= 2.40.0
    - require:
      - pkg: mailpile-system-packages

# install some ruby requirements with gem
mailpile-gem-packages:
  gem.installed:
    - names:
      - therubyracer
      - less
    - require:
      - pkg: mailpile-system-packages
&lt;/pre&gt;
&lt;/p&gt;</content><category term="misc"/><category term="DevOps"/><category term="Python"/><category term="Salt"/></entry><entry><title>You can hack on FreeNAS 9</title><link href="https://russell.ballestrini.net/you-can-hack-on-freenas-9/" rel="alternate"/><published>2014-05-15T01:06:00-04:00</published><updated>2014-05-15T01:06:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-05-15:/you-can-hack-on-freenas-9/</id><summary type="html">&lt;p&gt;This post analyses the FreeNAS 9 code base and discusses the various
places users may feel confident to hack on.&lt;/p&gt;
&lt;p&gt;FreeNAS uses the following software stack:&lt;/p&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;Django&lt;/dt&gt;
&lt;dd&gt;A Python Web Application Framework which complies with WSGI&lt;/dd&gt;
&lt;dt&gt;Nginx&lt;/dt&gt;
&lt;dd&gt;A very fast web server which may act as a reverse proxy server …&lt;/dd&gt;&lt;/dl&gt;</summary><content type="html">&lt;p&gt;This post analyses the FreeNAS 9 code base and discusses the various
places users may feel confident to hack on.&lt;/p&gt;
&lt;p&gt;FreeNAS uses the following software stack:&lt;/p&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;Django&lt;/dt&gt;
&lt;dd&gt;A Python Web Application Framework which complies with WSGI&lt;/dd&gt;
&lt;dt&gt;Nginx&lt;/dt&gt;
&lt;dd&gt;A very fast web server which may act as a reverse proxy server for
HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load
balancer and HTTP cache.&lt;/dd&gt;
&lt;dt&gt;Dojo Toolkit&lt;/dt&gt;
&lt;dd&gt;The Javascript toolkit used to create widgets and handle client side
processing.&lt;/dd&gt;
&lt;dt&gt;FreeBSD&lt;/dt&gt;
&lt;dd&gt;FreeBSD is an advanced computer operating system used to power
modern servers.&lt;/dd&gt;
&lt;dt&gt;Want to hack on the frontend web application?&lt;/dt&gt;
&lt;dd&gt;&lt;p class="first"&gt;Start here if you enjoy Python, or if you really enjoy coding on
Django applications:&lt;/p&gt;
&lt;p class="last"&gt;&lt;a class="reference external" href="https://github.com/freenas/freenas/tree/master/gui"&gt;https://github.com/freenas/freenas/tree/master/gui&lt;/a&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;dt&gt;Want to hack on the GUI?&lt;/dt&gt;
&lt;dd&gt;&lt;p class="first"&gt;Start here if you are a front end developer and enjoy writing HTML,
CSS, and working with Javascript:&lt;/p&gt;
&lt;p class="last"&gt;&lt;a class="reference external" href="https://github.com/freenas/freenas/tree/master/gui/templates"&gt;https://github.com/freenas/freenas/tree/master/gui/templates&lt;/a&gt;&lt;/p&gt;
&lt;/dd&gt;
&lt;dt&gt;Want to change Nginx?&lt;/dt&gt;
&lt;dd&gt;&lt;p class="first"&gt;Take a look here if you would like to review, change, or tune Nginx
on FreeNAS:&lt;/p&gt;
&lt;p class="last"&gt;&lt;a class="reference external" href="https://github.com/freenas/freenas/tree/master/nanobsd/Files/usr/local/etc/nginx"&gt;https://github.com/freenas/freenas/tree/master/nanobsd/Files/usr/local/etc/nginx&lt;/a&gt;
This directory holds the nginx &amp;quot;vhost&amp;quot; config files and CGI parameters.&lt;/p&gt;
&lt;/dd&gt;
&lt;dt&gt;Want to hack on the OS?&lt;/dt&gt;
&lt;dd&gt;&lt;p class="first"&gt;Start here, if you know about &lt;tt class="docutils literal"&gt;FreeBSD&lt;/tt&gt; or operating systems in general:&lt;/p&gt;
&lt;p class="last"&gt;&lt;a class="reference external" href="https://github.com/freenas/freenas/tree/master/nanobsd"&gt;https://github.com/freenas/freenas/tree/master/nanobsd&lt;/a&gt;
This directory seems like a customized and completely version controlled nanoBSD install!&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
</content><category term="misc"/><category term="Code"/><category term="Python"/></entry><entry><title>Nginx with SSL and mixed content errors with upstream WSGI servers</title><link href="https://russell.ballestrini.net/nginx-with-ssl-and-mixed-content-errors-with-upstream-wsgi-servers/" rel="alternate"/><published>2014-05-08T16:28:00-04:00</published><updated>2014-05-08T16:28:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-05-08:/nginx-with-ssl-and-mixed-content-errors-with-upstream-wsgi-servers/</id><summary type="html">&lt;p&gt;Mixed content errors occur because Nginx (the front-end server)
communicates to the upstream WSGI server using http. WSGI does not know
(or care) about the SSL session between Nginx and the user. The WSGI
server will naively generate URIs and serve assets as http.&lt;/p&gt;
&lt;p&gt;To fix mixed content errors, we …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Mixed content errors occur because Nginx (the front-end server)
communicates to the upstream WSGI server using http. WSGI does not know
(or care) about the SSL session between Nginx and the user. The WSGI
server will naively generate URIs and serve assets as http.&lt;/p&gt;
&lt;p&gt;To fix mixed content errors, we need to communicate the inbound request
scheme or configure the WSGI server to always use https.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;waitress&lt;/strong&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Waitress is meant to be a production-quality pure-Python WSGI server
with very acceptable performance.&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;It commonly powers Pyramid and Substance D deployments.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;To configure waitress to always use https in code:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
from waitress import serve
serve(wsgiapp, host='0.0.0.0', port=8080, url_scheme='https')
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;configure waitress to always use https in paste deploy compatible
configuration file:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
[server:main]
host = 127.0.0.1
port = 6543
url_scheme = https
&lt;/pre&gt;
&lt;/p&gt;</content><category term="misc"/><category term="Code"/><category term="Guide"/><category term="Python"/></entry><entry><title>How to patch Heartbleed OpenSSL defect (libssl) on Ubuntu</title><link href="https://russell.ballestrini.net/how-to-patch-heartbleed-openssl-defect-libssl-on-ubuntu/" rel="alternate"/><published>2014-04-08T22:42:00-04:00</published><updated>2014-04-08T22:42:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-04-08:/how-to-patch-heartbleed-openssl-defect-libssl-on-ubuntu/</id><summary type="html">&lt;p&gt;Lots of people claim that you need to upgrade openssl package, but this
will not fix the issue.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;The issue is not the openssl package, it is one of the libraries that
the package relies on (libssl).&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;a class="reference external" href="https://www.ubuntu.com/usn/usn-2165-1/"&gt;https://www.ubuntu.com/usn/usn-2165-1/&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The output of &lt;tt class="docutils literal"&gt;openssl version &lt;span class="pre"&gt;-a&lt;/span&gt;&lt;/tt&gt; command …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Lots of people claim that you need to upgrade openssl package, but this
will not fix the issue.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;The issue is not the openssl package, it is one of the libraries that
the package relies on (libssl).&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;a class="reference external" href="https://www.ubuntu.com/usn/usn-2165-1/"&gt;https://www.ubuntu.com/usn/usn-2165-1/&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The output of &lt;tt class="docutils literal"&gt;openssl version &lt;span class="pre"&gt;-a&lt;/span&gt;&lt;/tt&gt; command should have a &lt;tt class="docutils literal"&gt;built on&lt;/tt&gt;
date older then &lt;tt class="docutils literal"&gt;Mon Apr&amp;nbsp; 7 20:33:29 UTC 2014&lt;/tt&gt;. After patching openssl
we still see the vulnerable date:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
openssl version -a | egrep &amp;quot;OpenSSL|built&amp;quot;
OpenSSL 1.0.1 14 Mar 2012
built on: Tue Aug 21 05:18:48 UTC 2012
&lt;/pre&gt;
&lt;p&gt;Now we patch &lt;tt class="docutils literal"&gt;libssl1.0.0&lt;/tt&gt;:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo apt-get update
sudo apt-get install libssl1.0.0
&lt;/pre&gt;
&lt;p&gt;Notice the patched &lt;tt class="docutils literal"&gt;built on&lt;/tt&gt; date:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
openssl version -a | egrep &amp;quot;OpenSSL|built&amp;quot;
OpenSSL 1.0.1 14 Mar 2012
built on: Mon Apr  7 20:33:29 UTC 2014
&lt;/pre&gt;
&lt;p&gt;In my case I used a Salt remote execution to patch, verify, and restart
nginx on all of my 14 hosts:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo salt '*' cmd.run 'apt-get update'
&lt;/pre&gt;
&lt;pre class="literal-block"&gt;
sudo salt '*' cmd.run 'apt-get -y install libssl1.0.0'
&lt;/pre&gt;
&lt;pre class="literal-block"&gt;
sudo salt '*' cmd.run 'openssl version -a | egrep &amp;quot;OpenSSL|built&amp;quot;'
&lt;/pre&gt;
&lt;pre class="literal-block"&gt;
sudo salt '*' service.restart nginx
&lt;/pre&gt;
&lt;p&gt;
&lt;center&gt;&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;img alt="image0" src="https://imgs.xkcd.com/comics/heartbleed.png" /&gt;&lt;/div&gt;
&lt;div class="line"&gt;xkcd 1353&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/center&gt;
&lt;/p&gt;</content><category term="misc"/><category term="Guide"/><category term="Security"/><category term="Salt"/></entry><entry><title>IRC Bot (Foxbot) runs canned remote executions using Salt Stack</title><link href="https://russell.ballestrini.net/irc-bot-foxbot-runs-canned-remote-executions-using-salt-stack/" rel="alternate"/><published>2014-03-15T16:33:00-04:00</published><updated>2014-03-15T16:33:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-03-15:/irc-bot-foxbot-runs-canned-remote-executions-using-salt-stack/</id><summary type="html">&lt;p&gt;I extended my IRC Bot Foxbot today to allow it to run canned remote
executions on behalf of users in an IRC channel. This is only a
prototype or proof-of-concept. Be very careful not to allow users to
inject their own commands. Foxbot must be running on the Salt Master …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I extended my IRC Bot Foxbot today to allow it to run canned remote
executions on behalf of users in an IRC channel. This is only a
prototype or proof-of-concept. Be very careful not to allow users to
inject their own commands. Foxbot must be running on the Salt Master and
must be running as the same user that runs the salt-master daemon.&lt;/p&gt;
&lt;p&gt;The code lives here:
&lt;a class="reference external" href="https://bitbucket.org/russellballestrini/foxbot/src/tip/plugins/checks.py"&gt;foxbot/plugins/checks.py&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Example usage:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;16:18:25            * | russell checks uptime minion2.foxhop.net
16:18:25       foxbot | minion2.foxhop.net:  16:18:25 up 496 days, 22:36, 17 users,  load average: 0.33, 0.70, 0.87
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;16:29:10            * | russell checks procs *
16:29:17       foxbot | minion2.foxhop.net: PROCS WARNING: 165 processes
16:29:17       foxbot | minion5.foxhop.net: PROCS CRITICAL: 309 processes
&lt;/pre&gt;&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Salt"/></entry><entry><title>Filter Salt Stack Return Data Output</title><link href="https://russell.ballestrini.net/filter-salt-stack-return-data-output/" rel="alternate"/><published>2014-03-13T18:21:00-04:00</published><updated>2014-03-13T18:21:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-03-13:/filter-salt-stack-return-data-output/</id><summary type="html">&lt;p&gt;Sometimes you only want to see what has changed, and that is OK.&lt;/p&gt;
&lt;p&gt;Create a file like this:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;filter.py&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre class="literal-block"&gt;
#!/usr/bin/python

from json import loads
from json import dumps

import fileinput

stdin_lines = [line for line in fileinput.input()]

ret = loads(''.join(stdin_lines))

for minion_id, data in ret.items …&lt;/pre&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;Sometimes you only want to see what has changed, and that is OK.&lt;/p&gt;
&lt;p&gt;Create a file like this:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;filter.py&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre class="literal-block"&gt;
#!/usr/bin/python

from json import loads
from json import dumps

import fileinput

stdin_lines = [line for line in fileinput.input()]

ret = loads(''.join(stdin_lines))

for minion_id, data in ret.items():
    print(minion_id)
    print('='*len(minion_id))
    for key, value in ret[minion_id].items():
        if value['changes'] or value['result'] == False:
            print('')
            print(dumps(value, indent=4))
            print('')
&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;Make the file executable:&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre class="literal-block"&gt;
chmod 755 filter.py
&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;Execute your remote execution like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre class="literal-block"&gt;
sudo salt-call --out=json state.highstate | ./filter.py
&lt;/pre&gt;
&lt;/p&gt;&lt;pre class="literal-block"&gt;
sudo salt '*' --out=json  --timeout=60 --static state.highstate | ./filter.py
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;The flags &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;--timeout=60&lt;/span&gt;&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;--static&lt;/span&gt;&lt;/tt&gt; will cause the Salt
command to block until the specified seconds for each minion to
return results. We then pipe the returned JSON into our
&lt;tt class="docutils literal"&gt;filter.py&lt;/tt&gt; script to filter out only the changes and failures!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Profit!&lt;/p&gt;
&lt;p&gt;Change the conditional depending on what you want. For example, for just
failures do this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre class="literal-block"&gt;
if value['result'] == False:
&lt;/pre&gt;
&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Example output:&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre class="literal-block"&gt;
# sudo salt 'graphite.foxhop.net' --out=json --static --timeout=60 state.highstate | ./filter.py

graphite.foxhop.net
===================

{
    &amp;quot;comment&amp;quot;: &amp;quot;File /tmp/taco updated&amp;quot;,
    &amp;quot;__run_num__&amp;quot;: 15,
    &amp;quot;changes&amp;quot;: {
        &amp;quot;diff&amp;quot;: &amp;quot;New file&amp;quot;,
        &amp;quot;mode&amp;quot;: &amp;quot;0640&amp;quot;
    },
    &amp;quot;name&amp;quot;: &amp;quot;/tmp/taco&amp;quot;,
    &amp;quot;result&amp;quot;: true
}
&lt;/pre&gt;
&lt;/p&gt;&lt;/blockquote&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/><category term="Salt"/></entry><entry><title>Replace the Nagios Scheduler and NRPE with Salt Stack</title><link href="https://russell.ballestrini.net/replace-the-nagios-scheduler-and-nrpe-with-salt-stack/" rel="alternate"/><published>2014-03-08T15:53:00-05:00</published><updated>2014-03-08T15:53:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-03-08:/replace-the-nagios-scheduler-and-nrpe-with-salt-stack/</id><summary type="html">&lt;p&gt;Note: I will update this post as I progress.&lt;/p&gt;
&lt;p&gt;So the idea is to use Salt Stack's remote execution to communicate with
all nodes and run the Nagios checks and collect the return output
instead of using the NRPE client/service protocol. This reduces the
number of agents running on …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Note: I will update this post as I progress.&lt;/p&gt;
&lt;p&gt;So the idea is to use Salt Stack's remote execution to communicate with
all nodes and run the Nagios checks and collect the return output
instead of using the NRPE client/service protocol. This reduces the
number of agents running on each host and appears significantly more
secure. Salt Stack uses public/private crypto on top of the ZMQ
publisher/subscriber model. This means the communication transport is
very fast, very secure, and all nodes will run checks in parallel!&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;First, we need to install the Nagios checks and plugins on each host
or minion.&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;I used the following State Formula on my Ubuntu and Debian hosts:&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;salt://nagios/plugins.sls&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre class="literal-block"&gt;
nagios-plugins:
  pkg:
    - installed

nagios-plugins-extra:
  pkg:
    - installed
&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;salt://top.sls&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre class="literal-block"&gt;
base:
  '*'
    - nagios.plugins
&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;I kicked off a highstate (&lt;tt class="docutils literal"&gt;salt '*' state.highstate&lt;/tt&gt;) on all minions
and eventually they all returned in the affirmative. We now have all the
Nagios plugins and checks installed on each host.&lt;/p&gt;
&lt;p&gt;This next part is FUN, We run a check on every host concurrently.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This particular check counts the number of processes running on each
host and warns at 150 and criticals at 200.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
salt '*' --out=json --static cmd.run_all '/usr/lib/nagios/plugins/check_procs -w 150 -c 200'
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Output static JSON - Wait for all minions to return and then
generate a single JSON object.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
{
    &amp;quot;graphite.foxhop.net&amp;quot;: {
        &amp;quot;pid&amp;quot;: 24356,
        &amp;quot;retcode&amp;quot;: 0,
        &amp;quot;stderr&amp;quot;: &amp;quot;&amp;quot;,
        &amp;quot;stdout&amp;quot;: &amp;quot;PROCS OK: 78 processes&amp;quot;
    },
    &amp;quot;akuma.foxhop.net&amp;quot;: {
        &amp;quot;pid&amp;quot;: 4610,
        &amp;quot;retcode&amp;quot;: 1,
        &amp;quot;stderr&amp;quot;: &amp;quot;&amp;quot;,
        &amp;quot;stdout&amp;quot;: &amp;quot;PROCS WARNING: 158 processes&amp;quot;
    },
    &amp;quot;ken.foxhop.net&amp;quot;: {
        &amp;quot;pid&amp;quot;: 31254,
        &amp;quot;retcode&amp;quot;: 2,
        &amp;quot;stderr&amp;quot;: &amp;quot;&amp;quot;,
        &amp;quot;stdout&amp;quot;: &amp;quot;PROCS CRITICAL: 392 processes&amp;quot;
    }
}
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Review the &lt;a class="reference external" href="https://nagios.sourceforge.net/docs/3_0/pluginapi.html"&gt;Nagios Plugin
API&lt;/a&gt; for
more information about return codes and STDOUT formats.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Last, we take this JSON data perform further processing or display it in
a meaningful way.&lt;/p&gt;
&lt;p&gt;For example, we could generate an HTML/JavaScript dashboard from the raw
JSON objects. We could cut metrics out of the STDOUT and persist
historic values in a data store. We could push the data into another
system like Graphite or even send an email alerts.&lt;/p&gt;
&lt;p&gt;This solution has all the perks of Nagios without ANY of the cons!&lt;/p&gt;
&lt;p&gt;Returning the check output data to the CLI isn't super useful for
further processing, so I hacked in a way to return data back to the
master using the encrypted zeromq bus.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;_returners/zeromq_return.py&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# -*- coding: utf-8 -*-
'''
The zeromq returner will send return data back to the Salt Master over the
Encrypted 0MQ event bus with a custom tag for filtering on the other end.

Basically after the remote execution finishes, the ret data is &amp;quot;packaged&amp;quot; into
a special &amp;quot;envelope&amp;quot; which triggers the local Salt Minion Daemon to
forward the ret data to the Salt Master's event bus.

The &amp;quot;package&amp;quot; basically wraps the ret data and uses the tag 'fire_master'.

For example, a ret data object from the execution of test.ping
would be &amp;quot;packaged&amp;quot; like this::

  ret = {
    'graphite.foxhop.net': true
  }

  ret['tag'] = 'third-party'

  package = {
    'events': [ ret ],
    'tag': None,
    'pretag': None,
    'data': None
  }

The Salt Minion Daemon will forward this package to the Salt Master
where a 3rd party script may be filtering on the specified internal event tag.

To use the zeromq returner, append '--return zeromq' to the salt command. ex::

  salt --return zeromq '*' test.ping

TODO:

 figure out a way for user to define custom tag for filtering ...
 Most returners use the Salt Minion config file to supply returner
 details... that is not optimal, it would be ideal if the custom tag
 could be supplied on the CLI when the remote execution is run, like::

   --return=zeromq --tag=mytag

'''

# needed to log to log file
import logging

# needed for config to opts processing
import os
import salt.syspaths as syspaths
from salt.config import minion_config

# needed to send events over ZMQ
import salt.utils.event

log = logging.getLogger(__name__)

# needed to define the module's virtual name
__virtualname__ = 'zeromq'

def __virtual__():
    return __virtualname__


def returner(ret):
    '''
    Send the return data to the Salt Master over the encrypted
    0MQ bus with custom tag for 3rd party script filtering.
    '''

    # get opts from minion config file, supports minion.d drop dir!
    opts = minion_config(os.path.join(syspaths.CONFIG_DIR, 'minion'))

    # TODO: this needs to be customizable!
    tag = 'third-party'

    # add custom tag to return data for filtering
    ret['tag'] = tag

    # multi event example, supports a list of event ret objects.
    # single event does not currently expand/filter properly on Master side.
    package = {
      #'id': opts['id'],
      'events': [ ret ],
      'tag': None,
      'pretag': None,
      'data': None
    }

    # opts must contain valid minion ID else it binds to invalid 0MQ socket.
    event = salt.utils.event.SaltEvent('minion', **opts)

    # Fire event payload with 'fire_master' tag which triggers the
    # salt-minion daemon to forward payload to the master event bus!
    event.fire_event(package, 'fire_master')
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Next we run a third party application on the Salt Master which
subscribes to our events by filtering on the special tag
('third-party').&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;listen_to_master_bus.py&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# event libary for events over ZMQ
import salt.utils.event

# create event object, attach to master socket ...
event = salt.utils.event.MasterEvent('/var/run/salt/master')

tag = 'third-party'

print('Listening for events tagged \'{}\' on Salt Master bus.'.format(tag))

# generator iterator yields events forever, we filter on tag
for data in event.iter_events(tag=tag):
    print(data)
&lt;/pre&gt;
&lt;p&gt;This small application just prints the incoming return data, but it
could easily be expanded to process the incoming return data and persist
it somewhere.&lt;/p&gt;
&lt;p&gt;Open two terminals on the Salt Master host.&lt;/p&gt;
&lt;blockquote&gt;
&lt;pre class="literal-block"&gt;
# on terminal 1 run:
python listen_to_master_bus.py
&lt;/pre&gt;
&lt;pre class="literal-block"&gt;
# on terminal 2 run:
salt '*' --return zeromq cmd.run_all '/usr/lib/nagios/plugins/check_procs -w 150 -c 200'
&lt;/pre&gt;
&lt;p&gt;Same remote execution check as before but now our new returner will
make data appear in terminal 1!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This zeromq returner is more of a proof-of-concept. I think the salt
remote execution command line tool should allow end-users to provide a
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;--tag&lt;/span&gt;&lt;/tt&gt; so that data may be feed directly back to a third-party script
listening to the Salt Master's event bus which filters on the particular
tag. My next step is to look into what it would take to build in this
functionality.&lt;/p&gt;
&lt;p&gt;In the future I want to rig up the Salt scheduler to invoke these remote
execution checks on a steady and predictable cadence. Nagios
historically runs checks every 5 minutes. The Salt scheduler will allow
us schedule different checks with different frequencies. For example, I
might want my load checks and metrics to be collected every 10 secs, but
my disk capacity usage checked every 2 minutes. This fine-grain control
is super powerful!&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/><category term="Salt"/></entry><entry><title>Configuration Management and the Golden Image</title><link href="https://russell.ballestrini.net/configuration-management-and-the-golden-image/" rel="alternate"/><published>2014-02-21T17:19:00-05:00</published><updated>2014-02-21T17:19:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-02-21:/configuration-management-and-the-golden-image/</id><summary type="html">&lt;p&gt;When operations first became a thing, system administrators stood up
servers using a base image from their favourite distribution. Things
were done manually. Some administrators created their own distros, some
wrote customised shell scripts to be run once-and-only-once to provision
software and settings. This method worked, but it was slow …&lt;/p&gt;</summary><content type="html">&lt;p&gt;When operations first became a thing, system administrators stood up
servers using a base image from their favourite distribution. Things
were done manually. Some administrators created their own distros, some
wrote customised shell scripts to be run once-and-only-once to provision
software and settings. This method worked, but it was slow, manual, and
the human element caused defects. Then the request came in to stand up
100 servers the exact same way.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;System admins resolved this large request by coming up with a Standard
Operating Environment (SOE) image which would end up becoming the
&amp;quot;golden image&amp;quot;. This golden image was the source of truth, and we
built hundreds, thousands of machines this way. All systems were the
same, or rather started out the same, but it didn't take long for
deviations occur.&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;(Norton Ghost, DD, ISOs)&lt;/div&gt;
&lt;div class="line"&gt;&lt;a class="reference external" href="https://en.wikipedia.org/wiki/Standard_Operating_Environment"&gt;https://en.wikipedia.org/wiki/Standard_Operating_Environment&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The golden image by itself was a flawed idea. The task of creating a
golden image was difficult and required a lot of work. Not only was it
technical, but there was a lot of politics involved in what was worthy
of inclusion. We didn't want to add cruft to every machine. Also the
golden image was only updated every couple of years, so it would quickly
become outdated and it still required provisioning scripts. Systems
already in production didn't get configuration updates that were
recently added to the golden image. There had to be a better way, and
there was...&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Some novel and smart system administrators who also knew how to
program decided that they didn't want to maintain a golden image and
deal with all the headaches involved and opted to use the light weight
distribution image and then build sophisticated remote execution
software to manage and maintain each server's configuration. Later on
they built configuration management systems on top of this remote
execution layer and were finally able to keep each system up-to-date,
regardless of when it was deployed. They were geniuses and everyone
who knew anything quickly rushed to implement remote execution and
configuration management. It was the right way to manage servers,
until the cloud drifted in.&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;(SaltStack, Ansible, Puppet, Chef, CFEngine)&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;In the day of cloud computing, we needed to scale up and down servers in
seconds. A complex configuration manifest could take hours to run from
start to finish. Configuration management was too slow. Each server
needed to download, install, and configure the software stack, in
real-time. Sure we could spin up multiple machines in parallel, but it
was still slow. We started to look back at the golden age of the golden
image, when a server was built and booted in moments. How could we pair
the speed of the golden image with the flexibility of configuration
management?&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;In the future could we use configuration management manifests and
revision control to document how to build the golden image and then
take a snapshot? Could we overlay multiple layers or dataset of images
on top of each other? Perhaps we take a step way back and create
golden images with a combination of a highly customizable distribution
like Gentoo and configuration management.&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;(Joyant SmartOS zone datasets, Docker container images, Vagrant box
files, AWS AMI, Digital Ocean Snapshots, etc)&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Greatest Hits"/><category term="Opinion"/><category term="AWS"/><category term="Salt"/><category term="Docker"/></entry><entry><title>Automatic Backups</title><link href="https://russell.ballestrini.net/automatic-backups/" rel="alternate"/><published>2014-02-07T17:14:00-05:00</published><updated>2014-02-07T17:14:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-02-07:/automatic-backups/</id><summary type="html">&lt;p class="first last"&gt;My tools for creating automatic backups for various systems&lt;/p&gt;
</summary><content type="html">&lt;p&gt;I maintain many tools for creating automatic backups for various systems.
This page will act as a hub to each of those solutions.&lt;/p&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;&lt;a class="reference external" href="/tar-back"&gt;tar-back&lt;/a&gt;:&lt;/dt&gt;
&lt;dd&gt;tar-back is a backup utility to tar and gzip target filesystems. It
supports a custom retention, filter exclusions, and backup
directory.&lt;/dd&gt;
&lt;dt&gt;&lt;a class="reference external" href="/virt-back-a-python-libvirt-backup-utility-for-kvm-xen-virtualbox/"&gt;virt-back&lt;/a&gt;:&lt;/dt&gt;
&lt;dd&gt;virt-back virt-back is a python application that uses the libvirt
API to safely shutdown, gzip, and restart guests. Supports KVM,
QEMU, XEN, Virtualbox, and more.&lt;/dd&gt;
&lt;dt&gt;&lt;a class="reference external" href="/mysql-back"&gt;mysql-back&lt;/a&gt;:&lt;/dt&gt;
&lt;dd&gt;mysql-back is a backup utility script to dump (backup) and gzip
every MySQL database on a host.&lt;/dd&gt;
&lt;dt&gt;&lt;a class="reference external" href="/backup-all-virtual-machines-on-a-smartos-hypervisor-with-smart-back-sh/"&gt;smart-back&lt;/a&gt;:&lt;/dt&gt;
&lt;dd&gt;smart-back is a utility used to backup of every virtual machine on a
SmartOS hypervisor.&lt;/dd&gt;
&lt;/dl&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Greatest Hits"/><category term="Python"/></entry><entry><title>tar-back</title><link href="https://russell.ballestrini.net/tar-back/" rel="alternate"/><published>2014-02-07T16:49:00-05:00</published><updated>2014-02-07T16:49:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-02-07:/tar-back/</id><summary type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;tar-back&lt;/span&gt;&lt;/tt&gt; is a backup utility to tar and gzip target filesystems.&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;It supports a custom retention, filter exclusions, and backup
directory.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;I use &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;tar-back&lt;/span&gt;&lt;/tt&gt; in combination with cron to perform regular backups
of all localhost filesystems into &lt;tt class="docutils literal"&gt;/archive/fs&lt;/tt&gt;. I then have a central
long term storage server that collects …&lt;/p&gt;</summary><content type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;tar-back&lt;/span&gt;&lt;/tt&gt; is a backup utility to tar and gzip target filesystems.&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;It supports a custom retention, filter exclusions, and backup
directory.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;I use &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;tar-back&lt;/span&gt;&lt;/tt&gt; in combination with cron to perform regular backups
of all localhost filesystems into &lt;tt class="docutils literal"&gt;/archive/fs&lt;/tt&gt;. I then have a central
long term storage server that collects the &lt;tt class="docutils literal"&gt;/archive&lt;/tt&gt; partition from
every host.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tar-back:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
#!/usr/bin/env python

DESCRIPTION = &amp;quot;&amp;quot;&amp;quot;A backup utility to tar and gzip
target filesystems. Custom retention, filter exclusions and
backup directory.
&amp;quot;&amp;quot;&amp;quot;

from os import path
from sys import exit
from shutil import move
from optparse import OptionParser
import tarfile

def filter_exclusions( tarinfo ):
    &amp;quot;&amp;quot;&amp;quot;Accept tarinfo, return tarinfo or None&amp;quot;&amp;quot;&amp;quot;
    if o.filters:
        for filtr in o.filters:
            if filtr in tarinfo.name:
                return None
    return tarinfo

def exclude_exclusions( filename ):
    &amp;quot;&amp;quot;&amp;quot;Accept filename, return True or False&amp;quot;&amp;quot;&amp;quot;
    &amp;quot;&amp;quot;&amp;quot;support for python 2.6 or lower&amp;quot;&amp;quot;&amp;quot;
    if o.filters:
        for filtr in o.filters:
            if filtr in filename:
                return True
    return False

def file_rotate( target, retention = 3 ):
    &amp;quot;&amp;quot;&amp;quot;file rotation routine&amp;quot;&amp;quot;&amp;quot;
    for i in range( retention-2, 0, -1 ): # count backwards
        old_name = &amp;quot;%s.%s&amp;quot; % ( target, i )
        new_name = &amp;quot;%s.%s&amp;quot; % ( target, i + 1 )
        try: move( old_name, new_name  )
        except IOError: pass
    move( target, target + '.1' )

if __name__ == '__main__':

    p = OptionParser()  # create an option parser object

    p.set_description( DESCRIPTION )

    p.add_option( '-t', '--targets',
      help='list of target filesystems to backup (coma delimited)',
      default=None, dest='targets', metavar='&amp;quot;/home&amp;quot;',
    )

    p.add_option( '-b', '--backup-dir',
      help='path to backup directory',
      dest='backdir', metavar='&amp;quot;PATH&amp;quot;',
    )

    p.add_option( '-f', '--filters',
      help='list of filter patterns to exclude (coma delimited)',
      default=None, dest='filters', metavar='&amp;quot;.mp3&amp;quot;',
    )

    p.add_option( '-r', '--retention',
      help='backups to retain [default: 3]',
      default=3, type='int', dest='retention', metavar='amount',
    )

    o, args = p.parse_args()  # parse options and args

    extension = '.tar.gz'

    if not o.targets:
        exit( &amp;quot;missing filesystem target, run --help&amp;quot; )
    if not o.backdir:
        exit( &amp;quot;missing backup directory, run --help&amp;quot; )
    if not path.isdir( o.backdir ):
        exit( &amp;quot;backup-dir %s does not exist&amp;quot; % o.backdir )

    if o.filters:
        o.filters = o.filters.split(',')

    o.targets = o.targets.split(',')

    for target in o.targets:
        slug = target.replace( '/', '-' ).lstrip( '-' ) # change / to -

        if slug == '': slug = 'r' # / slug is empty (root)

        tarpath = path.join( o.backdir, slug + extension )

        if path.isfile( tarpath ):
            file_rotate( tarpath, o.retention )

        tar = tarfile.open( tarpath, 'w:gz' )
        try:
            tar.add( target, filter=filter_exclusions )
        except TypeError:
            tar.add( target, exclude=exclude_exclusions )
        tar.close()
&lt;/pre&gt;
&lt;/p&gt;</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/></entry><entry><title>mysql-back</title><link href="https://russell.ballestrini.net/mysql-back/" rel="alternate"/><published>2014-02-07T16:39:00-05:00</published><updated>2014-02-07T16:39:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-02-07:/mysql-back/</id><summary type="html">&lt;p&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;mysql-back&lt;/span&gt;&lt;/tt&gt;is a backup utility script to dump (backup) and gzip
every MySQL database on a host.&lt;/p&gt;
&lt;p&gt;I use &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;mysql-back&lt;/span&gt;&lt;/tt&gt; in combination with cron to perform regular
database dumps of MySQL servers to the &lt;tt class="docutils literal"&gt;/archive/db&lt;/tt&gt; partition on
localhost. I then have a central long term storage server that collects …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;mysql-back&lt;/span&gt;&lt;/tt&gt;is a backup utility script to dump (backup) and gzip
every MySQL database on a host.&lt;/p&gt;
&lt;p&gt;I use &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;mysql-back&lt;/span&gt;&lt;/tt&gt; in combination with cron to perform regular
database dumps of MySQL servers to the &lt;tt class="docutils literal"&gt;/archive/db&lt;/tt&gt; partition on
localhost. I then have a central long term storage server that collects
the &lt;tt class="docutils literal"&gt;/archive&lt;/tt&gt; partition from every host.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;mysql-back:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
#!/bin/bash

USER=&amp;quot;root&amp;quot;
PASSWORD=&amp;quot;&amp;quot;
TODAY=`date +%Y-%m-%d`
OUTPUTDIR=&amp;quot;/archive/db/$TODAY&amp;quot;
MYSQLDUMP=`which mysqldump`
MYSQL=`which mysql`
GZIP=`which gzip`

mkdir $OUTPUTDIR

# get a list of databases
databases=`$MYSQL --user=$USER --password=$PASSWORD \
 -e &amp;quot;SHOW DATABASES;&amp;quot; | tr -d &amp;quot;| &amp;quot; | grep -v Database`

# dump each database in turn
for db in $databases; do
    $MYSQLDUMP --force --opt --user=$USER --password=$PASSWORD \
    --databases $db &amp;gt; &amp;quot;$OUTPUTDIR/$db.sql&amp;quot;
done

# compress all of the files
$GZIP $OUTPUTDIR/*

# clean up all dumps 30 days old
find /archive/db -ctime +30 -type d | xargs rm -rf
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;If you ever need to restore, like in my case when I moved all my
databases over to a brand new Percona MySQL SmartOS zone, I used the
following command:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
zcat *sql.gz | mysql -u root -p
&lt;/pre&gt;
&lt;/p&gt;</content><category term="misc"/><category term="Code"/><category term="DevOps"/></entry><entry><title>The Three Deployment Management Strategies</title><link href="https://russell.ballestrini.net/the-three-deployment-management-strategies/" rel="alternate"/><published>2014-02-03T15:06:00-05:00</published><updated>2014-02-03T15:06:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-02-03:/the-three-deployment-management-strategies/</id><summary type="html">&lt;p&gt;There are three deployment management strategies that could be used to
maintain a system. Each has pros and cons which I outline in this
document.&lt;/p&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;&lt;strong&gt;run once&lt;/strong&gt;&lt;/dt&gt;
&lt;dd&gt;&lt;p class="first"&gt;A proceedure that is run once and only once to setup a system's
configuration values and settings. A semaphore or flag generally
blocks …&lt;/p&gt;&lt;/dd&gt;&lt;/dl&gt;</summary><content type="html">&lt;p&gt;There are three deployment management strategies that could be used to
maintain a system. Each has pros and cons which I outline in this
document.&lt;/p&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;&lt;strong&gt;run once&lt;/strong&gt;&lt;/dt&gt;
&lt;dd&gt;&lt;p class="first"&gt;A proceedure that is run once and only once to setup a system's
configuration values and settings. A semaphore or flag generally
blocks repeated executions to prevent an undesirable outcome.&lt;/p&gt;
&lt;p class="last"&gt;Development Difficulty: LOW - binary state aware (has run/has not
run)&lt;/p&gt;
&lt;/dd&gt;
&lt;dt&gt;&lt;strong&gt;run always&lt;/strong&gt;&lt;/dt&gt;
&lt;dd&gt;&lt;p class="first"&gt;A proceedure that is safe to run multiple times but uses a brute
force method to setup a system's configuration and settings. It will
self-heal at the expense of clobbering existing state.&lt;/p&gt;
&lt;p class="last"&gt;Development Difficulty: MEDIUM - not state aware - attention must be
spent to allow multiple executions&lt;/p&gt;
&lt;/dd&gt;
&lt;dt&gt;&lt;strong&gt;run as needed&lt;/strong&gt;&lt;/dt&gt;
&lt;dd&gt;&lt;p class="first"&gt;A proceedure that is safe to run multiple times and checks a
system's configuration and settings before it sets a system's
configuration or settings. &amp;quot;checks before it steps&amp;quot;. It will
self-heal only the values it needs to.&lt;/p&gt;
&lt;p class="last"&gt;Development Difficulty: HIGH - state aware - must know how to both
get and set values and make desicions based on what it finds.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Opinion"/></entry><entry><title>How to reset HP iLO Lights-Out User and Password Settings with IPMItool</title><link href="https://russell.ballestrini.net/how-to-reset-hp-ilo-lights-out-user-and-password-settings-with-ipmitools/" rel="alternate"/><published>2014-01-25T09:18:00-05:00</published><updated>2014-01-25T09:18:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-01-25:/how-to-reset-hp-ilo-lights-out-user-and-password-settings-with-ipmitools/</id><summary type="html">&lt;dl class="docutils"&gt;
&lt;dt&gt;Warning:&lt;/dt&gt;
&lt;dd&gt;Do NOT follow guides that suggest to make a DOS boot disk, this is over complicated.&lt;/dd&gt;
&lt;/dl&gt;
&lt;p&gt;Use the &lt;tt class="docutils literal"&gt;ipmitool&lt;/tt&gt; which ships with most Unix based operating systems.
I tested on SmartOS and Ubuntu Linux. Use a live boot disk if you must.&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Run &lt;tt class="docutils literal"&gt;ipmitool user list&lt;/tt&gt; to list all …&lt;/li&gt;&lt;/ol&gt;</summary><content type="html">&lt;dl class="docutils"&gt;
&lt;dt&gt;Warning:&lt;/dt&gt;
&lt;dd&gt;Do NOT follow guides that suggest to make a DOS boot disk, this is over complicated.&lt;/dd&gt;
&lt;/dl&gt;
&lt;p&gt;Use the &lt;tt class="docutils literal"&gt;ipmitool&lt;/tt&gt; which ships with most Unix based operating systems.
I tested on SmartOS and Ubuntu Linux. Use a live boot disk if you must.&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Run &lt;tt class="docutils literal"&gt;ipmitool user list&lt;/tt&gt; to list all users installed on the iLO.&lt;/li&gt;
&lt;li&gt;Choose a user ID from the list and run &lt;tt class="docutils literal"&gt;ipmitool user set password [ID]&lt;/tt&gt;.&lt;/li&gt;
&lt;li&gt;Last, make sure the user is enabled with &lt;tt class="docutils literal"&gt;ipmitool user enable [ID]&lt;/tt&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Here is a complete set of commands I used to reset user ID 6:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
[root&amp;#64;1c-c1-de-f0-ad-36 /opt]# ipmitool user list
ID  Name             Callin  Link Auth  IPMI Msg   Channel Priv Limit
2   ROUSER           true    false      false      Unknown (0x00)
3   USERID           true    false      false      Unknown (0x00)
4   OEM              true    false      false      Unknown (0x00)
5   Operator         true    false      false      Unknown (0x00)
6   admin            true    false      false      Unknown (0x00)
... truncated ...
15  admin            true    false      false      Unknown (0x00)
16  OEM              true    false      false      Unknown (0x00)

[root&amp;#64;1c-c1-de-f0-ad-36 /opt]# ipmitool user set password 6 mynew-password

[root&amp;#64;1c-c1-de-f0-ad-36 /opt]# ipmitool user enable 6
&lt;/pre&gt;
&lt;p&gt;Next I logged into the web GUI and cleaned up this crazy list of users.
You may also need to change the privileges on a user ID to grant admin
level access. Run &lt;tt class="docutils literal"&gt;ipmitool user&lt;/tt&gt; for a list of possible sub-commands.&lt;/p&gt;
&lt;p&gt;After I finished poking around with the web GUI, I decided to learn a
bit more about IPMI and the &lt;tt class="docutils literal"&gt;ipmitool&lt;/tt&gt; command.&lt;/p&gt;
&lt;p&gt;I figured out how to get sensor information over the LAN using this
command:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo ipmitool -I lan -H guy-ilo.foxhop.net -U admin sensor
&lt;/pre&gt;
&lt;p&gt;I was even able to log into the server OS via Serial-over-LAN (SOL)
using this command:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo ipmitool -I lanplus -H guy-ilo.foxhop.net -U admin sol activate
&lt;/pre&gt;
&lt;p&gt;Serial-over-LAN completely resolves the need for a complicated JAVA/KVM
(keyboard video mouse) setup, I was able to reboot the server and watch
the machine POST over the serial connection! Now you don't need to shell
out $229+ on an Advanced 1 yr single server Licence for HP Lights-Out 100i (LO100i)!&lt;/p&gt;
&lt;p&gt;I uploaded a video showing
&lt;a class="reference external" href="https://www.youtube.com/watch?v=xAFjbKAzB4s"&gt;Serial-Over-LAN with IPMItools to an HP Proliant DL160 G6 running
SmartOS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Documenting a few other useful commands for myself here.&lt;/p&gt;
&lt;p&gt;Power on and off the server chassis:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo ipmitool -I lan -H guy-ilo.foxhop.net -U admin chassis power status
sudo ipmitool -I lan -H guy-ilo.foxhop.net -U admin chassis power on
sudo ipmitool -I lan -H guy-ilo.foxhop.net -U admin chassis power soft
sudo ipmitool -I lan -H guy-ilo.foxhop.net -U admin chassis power off
sudo ipmitool -I lan -H guy-ilo.foxhop.net -U admin chassis power cycle
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Apparently after you know the ILO username and password you may
also use SSH to connect and manage the server:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
ssh admin&amp;#64;guy-ilo.foxhop.net
admin&amp;#64;guile-ilo.foxhop.net's password:

Lights-Out 100 Management
Copyright 2005-2007 ServerEngines Corporation
Copyright 2006-2007 Hewlett-Packard Development Company, L.P.

/./-&amp;gt; help
Root Directory

/./-&amp;gt; show
    /./
    Targets
        system1
        map1

    Properties

    Verbs
        cd
        version
        exit
        show
        help

/./-&amp;gt; cd system1
/./system1/-&amp;gt; show
    /./system1/
    Targets
        oemhp_sensors
        oemhp_frus
        console1
        led1

    Properties
        name=DL180(Aspen)    _R
        enabledstate=enabled

    Verbs
        cd
        version
        exit
        show
        reset
        start
        stop
        help
&lt;/pre&gt;
&lt;p&gt;You can even trigger the server OS to stop change run levels or mess
with chassis power for more extreme measures.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
/./system1/-&amp;gt; stop
System1 stopped.
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I run FreeNAS on an HP DL180 G6 and just replaced my p410 controller with an LSI SAS9220-8i (IBM M1015) flashed to IT mode. The stock cables are long enough. I did not have issues with fans running at high RPM.&lt;/p&gt;
&lt;p&gt;(I did the same replacement on an &lt;a class="reference external" href="/how-i-added-two-seagate-240g-ssds-as-smartos-l2arc/"&gt;HP DL160 G6 running SmartOS&lt;/a&gt; and it didn't have a high fan RPM issues either)&lt;/p&gt;
</content><category term="misc"/><category term="Guide"/></entry><entry><title>How I added two Seagate 240G SSDs as SmartOS L2ARC</title><link href="https://russell.ballestrini.net/how-i-added-two-seagate-240g-ssds-as-smartos-l2arc/" rel="alternate"/><published>2014-01-05T14:56:00-05:00</published><updated>2014-01-05T14:56:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-01-05:/how-i-added-two-seagate-240g-ssds-as-smartos-l2arc/</id><summary type="html">&lt;p&gt;&lt;strong&gt;How I added two Seagate 240G SSDs as SmartOS L2ARC&lt;/strong&gt;&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;removed icepacks from two western digital velociraptors&lt;/li&gt;
&lt;li&gt;installed ssds into icepacks&lt;/li&gt;
&lt;li&gt;installed icepacks into HP hotswap trays&lt;/li&gt;
&lt;li&gt;installed trays into HP prolaient g6 server&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;How to list all drive installed in Solaris, Open Solaris, or SmartOS&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
iostat -eE
&lt;/pre&gt;
&lt;pre class="literal-block"&gt;
format
&lt;/pre&gt;
&lt;/p&gt;&lt;pre class="literal-block"&gt;
AVAILABLE …&lt;/pre&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;How I added two Seagate 240G SSDs as SmartOS L2ARC&lt;/strong&gt;&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;removed icepacks from two western digital velociraptors&lt;/li&gt;
&lt;li&gt;installed ssds into icepacks&lt;/li&gt;
&lt;li&gt;installed icepacks into HP hotswap trays&lt;/li&gt;
&lt;li&gt;installed trays into HP prolaient g6 server&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;How to list all drive installed in Solaris, Open Solaris, or SmartOS&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
iostat -eE
&lt;/pre&gt;
&lt;pre class="literal-block"&gt;
format
&lt;/pre&gt;
&lt;/p&gt;&lt;pre class="literal-block"&gt;
AVAILABLE DISK SELECTIONS:
       0. c1t0d0
          /pci&amp;#64;0,0/pci103c,330b&amp;#64;1f,2/disk&amp;#64;0,0
       1. c1t1d0
          /pci&amp;#64;0,0/pci103c,330b&amp;#64;1f,2/disk&amp;#64;1,0
       2. c1t2d0
          /pci&amp;#64;0,0/pci103c,330b&amp;#64;1f,2/disk&amp;#64;2,0
       3. c1t3d0
          /pci&amp;#64;0,0/pci103c,330b&amp;#64;1f,2/disk&amp;#64;3,0
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;&lt;strong&gt;list zpools:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
zpool list
&lt;/pre&gt;
&lt;pre class="literal-block"&gt;
NAME    SIZE  ALLOC   FREE  EXPANDSZ    CAP  DEDUP  HEALTH  ALTROOT
zones  1.81T  41.4G  1.77T         -     2%  1.00x  ONLINE  -
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Add the two 240G SSDs as L2ARC devices:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
zpool add zones cache c1t2d0 c1t3d0
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Look at the iostats of the drives in the zpool&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
zpool iostat -v
&lt;/pre&gt;
&lt;pre class="literal-block"&gt;
               capacity     operations    bandwidth
pool        alloc   free   read  write   read  write
----------  -----  -----  -----  -----  -----  -----
zones       41.4G  1.77T      5     46  78.7K   730K
  mirror    41.4G  1.77T      5     46  78.7K   730K
    c1t0d0      -      -      0     15  41.5K   733K
    c1t1d0      -      -      0     15  41.6K   733K
cache           -      -      -      -      -      -
  c1t2d0    14.6M   224G      0      3  2.48K   273K
  c1t3d0    45.5M   224G      0      6  2.48K   852K
----------  -----  -----  -----  -----  -----  -----
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;This machine is a test hypervisor and after reviewing the output from
&lt;tt class="docutils literal"&gt;zpool iostat &lt;span class="pre"&gt;-v&lt;/span&gt;&lt;/tt&gt; over the last couple days, I'm pretty sure adding
L2ARC to this box was not needed, but it was a great learning
experience.&lt;/p&gt;
</content><category term="misc"/><category term="Guide"/></entry><entry><title>Test Game Engine with Python and SFML</title><link href="https://russell.ballestrini.net/test-game-engine-with-python-and-sfml/" rel="alternate"/><published>2014-01-02T00:30:00-05:00</published><updated>2014-01-02T00:30:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2014-01-02:/test-game-engine-with-python-and-sfml/</id><summary type="html">&lt;p&gt;Over this holiday season, Christmas and New Years, I took the time to
mess with some Game Development. I wrote a demo game engine using Python
and SFML. I plan to use this post to track my progress.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Video Evolution Playlist&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.youtube.com/watch?v=ytEG21b2es4&amp;amp;list=PLPqlh_ebFYDFPv_tIdUb26Bit2Q2nIVrm"&gt;All
videos&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2014-01-01&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.youtube.com/watch?v=ytEG21b2es4"&gt;User Input, Simple world,
Scrolling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.youtube.com/watch?v=LFxfepBSgEc"&gt;Simple …&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;p&gt;Over this holiday season, Christmas and New Years, I took the time to
mess with some Game Development. I wrote a demo game engine using Python
and SFML. I plan to use this post to track my progress.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Video Evolution Playlist&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.youtube.com/watch?v=ytEG21b2es4&amp;amp;list=PLPqlh_ebFYDFPv_tIdUb26Bit2Q2nIVrm"&gt;All
videos&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2014-01-01&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.youtube.com/watch?v=ytEG21b2es4"&gt;User Input, Simple world,
Scrolling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.youtube.com/watch?v=LFxfepBSgEc"&gt;Simple Collision
detection&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2014-01-02&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.youtube.com/watch?v=SsAi00wv0vU"&gt;Simple Collision bump vs
push&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.youtube.com/watch?v=2-FFMP67atU"&gt;Power ups and grow and
shrink&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content><category term="misc"/><category term="Art"/><category term="Code"/><category term="Python"/></entry><entry><title>Heka, World!</title><link href="https://russell.ballestrini.net/heka-world/" rel="alternate"/><published>2013-12-20T18:25:00-05:00</published><updated>2013-12-20T18:25:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-12-20:/heka-world/</id><summary type="html">&lt;p&gt;This post serves as a &amp;quot;Hello World&amp;quot; for the &lt;a class="reference external" href="https://github.com/mozilla-services/heka"&gt;data collection and
processing software called
Heka&lt;/a&gt;. Heka is written in
Go and was open sourced by Mozilla, the same fabulous group that brings
us Firefox!&lt;/p&gt;
&lt;p&gt;I intend to use Heka to replace Logstash agents by sending logs directly
to ElasticSearch …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This post serves as a &amp;quot;Hello World&amp;quot; for the &lt;a class="reference external" href="https://github.com/mozilla-services/heka"&gt;data collection and
processing software called
Heka&lt;/a&gt;. Heka is written in
Go and was open sourced by Mozilla, the same fabulous group that brings
us Firefox!&lt;/p&gt;
&lt;p&gt;I intend to use Heka to replace Logstash agents by sending logs directly
to ElasticSearch and continuing to use Kibana3 for visualizations. Also
I aim to start collecting metrics and sending to a central Whisper
back-end to fuel Graphite charts. All that we need to make Heka take on
these responsibilities is one binary and some custom configuration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Heka:&lt;/strong&gt; Hello, World!&lt;/p&gt;
&lt;p&gt;This mostly contrived example will show how to use Heka to watch
&lt;tt class="docutils literal"&gt;/tmp/input.log&lt;/tt&gt; and write to &lt;tt class="docutils literal"&gt;/tmp/output.log&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; install Heka&lt;/p&gt;
&lt;p&gt;Compile from source or install the &lt;a class="reference external" href="https://github.com/mozilla-services/heka/releases"&gt;Heka
package&lt;/a&gt; for your
operating system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; create a Heka TOML configuration file&lt;/p&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;/tmp/hello-heka.conf:&lt;/span&gt;&lt;/tt&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
[hello_heka_input_log]
type = &amp;quot;LogstreamerInput&amp;quot;
log_directory = &amp;quot;/tmp&amp;quot;
file_match = 'input\.log'

[hello_heka_output_log]
type = &amp;quot;FileOutput&amp;quot;
message_matcher = &amp;quot;TRUE&amp;quot;
path = &amp;quot;/tmp/output.log&amp;quot;
perm = &amp;quot;664&amp;quot;
encoder = &amp;quot;hello_heka_output_encoder&amp;quot;

[hello_heka_output_encoder]
type = &amp;quot;PayloadEncoder&amp;quot;
append_newlines = false
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; start Hekad and test!&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;in terminal 1:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo hekad -config=/tmp/hello-heka.conf
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;in terminal 2:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
tail -f /tmp/output.log
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;in terminal 3:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
echo 'Heka, World!' &amp;gt;&amp;gt; /tmp/input.log
&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Like magic the data appended to &lt;tt class="docutils literal"&gt;input.log&lt;/tt&gt; will appear in
&lt;tt class="docutils literal"&gt;output.log&lt;/tt&gt;&lt;/p&gt;
&lt;p&gt;Heka also has great docs and a number of input and output plugins, but
don't take my word for it, try it yourself!&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/></entry><entry><title>sensu-salt</title><link href="https://russell.ballestrini.net/sensu-salt/" rel="alternate"/><published>2013-12-17T11:35:00-05:00</published><updated>2013-12-17T11:35:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-12-17:/sensu-salt/</id><summary type="html">&lt;p&gt;A while back I explained how to &lt;a class="reference external" href="/create-your-own-fleet-of-servers-with-digital-ocean-and-salt-cloud/"&gt;Create your own fleet of servers with
Digital Ocean and
salt-cloud&lt;/a&gt;.
Today I will extend that post and show how I deployed a test environment
for &lt;a class="reference external" href="https://sensuapp.org/"&gt;Sensu, an open source monitoring
framework&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Before I test out new infrastructure software, I always attempt to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A while back I explained how to &lt;a class="reference external" href="/create-your-own-fleet-of-servers-with-digital-ocean-and-salt-cloud/"&gt;Create your own fleet of servers with
Digital Ocean and
salt-cloud&lt;/a&gt;.
Today I will extend that post and show how I deployed a test environment
for &lt;a class="reference external" href="https://sensuapp.org/"&gt;Sensu, an open source monitoring
framework&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Before I test out new infrastructure software, I always attempt to write
deployment manifests. I subject myself to this exercise for the
following reasons:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;to get better at configuration management (self growth)&lt;/li&gt;
&lt;li&gt;to enable quick setup and tear down of test environments (save money)&lt;/li&gt;
&lt;li&gt;to make sure the new software may be deployed and maintained in
configuration management (a must)&lt;/li&gt;
&lt;li&gt;to document the install process and leave comments (my notes double
as automation scripts!)&lt;/li&gt;
&lt;li&gt;to allow knowledge transfer (sharing is caring, you're welcome!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The state formulas in this post were tested on Ubuntu 12.04.3 x64bit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clone or fork the Salt-State tree&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://github.com/russellballestrini/sensu-salt"&gt;https://github.com/russellballestrini/sensu-salt&lt;/a&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
git clone https://github.com/russellballestrini/sensu-salt.git
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Declare deployment targets&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Before deployment, we must declare some targets in the top.sls file:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
'*':
  - git

'sensu-client*':
  - sensu.client

'sensu-server*':
  - rabbitmq.server
  - redis.server
  - sensu.server
  - sensu.client
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Stand up Sensu test environment&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I used the following &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;salt-cloud&lt;/span&gt;&lt;/tt&gt; command to create the sensu monitor
server:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
salt-cloud --profile ubuntu_do sensu-server &amp;amp;&amp;amp; salt 'sensu-server' state.highstate
&lt;/pre&gt;
&lt;p&gt;Once the sensu-server was live, I altered the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;client-config.json&lt;/span&gt;&lt;/tt&gt; and
modified the RabbitMQ host with the new &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;sensu-server&lt;/span&gt;&lt;/tt&gt;'s IP or DNS
record.&lt;/p&gt;
&lt;p&gt;I then spun up 4 sensu-clients using the following command:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
salt-cloud -P --profile ubuntu_do sensu-client1 sensu-client2 sensu-client3 sensu-client4 &amp;amp;&amp;amp; salt 'sensu-client*' state.highstate
&lt;/pre&gt;
&lt;p&gt;This caused 4 cloud servers to be spawned in parallel, and when the
provisioning finished, they instantly appeared in the
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;sensu-dashboard&lt;/span&gt;&lt;/tt&gt; which runs on the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;sensu-server&lt;/span&gt;&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tear down Sensu test environment&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Later when I was done messing with writing checks, I used the following
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;salt-cloud&lt;/span&gt;&lt;/tt&gt; commands to destroy the sensu server and clients:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
salt-cloud --destroy sensu-server
salt-cloud --destroy sensu-client1 sensu-client2 sensu-client3 sensu-client4
&lt;/pre&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/><category term="Salt"/></entry><entry><title>Hackathon 2013 Virtualization</title><link href="https://russell.ballestrini.net/hackathon-2013-virtualization/" rel="alternate"/><published>2013-12-13T20:56:00-05:00</published><updated>2013-12-13T20:56:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-12-13:/hackathon-2013-virtualization/</id><summary type="html">&lt;p&gt;As a warning before we dive into things, this post is less of a formal
publication and more of a stream of conscience.&lt;/p&gt;
&lt;p&gt;My employer &lt;a class="reference external" href="https://newcars.com/jobs/"&gt;newcars.com&lt;/a&gt; has allowed the
technical staff to host hackathon! Over the past couple weeks I have had
quite a few ideas tumbling around in …&lt;/p&gt;</summary><content type="html">&lt;p&gt;As a warning before we dive into things, this post is less of a formal
publication and more of a stream of conscience.&lt;/p&gt;
&lt;p&gt;My employer &lt;a class="reference external" href="https://newcars.com/jobs/"&gt;newcars.com&lt;/a&gt; has allowed the
technical staff to host hackathon! Over the past couple weeks I have had
quite a few ideas tumbling around in my head:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Standup a central logging server&lt;/li&gt;
&lt;li&gt;Standup Sensu for monitoring&lt;/li&gt;
&lt;li&gt;Bake-off and document some KVM virtualization hypervisors (Ubuntu or
SmartOS)&lt;/li&gt;
&lt;li&gt;Test Docker and document findings&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ultimately I have chosen to dedicate my time to testing out
virtualization on the new Cisco UCS Blade servers. I plan to test Ubuntu
KVM first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;KVM (Ubuntu 12.04)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I decided to use a Salt-stack configuration management formulas to
document how I transformed &lt;tt class="docutils literal"&gt;kvmtest02&lt;/tt&gt; (a regular Ubuntu 12.04 server)
into a KVM hypervisor. I use &lt;tt class="docutils literal"&gt;kvmtest0a&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;kvmtest02b&lt;/tt&gt; when
refering to the virtual machines living on the hypervisor. Here is the
formula:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;kvm/init.sls:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# https://help.ubuntu.com/community/KVM
# Ubuntu server a KVM - Kernel Virtual Machine hypervisor

# the official ubuntu document suggests we install the following
kvm-hypervisor:
  pkg.installed:
    - names:
      - qemu-kvm
      - libvirt-bin
      - ubuntu-vm-builder
      - bridge-utils

# we need to make all of our ops people part of the libvirtd group
# so that they may create and manipulate guests. we skip this for now
# and assume all virtual machines will be owned by root.

# these are optional packages which gives us a GUI for the hypervisor
virt-manager-and-viewer:
  pkg.installed:
    - names:
      - virt-manager
      - virt-viewer
    - require:
      - pkg: kvm-hypervisor

# create a directory to hold virtual machine image files
kvm-image-dir:
  file.directory:
    - name: /cars/vms
    - user: root
    - group: root
    - mode: 775
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;I used the following to install the formula to the test hypervisor:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
salt 'kvmtest02.example.com' state.highstate
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;The salt highstate reported everything was good (green), so I moved on
to setting up the bridge networking interface. At this point I don't
want to figure out the logistics setting up the network bridge in
configuration management, so I simply manually edited
&lt;tt class="docutils literal"&gt;/etc/network/interfaces&lt;/tt&gt; to look like this (substitute your own
network values):&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
#auto eth0
#iface eth0 inet manual

# https://help.ubuntu.com/community/KVM/Networking
# create a bridge so guest VMs may have their own identities
auto br0
iface br0 inet static
    address XXX.XX.89.42
    netmask 255.255.255.0
    network XXX.XX.89.0
    broadcast XXX.XX.89.255
    gateway XXX.XX.89.1

    # dns-* options are implemented by the resolvconf package
    dns-nameservers XXX.XX.254.225 XXX.XX.254.225
    dns-search example.com

    # bridge_* options are implemented by bridge-utils package
    bridge_ports eth0
    bridge_stp off
    bridge_fd 0
    bridge_maxwait 0
&lt;/pre&gt;
&lt;p&gt;Then, I crossed my fingers and reloaded the network stack using this
command:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
/etc/init.d/networking restart
&lt;/pre&gt;
&lt;p&gt;I also used this to &amp;quot;bounce&amp;quot; the bridge network interface:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
ifdown br0 &amp;amp;&amp;amp; ifup br0
&lt;/pre&gt;
&lt;p&gt;I verified with &lt;tt class="docutils literal"&gt;ifconfig&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;I'm ready to create my first VM. There are many different ways to boot
the VM and install the operating system. KVM is fully virtualized so
nearly any operating system may be install on the VM.&lt;/p&gt;
&lt;p&gt;If you have not already, please get familiarized with the following two
commands:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;virsh&lt;/li&gt;
&lt;li&gt;virt-install&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The &lt;tt class="docutils literal"&gt;virsh&lt;/tt&gt; command is an unified tool / API for working with
hypervisors that support the libvirt library. Currently &lt;tt class="docutils literal"&gt;virsh&lt;/tt&gt;
supports Xen, QEmu, KVM, LXC, OpenVZ, VirtualBox and VMware ESX. For
more information run &lt;tt class="docutils literal"&gt;virsh help&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;virt-install&lt;/span&gt;&lt;/tt&gt; command line tool is used to create new KVM, Xen,
or Linux container guests using the &amp;quot;libvirt&amp;quot; hypervisor management
library. For more information run &lt;tt class="docutils literal"&gt;man &lt;span class="pre"&gt;virt-install&lt;/span&gt;&lt;/tt&gt;.&lt;/p&gt;
&lt;blockquote&gt;
Woah, virsh and virt-install both support LXC?&lt;/blockquote&gt;
&lt;p&gt;We have decided to only support Ubuntu 12.04 at this time, so obviously
we will choose that for our guest's OS. Now we need to decide on an
installation strategy. We may use the following techniques to perform an
install:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;boot from local CD-rom&lt;/li&gt;
&lt;li&gt;boot from local ISO&lt;/li&gt;
&lt;li&gt;boot from PXE server on our local vLAN&lt;/li&gt;
&lt;li&gt;boot from netboot image from anywhere in the world&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We will choose the PXE boot strategy because our vLAN environment
already uses that for physical hosts.&lt;/p&gt;
&lt;p&gt;We will use the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;virt-install&lt;/span&gt;&lt;/tt&gt; helper tool to create the virtual
machine's &amp;quot;hardware&amp;quot; with various flags. Lets document the creation of
this guest in a simple bash script so we may reference it again in the
future.&lt;/p&gt;
&lt;p&gt;/tmp/create-kvmtest02-a.sh:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
HOSTNAME=kvmtest02-a
DOMAIN=example.com

sudo virt-install \
  --connect qemu:///system \
  --virt-type kvm \
  --name $HOSTNAME \
  --vcpu 2 \
  --ram 4096 \
  --disk /cars/vms/$HOSTNAME.qcow2,size=20 \
  --os-type linux \
  --graphics vnc \
  --network bridge=br0,mac=RANDOM \
  --autostart \
  --pxe
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;This was not used, but shows the flags to perform a netboot from
Internet:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
--location=https://archive.ubuntu.com/ubuntu/dists/raring/main/installer-amd64/ \
--extra-args=&amp;quot;auto=true priority=critical keymap=us locale=en_US hostname=$HOSTNAME domain=$DOMAIN url=http://192.168.1.22/my-debconf-preseed.txt&amp;quot;
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;I created the vm:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
bash /tmp/create-kvmtest02-a.sh
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;virt-install&lt;/span&gt;&lt;/tt&gt; drops you into the &amp;quot;console&amp;quot; of the VM, but this will
not work yet, so we use ctrl+] to break out and get back to our
hypervisor. Use &lt;tt class="docutils literal"&gt;virsh list&lt;/tt&gt; to list all the currently running VMs.&lt;/p&gt;
&lt;p&gt;Lets use &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;virt-viewer&lt;/span&gt;&lt;/tt&gt; to view the VMs display. For this we need to
SSH to the hypervisor and forward our display to our workstation, we do
this with the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-X&lt;/span&gt;&lt;/tt&gt; flag. For example:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
ssh -X kvmtest02
&lt;/pre&gt;
&lt;p&gt;Now we can launch &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;virt-viewer&lt;/span&gt;&lt;/tt&gt; on the remote hypervisor, and the GUI
will be drawn on our local X display!&lt;/p&gt;
&lt;pre class="literal-block"&gt;
virt-viewer kvmtest02-a
&lt;/pre&gt;
&lt;p&gt;Once I got that to work, I also tested &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;virt-manager&lt;/span&gt;&lt;/tt&gt; which gives a
GUI to control all guests on the remote hypervisor.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
virt-manager
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Now we need to determine the auto-generated MAC Address of the new
virtual machine.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
virsh dumpxml kvmtest02-a | grep -i &amp;quot;mac &amp;quot;
  mac address='52:54:00:47:86:8e'
&lt;/pre&gt;
&lt;p&gt;We need to add this MAC address to our PXE server's DHCP configuration
to allocate the IP and tell it where to PXE-boot from.&lt;/p&gt;
&lt;p&gt;During a real deployment we would get an IP address allocated and an A
record and PTR setup for new servers. This is a test and I will be
destroying all traces of this virtual machine after presenting during
the hackathon, so for now I'm going to skip the DNS entries and &amp;quot;steal&amp;quot;
an IP address. I must be VERY careful not to use an IP address already
in production. First use dig to find an IP without a record, then
attempt to ping and use NMAP on the IP.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
dig -x XXX.XX.89.240 +short
ping XXX.XX.89.240
nmap XXX.XX.89.240 -PN
&lt;/pre&gt;
&lt;p&gt;The IP address checked out, it didn't have a PTR, it didn't respond to
pings, and using nmap proved there were no open ports. I'm very
confident this IP address is not in use.&lt;/p&gt;
&lt;p&gt;I added a record to our DHCP / PXE server for this Virtual Machine. I
attempted multiple times to pxe boot the VM, but the network stack was
never automatically configured... The DHCP server was discovering the
new VMs MAC and offering the proper IP address, as noted by these log
lines:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
Dec 13 07:57:43 pxeserver60 dhcpd: DHCPDISCOVER from 52:54:00:47:86:8e via eth0
Dec 13 07:57:43 pxeserver60 dhcpd: DHCPOFFER on xxx.xx.89.240 to 52:54:00:47:86:8e via eth0
Dec 13 07:57:44 pxeserver60 dhcpd: DHCPDISCOVER from 52:54:00:47:86:8e via eth0
Dec 13 07:57:44 pxeserver60 dhcpd: DHCPOFFER on xxx.xx.89.240 to 52:54:00:47:86:8e via eth0
Dec 13 07:57:48 pxeserver60 dhcpd: DHCPDISCOVER from 52:54:00:47:86:8e via eth0
Dec 13 07:57:48 pxeserver60 dhcpd: DHCPOFFER on xxx.xx.89.240 to 52:54:00:47:86:8e via eth0
&lt;/pre&gt;
&lt;p&gt;I wasted about 4 hours attempting to troubleshoot and diagnose why the
VM wouldn't work with DHCP. I ended the night without any guests
online...&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Next DAY!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So today I decided to stop trying to get DHCP and PXE working. I
downloaded an Ubuntu server ISO to the hypervisor, and used
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;virt-manager&lt;/span&gt;&lt;/tt&gt; to mount the ISO on the guest and booted for a manual
operating system install.&lt;/p&gt;
&lt;p&gt;This did two things, it proved that the hypervisor's network bridge
&lt;tt class="docutils literal"&gt;br0&lt;/tt&gt; worked for static network assigned settings and that something
between the DHCP server and the hypervisor was preventing the
&lt;tt class="docutils literal"&gt;DHCPOFFER&lt;/tt&gt; answer from getting back to the VM. I looked into iptables
firewall, removed apparmor, removed SELINUX and reviewed countless logs
looking for hints... then moved on...&lt;/p&gt;
&lt;p&gt;I was able to get salt-minion installed on the vm using our
post-install-salt-minion.sh script, which I manually downloaded from the
salt master. But to keep this test self contained, I pointed the VM's
salt-minion to &lt;tt class="docutils literal"&gt;kvmtest02&lt;/tt&gt; which we already had setup as a test
salt-master.&lt;/p&gt;
&lt;p&gt;The salt-master saw the salt-minion's key right away, so I decided to
target an install. This what I applied to the VM:&lt;/p&gt;
&lt;p&gt;salt/top.sls:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
'kvmtest02.example.com':
  - kvm

'kvmtest02b.example.com':
  - virtualenv
  - python-ldap
  - nginx
  - the-gateway
&lt;/pre&gt;
&lt;p&gt;salt-pillar/top.sls&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# kvmtest02b gateway in a VM experiment
'kvmtest02b.example.com':
  - nginx
  - the-gateway.alpha
  - deployment-keys.the-gateway-alpha
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;The stack was successfully deployed to the VM and proved that virtual
machines are a viable solution for stage or production. It also gave me
the change to test out this particular deployment again and found a few
gotchas we need to create maintenance tickets for.&lt;/p&gt;
&lt;p&gt;Without configuration management, it would have taken weeks to deploy
this custom application stack. The install with configuration management
took less then 10 minutes!&lt;/p&gt;
&lt;p&gt;One of the KVM related snags I ran into was that Nginx does some fun
calculations with cpu cache to determine hash table sizes. As a
temporary work around, until I can devote more research time, I raised
up the following three hash table directives in the http section of
nginx.conf:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
server_names_hash_bucket_size 512;
types_hash_bucket_size 512;
types_hash_max_size 4096;
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;&lt;strong&gt;SmartOS&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;snippet from /etc/dhcp/dhcpd.conf&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# SmartOS hypervisor group to boot image
group &amp;quot;smartos-hypervisors&amp;quot; {
  next-server xxx.xx.89.71;

   host smrtest01-eth0 {
        hardware ethernet 00:25:B5:02:07:DF;
        option host-name &amp;quot;ncstest01&amp;quot;;
        fixed-address smrtest01.example.com;

        if exists user-class and option user-class = &amp;quot;iPXE&amp;quot; {
           filename = &amp;quot;smartos/menu.ipxe&amp;quot;;
        } else {
           filename = &amp;quot;smartos/undionly.kpxe&amp;quot;;
        }
    }

}
&lt;/pre&gt;
&lt;pre class="literal-block"&gt;
mkdir /cars/tftp/smartos
cd /cars/tftp/smartos
wget http://boot.ipxe.org/undionly.kpxe
wget https://download.joyent.com/pub/iso/platform-latest.tgz
tar -xzvf platform-latest.tgz
mv platform-20130629T040542Z 20130629T040542Z
mkdir platform
mv i86pc/ platform/
&lt;/pre&gt;
&lt;p&gt;create boot menu that we referenced,
&lt;tt class="docutils literal"&gt;vim /cars/tftp/smartos/menu.ipxe&lt;/tt&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
#!ipxe

kernel /smartos/20130629T040542Z/platform/i86pc/kernel/amd64/unix
initrd /smartos/20130629T040542Z/platform/i86pc/amd64/boot_archive
boot
&lt;/pre&gt;
&lt;p&gt;Make sure to replace platform version with current.&lt;/p&gt;
&lt;p&gt;I was able to get the blade server to PXE boot the image, but it seems
SmartOS doesn't really support the SANs. SmartOS really expects to see
local disks, and to build a ZFS pool on top of that. Basically SmartOS
could be used to build a SAN, so they didn't put much effort in
supporting SANs. After I figured this out I abandoned this test. We
could revist this again, using one of the Dell servers, or use it to
stand up a really powerful Alpha server environment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LXC&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Run /usr/bin/httpd in a linux container guest (LXC). Resource usage is
capped at 512 MB of ram and 2 host cpus:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
virt-install \
--connect lxc:/// \
--name lxctest02-a \
--ram 512 \
--vcpus 2 \
--init /usr/bin/httpd
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Discussion points&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Why doesn't DHCP work on bridge?&lt;/li&gt;
&lt;li&gt;If we use virtualization, we need to come up with a plan for IP
addresses, like possibly allocate ~5 IP addresses to a hypervisor
host&lt;/li&gt;
&lt;li&gt;We need to come up with a naming convention for guests, in testing I
appended a letter to the hypervisor name &lt;tt class="docutils literal"&gt;kvmtest02&lt;/tt&gt; so the guests
names were &lt;tt class="docutils literal"&gt;kvmtest02a&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;kvmtest02b&lt;/tt&gt;, is this plausible going
forward?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;If I had more time ...&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;I would have liked to test out LXC&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I would have liked to test out Docker&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I would have liked to test out physical to virtual migrations&lt;/p&gt;
&lt;/li&gt;
&lt;/p&gt;</content><category term="misc"/><category term="Project"/><category term="Python"/><category term="Salt"/><category term="Docker"/></entry><entry><title>Backup all virtual machines on a SmartOS hypervisor with smart-back.sh</title><link href="https://russell.ballestrini.net/backup-all-virtual-machines-on-a-smartos-hypervisor-with-smart-back-sh/" rel="alternate"/><published>2013-10-27T20:45:00-04:00</published><updated>2013-10-27T20:45:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-10-27:/backup-all-virtual-machines-on-a-smartos-hypervisor-with-smart-back-sh/</id><summary type="html">&lt;p&gt;This post will explain how to create a cronjob to backup of every
virtual machine on a SmartOS hypervisor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create&lt;/strong&gt; the following bash script in /opt/smart-back.sh:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
#!/usr/bin/bash

# Backup all virtual machines on a SmartOS hypervisor
# Author:  russell&amp;#64;ballestrini.net
# Website: https://russell.ballestrini.net/

# Backup directory …&lt;/pre&gt;</summary><content type="html">&lt;p&gt;This post will explain how to create a cronjob to backup of every
virtual machine on a SmartOS hypervisor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create&lt;/strong&gt; the following bash script in /opt/smart-back.sh:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
#!/usr/bin/bash

# Backup all virtual machines on a SmartOS hypervisor
# Author:  russell&amp;#64;ballestrini.net
# Website: https://russell.ballestrini.net/

# Backup directory without trailing slash
backupdir=/opt/backups

# temp dir where we ZFS send and gzip before moving to backupdir
tmpdir=/opt

svcadm enable autofs

for VM in `vmadm list -p -o alias,uuid`
  do
    # create an array called VM_PARTS splitting on ':'
    IFS=':' VM_PARTS=($VM)

    # create some helper varibles for alias and uuid
    alias=${VM_PARTS[0]}
    uuid=${VM_PARTS[1]}

    # echo &amp;quot;Backup started for $VM&amp;quot;
    vmadm send $uuid &amp;gt; $tmpdir/$alias

    # echo &amp;quot;Starting $VM&amp;quot;
    vmadm start $uuid

    # disk space is cheap, uncomment if you disagree.
    #pbzip2 $tmpdir/$alias

  done

mv $tmpdir/*.bz2 $backupdir
&lt;/pre&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Create&lt;/strong&gt; a cronjob entry to schedule the backups:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
crontab -e
&lt;/pre&gt;
&lt;pre class="literal-block"&gt;
2 6 * * 0 /usr/bin/bash /opt/smart-back.sh
&lt;/pre&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;If I expand on this script much more, I plan to stick it into revision
control.&lt;/p&gt;
&lt;p&gt;If you look closely, I have also added a hack to enable autofs
(&lt;tt class="docutils literal"&gt;svcadm enable autofs&lt;/tt&gt;) which allows me to automount an NFS
share on my remote FreeNAS by setting
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;backupdir=/net/[ip-or-fqdn-of-freenas]/mnt/zfs-mirror/backup/vms&lt;/span&gt;&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;We have scheduled a backup of each virtual machine on your SmartOS
hypervisor!&lt;/p&gt;
&lt;p&gt;If or when the time comes to restore a VM from a backup, use the
following:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# decompress the backup file.
pbzip2 -d backup-file.bz2

# ingest the backup file into the hypervisor.
vmadm receive -f /path/to/backup-file
&lt;/pre&gt;
&lt;p&gt;Just make sure the VM doesn't currently exist on the hypervisor.&lt;/p&gt;
&lt;p&gt;This strategy is great for complete backups of machines which could be
used during a manual migration, or if corruption happened to the VM and
we wanted to restore to a previous version.&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/></entry><entry><title>Simplify deployments with Upstart and uWSGI</title><link href="https://russell.ballestrini.net/simplify-deployments-with-upstart-and-uwsgi/" rel="alternate"/><published>2013-10-19T22:35:00-04:00</published><updated>2013-10-19T22:35:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-10-19:/simplify-deployments-with-upstart-and-uwsgi/</id><summary type="html">&lt;p&gt;As you know from my previous post, I recently &lt;a class="reference external" href="https://russell.ballestrini.net/honey-i-just-deleted-linkpeek-com/"&gt;deleted
LinkPeek.com&lt;/a&gt;
and after struggling to get it back online, I vowed to start utilizing
configuration management. During this exercise, I noticed that the
architecture I use in production seems overly complicated.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The current production deployment stack:&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Nginx listen on …&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;p&gt;As you know from my previous post, I recently &lt;a class="reference external" href="https://russell.ballestrini.net/honey-i-just-deleted-linkpeek-com/"&gt;deleted
LinkPeek.com&lt;/a&gt;
and after struggling to get it back online, I vowed to start utilizing
configuration management. During this exercise, I noticed that the
architecture I use in production seems overly complicated.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The current production deployment stack:&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Nginx listen on 80/443 proxy upstream 9901/9902&lt;/li&gt;
&lt;li&gt;Upstart =&amp;gt; Supervisord =&amp;gt; Cherrypy/Paste listen on 9901/9902&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Each of these services and processes have their own configuration files
which must work together. Upstart needs to know the location of
Supervisord's configuration files. Supervisord needs to know the
location of Cherrypy/PasteDeploy's configuration files. Supervisord also
must bring up a specified number of worker processes who listen on a
pool of ports. Nginx needs to proxy upstream to that pool of worker
ports.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This architecture seems difficult to automate because of the numerous
places errors may sneak in. I started researching an alternative
architecture and stumbled upon Nicholas Piel's &lt;a class="reference external" href="https://nichol.as/benchmark-of-python-web-servers"&gt;Benchmark of Python Web
Servers&lt;/a&gt;. The
graphs Nicholas compiled allowed me to narrow down a list of potential
replacements.&lt;/p&gt;
&lt;p&gt;I chose to review uWSGI first because it was a top contender on every
chart. After briefly testing uWSGI, I halted my explorations and
selected it as the winner. I know you probably feel that was a premature
decision but uWSGI fit what I was looking for.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The new simplified stack:&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Nginx listen on 80/443 proxy upstream 5200&lt;/li&gt;
&lt;li&gt;Upstart =&amp;gt; uWSGI listen on 5200&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The uWSGI server possesses great performance out-of-the-box but also
presents many options for fine tuning. These options may be specified
either directly in the Upstart script using flags or in a configuration
file (like a Pyramid .ini). Either way, the stack uses less files, less
processes, and less complexity then the original architecture. For
&lt;a class="reference external" href="https://linkpeek.com"&gt;LinkPeek&lt;/a&gt; deployments I decided to place all
uWSGI related configuration directly in the Upstart script which I
embedded below:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;linkpeek/weblinkpeek.conf | Upstart for starting uWSGI&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
description &amp;quot;Start the uWSGI master for the LinkPeek WEB&amp;quot;

start on runlevel [2345]
stop on runlevel [!2345]

respawn

# run service as linkpeek user
setuid linkpeek
setgid linkpeek

# uWSGI command to execute and treat as a daemon
exec /path/to/linkpeek/web/env/bin/uwsgi --master --processes=4 --http=127.0.0.1:5200 --die-on-term --logto2=/path/to/linkpeek/web/uwsgi.log --virtualenv=/path/to/linkpeek/web/env --ini-paste=/path/to/linkpeek/web/linkpeek/production.ini
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;The web application is now daemonized and listening on port 5200 with 4
workers. I leave implementing the Nginx front end to the reader.&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Opinion"/><category term="Python"/></entry><entry><title>Postfix Salt State Formula</title><link href="https://russell.ballestrini.net/postfix-salt-state-formula/" rel="alternate"/><published>2013-10-17T21:28:00-04:00</published><updated>2013-10-17T21:28:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-10-17:/postfix-salt-state-formula/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;img alt="postfix-config-management" src="/uploads/2013/10/mysza.gif" /&gt;&lt;/p&gt;
&lt;p&gt;The following formula was tested on Ubuntu and Debian however it would
not take much work to test on other operating systems.&lt;/p&gt;
&lt;p&gt;This state formula will install postfix and mutt. The postfix service
will watch various configuration files for changes and restart
accordingly.&lt;/p&gt;
&lt;p&gt;This formula will also manage and watch the /etc/aliases file and invoke
the &lt;em&gt;newaliases&lt;/em&gt; command to initialize or re-initialize the alias
database. This formula will also manage and watch the
/etc/postfix/virtual file and invoke the &lt;em&gt;postmap&lt;/em&gt; command to create or
update a managed Postfix lookup table.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;postfix/init.sls:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# Install mutt and postfix mutt packages.
#
# This formula supports setting an optional:
#
#  * 'aliases' file
#  * 'virtual' map file
#
# Both aliases and virtual use a pillar data schema
# which takes the following form:
#
# postfix:
#   aliases: |
#       postmaster: root
#       root: testuser
#       testuser: russell&amp;#64;example.com
#   virtual: |
#       example.com             this is a comment
#       test1&amp;#64;example.com       me&amp;#64;example.com
#       test2&amp;#64;example.com       me&amp;#64;example.com
#

# install mutt
mutt:
  pkg:
    - installed

# install postfix have service watch main.cf
postfix:
  pkg:
    - installed
  service:
    - running
    - enable: True
    - watch:
      - pkg: postfix
      - file: /etc/postfix/main.cf

# postfix main configuration file
/etc/postfix/main.cf:
  file.managed:
    - source: salt://postfix/main.cf
    - user: root
    - group: root
    - mode: 644
    - template: jinja
    - require:
      - pkg: postfix

# manage /etc/aliases if data found in pillar
{% if 'aliases' in pillar.get('postfix', '') %}
/etc/aliases:
  file.managed:
    - source: salt://postfix/aliases
    - user: root
    - group: root
    - mode: 644
    - template: jinja
    - require:
      - pkg: postfix

run-newaliases:
  cmd.wait:
    - name: newaliases
    - cwd: /
    - watch:
      - file: /etc/aliases
{% endif %}

# manage /etc/postfix/virtual if data found in pillar
{% if 'virtual' in pillar.get('postfix', '') %}
/etc/postfix/virtual:
  file.managed:
    - source: salt://postfix/virtual
    - user: root
    - group: root
    - mode: 644
    - template: jinja
    - require:
      - pkg: postfix

run-postmap:
  cmd.wait:
    - name: /usr/sbin/postmap /etc/postfix/virtual
    - cwd: /
    - watch:
      - file: /etc/postfix/virtual
{% endif %}
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;postfix/aliases:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# Managed by config management
# See man 5 aliases for format
{{pillar['postfix']['aliases']}}
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;postfix/virtual:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# Managed by config management
{{pillar['postfix']['virtual']}}
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;postfix/main.cf:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# Managed by config management
# See /usr/share/postfix/main.cf.dist for a commented, more complete version

# Debian specific:  Specifying a file name will cause the first
# line of that file to be used as the name.  The Debian default
# is /etc/mailname.
#myorigin = /etc/mailname

smtpd_banner = $myhostname ESMTP $mail_name
biff = no

# appending .domain is the MUA's job.
append_dot_mydomain = no

# Uncomment the next line to generate &amp;quot;delayed mail&amp;quot; warnings
#delay_warning_time = 4h

readme_directory = no

# TLS parameters
smtpd_tls_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
smtpd_tls_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
smtpd_use_tls=yes
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache

# See /usr/share/doc/postfix/TLS_README.gz in the postfix-doc package for
# information on enabling SSL in the smtp client.

myhostname = {{ grains['fqdn'] }}
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
mydestination = {{ grains['fqdn'] }}, localhost
relayhost =
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all

{% if 'virtual' in pillar.get('postfix','') %}
virtual_alias_maps = hash:/etc/postfix/virtual
{% endif %}
&lt;/pre&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/><category term="Salt"/></entry><entry><title>Control a MongoDB collection in configuration management</title><link href="https://russell.ballestrini.net/control-a-mongodb-collection-in-configuration-management/" rel="alternate"/><published>2013-10-14T21:39:00-04:00</published><updated>2013-10-14T21:39:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-10-14:/control-a-mongodb-collection-in-configuration-management/</id><summary type="html">&lt;p&gt;This post explains how to use configuration management (Salt Stack) to
completely control a MongoDB collection. In our example we want to
control a store's collection of plans.&lt;/p&gt;
&lt;p&gt;First we create a JSON representation of the collection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;mongodb/plan.json:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
{
  &amp;quot;_id&amp;quot; : { &amp;quot;$oid&amp;quot; : &amp;quot;4ef8b9e2be329f491d98f74b&amp;quot; },
  &amp;quot;cost&amp;quot; : 20, &amp;quot;description&amp;quot; : &amp;quot;development&amp;quot;,
  &amp;quot;name&amp;quot; : &amp;quot;good&amp;quot;, &amp;quot;count …&lt;/pre&gt;</summary><content type="html">&lt;p&gt;This post explains how to use configuration management (Salt Stack) to
completely control a MongoDB collection. In our example we want to
control a store's collection of plans.&lt;/p&gt;
&lt;p&gt;First we create a JSON representation of the collection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;mongodb/plan.json:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
{
  &amp;quot;_id&amp;quot; : { &amp;quot;$oid&amp;quot; : &amp;quot;4ef8b9e2be329f491d98f74b&amp;quot; },
  &amp;quot;cost&amp;quot; : 20, &amp;quot;description&amp;quot; : &amp;quot;development&amp;quot;,
  &amp;quot;name&amp;quot; : &amp;quot;good&amp;quot;, &amp;quot;count&amp;quot; : 6000
}
{
  &amp;quot;_id&amp;quot; : { &amp;quot;$oid&amp;quot; : &amp;quot;4ef8b9e8be329f491d98f74c&amp;quot; },
  &amp;quot;cost&amp;quot; : 60, &amp;quot;description&amp;quot; : &amp;quot;freelancers&amp;quot;,
  &amp;quot;name&amp;quot; : &amp;quot;better&amp;quot;, &amp;quot;count&amp;quot; : 36000
}
{
  &amp;quot;_id&amp;quot; : { &amp;quot;$oid&amp;quot; : &amp;quot;4ef8b9f0be329f491d98f74d&amp;quot; },
  &amp;quot;cost&amp;quot; : 180, &amp;quot;description&amp;quot; : &amp;quot;production&amp;quot;,
  &amp;quot;name&amp;quot; : &amp;quot;best&amp;quot;, &amp;quot;count&amp;quot; : 162000
}
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Next we configure a salt state formula to manage the JSON file and watch
it for changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;mongodb/init.sls:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# install mongodb server
mongodb-server:
  pkg:
    - installed

# manage the store's plan.json
/tmp/plan.json:
  file.managed:
    - source: salt://mongodb/plan.json
    - user: root
    - group: root
    - mode: 644

# import the plan collection if it changes
import-plan-collection:
    cmd.wait:
      - name: mongoimport --db=store --collection=plan --upsert /tmp/plan.json
      - require:
        - pkg: mongodb-server
      - watch:
        - file: /tmp/plan.json
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;&lt;em&gt;Now whenever plan.json is altered in configuration management, the file
on the minion will update which will trigger a mongoimport with upsert
to occur.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Optionally, we could replace &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;--upsert&lt;/span&gt;&lt;/tt&gt; with &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;--drop&lt;/span&gt;&lt;/tt&gt; which will
drop the collection before re-importing thus removing stale records.&lt;/p&gt;
&lt;p&gt;We now have a version controlled JSON file in configuration management
and the power of MongoDB Document Objects in our application code!&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/><category term="Salt"/></entry><entry><title>Configuration Management vs Remote Execution</title><link href="https://russell.ballestrini.net/configuration-management-vs-remote-execution/" rel="alternate"/><published>2013-07-11T23:15:00-04:00</published><updated>2013-07-11T23:15:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-07-11:/configuration-management-vs-remote-execution/</id><summary type="html">&lt;p class="first last"&gt;It's about time to learn the difference.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;img alt="config-mangement-vs-remote-execution" src="/uploads/2013/07/config-mangement-vs-remote-execution.png" /&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;What is configuration management?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In a perfect world configuration management provides a centralized,
revision controlled, self-documented, change management location for
manifests and formulas which both define how to build a complete system
and organize a means of knowledge transfer. An infrastructure perfectly
described in configuration management allows any single part of the
system to be created, reproduced, multiplied, self-healed or even
re-purposed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When should I use configuration management?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use configuration management whenever you intend to permanently alter
system state or infrastructure data.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;System state - the current state of the system:&lt;/em&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;operating system (distro,kernel,patches,hot-fixes)&lt;/li&gt;
&lt;li&gt;software installed (base,role-specific)&lt;/li&gt;
&lt;li&gt;services running (base,role-specific)&lt;/li&gt;
&lt;li&gt;user access&lt;/li&gt;
&lt;li&gt;etc&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Infrastructure data - the information that describes the
infrastructure:&lt;/em&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;asset information (models,specs,IP,DNS,rack-elevation,etc)&lt;/li&gt;
&lt;li&gt;roles (app,web,db,proxy,load-balancer,etc)&lt;/li&gt;
&lt;li&gt;current allocations (allocated,unallocated,number of servers in each
role,etc)&lt;/li&gt;
&lt;li&gt;etc&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What is remote execution?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Remote execution is the act of issuing commands to one or more remote
systems. The most popular remote execution system in use today is SSH.
SSH is great for maintaining a small group of dissimilar systems. Once
the system count grows and similar roles present themselves, start
looking at something like Fabric. Fabric is a python framework which
builds on top of the SSH protocol and makes it possible to invoke the
same command on hundreds of servers sequentially. Once the fleet count
reaches thousands of servers and commands must run in parallel, look at
Salt-stack's remote execution layer written with python and ZeroMQ.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When should I use remote execution?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Only use remote execution for ad hoc reports, data collection, or for
temporary tests which have no expectation of persistence after research
is complete. Frequent data collection jobs work best when implemented in
a metrics collection or monitoring system. (I'll save that for another
post)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;When should I not use remote execution?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Do not use remote execution to change the state of the system or the
data which describes the infrastructure! If a remote execution job
changes either state or data it should be placed into the configuration
management for the reasons mentioned above.&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Greatest Hits"/><category term="Opinion"/><category term="Python"/><category term="Salt"/></entry><entry><title>Understanding Salt Stack user and group management</title><link href="https://russell.ballestrini.net/understanding-salt-stack-user-and-group-management/" rel="alternate"/><published>2013-07-08T13:51:00-04:00</published><updated>2013-07-08T13:51:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-07-08:/understanding-salt-stack-user-and-group-management/</id><summary type="html">&lt;p&gt;This state will create a user:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
russell:
  user:
    - present
&lt;/pre&gt;
&lt;p&gt;This state will create a user and a group. This also makes the user part
of the group, and handles creating the group first:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
russell:
  group:
    - present
  user:
    - present
    - groups:
      - russell
    - require:
      - group: russell
&lt;/pre&gt;
&lt;p&gt;This state handles user and group generation …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This state will create a user:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
russell:
  user:
    - present
&lt;/pre&gt;
&lt;p&gt;This state will create a user and a group. This also makes the user part
of the group, and handles creating the group first:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
russell:
  group:
    - present
  user:
    - present
    - groups:
      - russell
    - require:
      - group: russell
&lt;/pre&gt;
&lt;p&gt;This state handles user and group generation along with password and
ssh-key maintenance. This is all done securely using pillar to
parameterize arguments:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# This state will create users accounts
#
# This state requires a pillar named 'users' with data formatted like:
#
# users:
#
#  tusername:
#    fullname: Test Username
#    uid: 1007
#    gid: 1007
#    groups:
#      - sudo
#      - ops
#    crypt: $password-hash-sha512-prefered
#    pub_ssh_keys:
#      - ssh-rsa list-of-public-keys tusername-sm
#
#  anotheruser: ... snipped ...

# loop over all users presented by pillar:
# create user's group, create user, then add pub keys
{% for username, details in pillar.get('users', {}).items() %}
{{ username }}:

  group:
    - present
    - name: {{ username }}
    - gid: {{ details.get('gid', '') }}

  user:
    - present
    - fullname: {{ details.get('fullname','') }}
    - name: {{ username }}
    - shell: /bin/bash
    - home: /home/{{ username }}
    - uid: {{ details.get('uid', '') }}
    - gid: {{ details.get('gid', '') }}
    - crypt: {{ details.get('crypt','') }}
    {% if 'groups' in details %}
    - groups:
      {% for group in details.get('groups', []) %}
      - {{ group }}
      {% endfor %}
    - require:
      {% for group in details.get('groups', []) %}
      - group: {{ group }}
      {% endfor %}
    {% endif %}

  {% if 'pub_ssh_keys' in details %}
  ssh_auth:
    - present
    - user: {{ username }}
    - names:
    {% for pub_ssh_key in details.get('pub_ssh_keys', []) %}
      - {{ pub_ssh_key }}
    {% endfor %}
    - require:
      - user: {{ username }}
  {% endif %}

{% endfor %}
&lt;/pre&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/><category term="Salt"/></entry><entry><title>Create your own fleet of servers with Digital Ocean and salt-cloud</title><link href="https://russell.ballestrini.net/create-your-own-fleet-of-servers-with-digital-ocean-and-salt-cloud/" rel="alternate"/><published>2013-07-02T22:20:00-04:00</published><updated>2013-07-02T22:20:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-07-02:/create-your-own-fleet-of-servers-with-digital-ocean-and-salt-cloud/</id><summary type="html">&lt;p&gt;Have you heard about Digital Ocean?
They offer a polished user interface, KVM guests with SSD storage, and an API to interact with a cloud of hypervisors.
API integration got you down?
Don't worry, salt-cloud has already integrated Digital Ocean among it's list of providers!
The rest of this post …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Have you heard about Digital Ocean?
They offer a polished user interface, KVM guests with SSD storage, and an API to interact with a cloud of hypervisors.
API integration got you down?
Don't worry, salt-cloud has already integrated Digital Ocean among it's list of providers!
The rest of this post illustrates the steps I took to configure salt-cloud to work with Digital Ocean.&lt;/p&gt;
&lt;p&gt;This guide assumes you already have a:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;salt-master&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://www.digitalocean.com/?refcode=27e015299dc7%20"&gt;Digital Ocean&lt;/a&gt; account.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Step one,&lt;/strong&gt; install the most recent version of salt-cloud.&lt;/p&gt;
&lt;p&gt;On the salt-master:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo apt-get install salt-cloud

# or if you prefer ...
pip install salt-cloud==2015.5.0

# last verify it was successfully installed
salt-cloud --version
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step two,&lt;/strong&gt; configure salt-cloud.&lt;/p&gt;
&lt;p&gt;Salt-cloud uses the following files YAML files for configuration:&lt;/p&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;/etc/salt/cloud.conf.d/main.conf:&lt;/dt&gt;
&lt;dd&gt;This is the main configuration file. I have the following statements:&lt;/dd&gt;
&lt;/dl&gt;
&lt;pre class="literal-block"&gt;
minion:
    master: master.foxhop.net
    append_domain: foxhop.net
&lt;/pre&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;/etc/salt/cloud.providers/do.conf:&lt;/dt&gt;
&lt;dd&gt;This is a provider configuration file for Digital Ocean (do).
Collect your client_key and personal_access_token (api_key) from the Digital Ocean user dashboard.
Also create an SSH key and add the public key using the dashboard:&lt;/dd&gt;
&lt;/dl&gt;
&lt;pre class="literal-block"&gt;
# For Digital Ocean
do:
  provider: digital_ocean
  client_key: MyClientKeyLiftedFromDashboard
  personal_access_token: MyAPIKeyLiftedFromDashboard
  ssh_key_file: /keys/digital-ocean-salt-cloud
  ssh_key_name: digital-ocean-salt-cloud.pub
&lt;/pre&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;/etc/salt/cloud.profiles/do.conf:&lt;/dt&gt;
&lt;dd&gt;This is the Digital Ocean profiles configuration file.
We will create just two profiles for now, but you can create unlimited named combinations.&lt;/dd&gt;
&lt;/dl&gt;
&lt;pre class="literal-block"&gt;
ubuntu-12-04-do-512:
  provider: do
  image: ubuntu-12-04-x64
  size: 512mb
  location: nyc1

ubuntu-14-04-do-512:
  provider: do
  image: ubuntu-14-04-x64
  size: 512mb
  location: nyc1
&lt;/pre&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;ssh_key_file:&lt;/dt&gt;
&lt;dd&gt;This is your private SSH key located on your salt-master&lt;/dd&gt;
&lt;dt&gt;ssh_key_name:&lt;/dt&gt;
&lt;dd&gt;This is the name of the public key you added in your Digital Ocean dashboard&lt;/dd&gt;
&lt;dt&gt;size:&lt;/dt&gt;
&lt;dd&gt;The size or plan you would like to provision, 512mb is the smallest plan&lt;/dd&gt;
&lt;dt&gt;location:&lt;/dt&gt;
&lt;dd&gt;The geographical region, location, and/or data center&lt;/dd&gt;
&lt;dt&gt;image:&lt;/dt&gt;
&lt;dd&gt;The operating system image&lt;/dd&gt;
&lt;/dl&gt;
&lt;p&gt;After you configure the do provider in /etc/salt/cloud.providers you
gain access to the following commands:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
salt-cloud --list-sizes do
salt-cloud --list-locations do
salt-cloud --list-images do
salt-cloud --help
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Lets provision a new cloud server!&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
salt-cloud --profile ubuntu-14-04-do-512 deejay
&lt;/pre&gt;
&lt;p&gt;If all goes well you should have a newly provisioned server bootstrapped with salt-minion.
The new minion's keys are already added to the salt-master.
Now you just need to run highstate!&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/><category term="Python"/><category term="Salt"/></entry><entry><title>Add a custom header to all Salt managed files using pillar and jinja templates</title><link href="https://russell.ballestrini.net/add-a-custom-header-to-all-salt-managed-files-using-pillar-and-jinja-templates/" rel="alternate"/><published>2013-07-01T14:11:00-04:00</published><updated>2013-07-01T14:11:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-07-01:/add-a-custom-header-to-all-salt-managed-files-using-pillar-and-jinja-templates/</id><summary type="html">&lt;p&gt;Salt-stack (salt) provides a solution for centralized configuration
management and remote execution. One of the most basic things Salt
provides is the ability to manage the contents of a file or a directory
of files. Using Salt we can dictate the state of our minions and as a
result we …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Salt-stack (salt) provides a solution for centralized configuration
management and remote execution. One of the most basic things Salt
provides is the ability to manage the contents of a file or a directory
of files. Using Salt we can dictate the state of our minions and as a
result we also gain auto-healing of configuration files.&lt;/p&gt;
&lt;p&gt;Salt will clobber local changes to managed files and force the state to
reflect the version in configuration management. In an effort to avoid
confusing the uninformed, I place a header on each managed file which
announces &amp;quot;THIS FILE IS MANAGED BY SALT&amp;quot;.&lt;/p&gt;
&lt;p&gt;To avoid repeating myself in each managed file, I came up with the
following centralized solution -&lt;/p&gt;
&lt;p&gt;First, I give all minions access to the headers pillar tree in top.sls:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
base:
  '*':
    - headers
&lt;/pre&gt;
&lt;p&gt;Next, I create a couple headers in in headers/init.sls:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
headers:
  salt:
    file: |
        ################################
        # THIS FILE IS MANAGED BY SALT #
        ################################
    directory: |
        #####################################
        # THIS DIRECTORY IS MANAGED BY SALT #
        #####################################
&lt;/pre&gt;
&lt;p&gt;Then, I parse each managed file in the state tree with jinja, for
example hosts/init.sls:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
/etc/hosts:
  file.managed:
    source: salt://hosts/hosts
    user: root
    group: group
    mode: 644
    template: jinja
&lt;/pre&gt;
&lt;p&gt;Finally, in each file served by salt I add the jinja header
substitution, for example hosts/hosts:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
{{pillar['headers']['salt']['file']}}
127.0.0.1       localhost.localdomain localhost
::1     localhost6.localdomain6 localhost6
&lt;/pre&gt;
&lt;p&gt;I use this same technique to declare most configuration options as
parameters, like hostnames, IP addresses, ports, and more.&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/><category term="Salt"/></entry><entry><title>High load and CPU usage craftbukkit compared to vanilla minecraft</title><link href="https://russell.ballestrini.net/high-load-and-cpu-usage-craftbukkit-compared-to-vanilla-minecraft/" rel="alternate"/><published>2013-06-20T23:35:00-04:00</published><updated>2013-06-20T23:35:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-06-20:/high-load-and-cpu-usage-craftbukkit-compared-to-vanilla-minecraft/</id><summary type="html">&lt;p&gt;I started researching the best ways to use &lt;a class="reference external" href="https://bobbylikeslinux.net/post/2013/2013-salt-minecraft-fun/"&gt;salt to provision minecraft
servers&lt;/a&gt;. I wrote
a salt state formula for the vanilla minecraft server deployment. The
deployment worked out great so I decided to try my luck with plugins.&lt;/p&gt;
&lt;p&gt;In order to use plugins and mods we need to use …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I started researching the best ways to use &lt;a class="reference external" href="https://bobbylikeslinux.net/post/2013/2013-salt-minecraft-fun/"&gt;salt to provision minecraft
servers&lt;/a&gt;. I wrote
a salt state formula for the vanilla minecraft server deployment. The
deployment worked out great so I decided to try my luck with plugins.&lt;/p&gt;
&lt;p&gt;In order to use plugins and mods we need to use a customized server
package. I decided to try provisioning a craftbukkit server and quickly
noticed something was very wrong. Vanilla Craftbukkit (with no plugins)
produces very high load and CPU usage.&lt;/p&gt;
&lt;p&gt;Here is a chart for proof. The spike is when I stopped the vanilla
minecraft server and started the craftbukkit minecraft server:&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2013/06/minecraft-vs-craftbukkit.png"&gt;&lt;img alt="minecraft-vs-craftbukkit" src="/uploads/2013/06/minecraft-vs-craftbukkit.png" /&gt;&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Opinion"/><category term="Project"/><category term="Salt"/></entry><entry><title>Honey! I just DELETED LinkPeek.com</title><link href="https://russell.ballestrini.net/honey-i-just-deleted-linkpeek-com/" rel="alternate"/><published>2013-05-26T13:19:00-04:00</published><updated>2013-05-26T13:19:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-05-26:/honey-i-just-deleted-linkpeek-com/</id><summary type="html">&lt;p&gt;During the day I am an ops sys-admin. During the night I am a husband,
father of two, and a CEO of a bootstrapped start-up. After launch, my
first project was to schedule regular backups of user data and archive
off-site. My goal was to &lt;a class="reference external" href="https://russell.ballestrini.net/virt-back-restoring-from-backups/"&gt;create backups but never need …&lt;/a&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;During the day I am an ops sys-admin. During the night I am a husband,
father of two, and a CEO of a bootstrapped start-up. After launch, my
first project was to schedule regular backups of user data and archive
off-site. My goal was to &lt;a class="reference external" href="https://russell.ballestrini.net/virt-back-restoring-from-backups/"&gt;create backups but never need
them&lt;/a&gt;.
Boy was I lucky ...&lt;/p&gt;
&lt;p&gt;Yes, leave it to me to inadvertently delete the VPS root disk. One of
the major cloud providers places the &amp;quot;rename&amp;quot; and &amp;quot;remove&amp;quot; disk buttons
right next to each other and I learned a nasty habit of clearing pop-ups
without reading them (thanks Windows).&lt;/p&gt;
&lt;blockquote&gt;
&amp;quot;Honey! I just deleted LinkPeek.com&amp;quot;&lt;/blockquote&gt;
&lt;p&gt;The horror... My stomach felt like I took a tumble in a roller-coaster.
Instantly I tossed off my developer hat and put on my operations hat. I
checked the off-site backups. I had nightly dumps of MongoDB and weekly
tar backups of the /etc partition. The user data was in MongoDB and most
of the system configuration information was in the tar. I used the tar
to recover 2 upstart scripts, 2 supervisord scripts, 2 complex nginx
confs, an ssl cert, and the pyramid production.ini.&lt;/p&gt;
&lt;p&gt;I set out to stand up a new server, re-install the needed packages,
recover the user data, and restore the service. After 1.5 hours of
feverish typing, &lt;a class="reference external" href="https://linkpeek.com/"&gt;https://linkpeek.com/&lt;/a&gt; was back online.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I learned and my plan going forward&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you are a small team or a start-up, you must have somebody dedicated
to operations. Without backups I would not have been able to gracefully
recover. Most likely I would have reimbursed the existing members and
shuttered the doors.&lt;/p&gt;
&lt;p&gt;This experience was eye-opening. In my next couple of posts I will
explain how I &lt;a class="reference external" href="/automatic-backups/"&gt;create and maintain backups&lt;/a&gt; and
my next project will implement a configuration management and
provisioning system.&lt;/p&gt;
&lt;p&gt;This system will allow me to:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;take out the human element of recovery&lt;/li&gt;
&lt;li&gt;significantly reduce the time-to-recover from a catastrophic failure&lt;/li&gt;
&lt;li&gt;test disaster recovery procedures before needing them&lt;/li&gt;
&lt;li&gt;provision development and production environments without effort&lt;/li&gt;
&lt;li&gt;have a reproducible blueprint of &amp;quot;how to build a LinkPeek server&amp;quot;&lt;/li&gt;
&lt;/ol&gt;
</content><category term="misc"/><category term="DevOps"/><category term="LinkPeek"/><category term="Opinion"/><category term="Python"/></entry><entry><title>Survey Baby Monkey</title><link href="https://russell.ballestrini.net/survey-baby-monkey/" rel="alternate"/><published>2013-04-26T13:47:00-04:00</published><updated>2013-04-26T13:47:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-04-26:/survey-baby-monkey/</id><summary type="html">&lt;p class="first last"&gt;baby monkey fxhp cartoon&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;img alt="baby-monkey-fxhp" src="/uploads/2013/04/baby-monkey-fxhp.jpg" /&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Art"/></entry><entry><title>Automatic event hangout with cron</title><link href="https://russell.ballestrini.net/automatic-event-hangout-with-cron/" rel="alternate"/><published>2013-04-09T09:17:00-04:00</published><updated>2013-04-09T09:17:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-04-09:/automatic-event-hangout-with-cron/</id><summary type="html">&lt;p&gt;&lt;strong&gt;Create an online only, hangout event&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Create a new event with a date far into the future, like the year 2015.
Go to the event's options &amp;gt; advanced and enable 'this event is online
only' which will create a unique Hangout URI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create a cronjob&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Create a cronjob on each device …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;Create an online only, hangout event&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Create a new event with a date far into the future, like the year 2015.
Go to the event's options &amp;gt; advanced and enable 'this event is online
only' which will create a unique Hangout URI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Create a cronjob&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Create a cronjob on each device to open the web browser Monday-Friday at
12:49pm and open the unique Hangout URI.&lt;/p&gt;
&lt;p&gt;Firefox cron example:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# Run Firefox at 12:49 EST each weekday and open hangout URI
49 12 * * 1-5 export DISPLAY=:0 &amp;amp;&amp;amp; /usr/bin/firefox 'hangout-uri'
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Chromium-browser cron example:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
# Run Chromium-browser at 12:49 EST each weekday and open hangout URI
49 12 * * 1-5 export DISPLAY=:0 &amp;amp;&amp;amp; /usr/bin/chromium-browser 'hangout-uri'
&lt;/pre&gt;
&lt;/p&gt;</content><category term="misc"/><category term="Guide"/></entry><entry><title>Guido name dropped tornado python tulip and pep-3156</title><link href="https://russell.ballestrini.net/guido-name-dropped-tornado-python-tulip-and-pep-3156/" rel="alternate"/><published>2013-03-22T22:07:00-04:00</published><updated>2013-03-22T22:07:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-03-22:/guido-name-dropped-tornado-python-tulip-and-pep-3156/</id><summary type="html">&lt;p&gt;Pycon 2013 was excellent, in fact it was my first one I have attended.&lt;/p&gt;
&lt;p&gt;I found it odd that django and Pyramid had plenty of talks but nobody
mentioned tornado.&lt;/p&gt;
&lt;p&gt;The only person that brought up tornado was Guido himself, who has been
researching and developing async python since December …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Pycon 2013 was excellent, in fact it was my first one I have attended.&lt;/p&gt;
&lt;p&gt;I found it odd that django and Pyramid had plenty of talks but nobody
mentioned tornado.&lt;/p&gt;
&lt;p&gt;The only person that brought up tornado was Guido himself, who has been
researching and developing async python since December 12th, 2012. Guido
wants to add async API libraries to python core, and has been comparing
his work to twisted and more importantly tornado. He is leveraging the
existing solutions to stay on the correct track.&lt;/p&gt;
&lt;p&gt;Async API's in Python core! This news was extra exciting for me, because
I have already learned the power of async by messing around with
tornado. Since the talk many people have approached me and asked for
more information about async API's.&lt;/p&gt;
&lt;p&gt;My favorite use for async is long polling for web applications. Here is
a diagram that shows how great tornado is:&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2013/03/2013-03-22-tornado-callback-long-polling-event-loop-scaled.png"&gt;&lt;img alt="2013-03-22-tornado-callback-long-polling-event-loop-scaled" src="/uploads/2013/03/2013-03-22-tornado-callback-long-polling-event-loop-scaled.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This type of polling can support thousands of concurrent users, all
connected and waiting for a callback event to occur. I used tornado to
build &lt;a class="reference external" href="https://four2go.gumyum.com"&gt;four2go&lt;/a&gt;, a real-time and
multi-player browser-based-game.&lt;/p&gt;
</content><category term="misc"/><category term="Uncategorized"/><category term="Python"/></entry><entry><title>virt-back's Domfetcher class returns doms from libvirt API</title><link href="https://russell.ballestrini.net/virt-backs-domfetcher-class-returns-doms-from-libvirt-api/" rel="alternate"/><published>2013-03-02T15:04:00-05:00</published><updated>2013-03-02T15:04:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-03-02:/virt-backs-domfetcher-class-returns-doms-from-libvirt-api/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2013/03/2013-03-02-scale-virt-back-domfetcher-scaled.png"&gt;&lt;img alt="2013-03-02-scale-virt-back-domfetcher-scaled" src="/uploads/2013/03/2013-03-02-scale-virt-back-domfetcher-scaled.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I'm hooked...&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I attended SCaLE 11x, my first technical conference, and had an amazing
time. My favorite talk was Michael Day's &amp;quot;&lt;a class="reference external" href="https://code.ncultra.org/2013/02/scale-11x-open-virtualization/"&gt;Advancements with Open
Virtualization &amp;amp;
KVM&lt;/a&gt;&amp;quot;
(link to slides). Michael's presentation inspired me to continue my work
on virt-back.&lt;/p&gt;
&lt;p&gt;During my trip home I used the in-flight wifi to push this
&lt;a class="reference external" href="https://git.unturf.com/python/virt-back/commit/d6dff27323650bf784cc284f676299ffe07953cb"&gt;commit&lt;/a&gt;
into the cloud from the clouds! This particular commit re-factored the
dom object list generation into a simple-to-use class called Domfetcher.
Domfetcher abstracts the libvirt API and grants access to the following
helper methods:&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;get_all_doms( )&lt;/strong&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Return a list of all dom objects&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;get_doms_by_names( guest_names=[] )&lt;/strong&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Accept a list of guest_names, return a list of related dom objects&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;get_running_doms( )&lt;/strong&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Return a list of running dom objects&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;get_shutoff_doms( )&lt;/strong&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Return a list of shutoff but defined dom objects&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;This is an example of how to use Domfetcher:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
import virtback

# optionally supply hypervisor uri
domfetcher = virtback.Domfetcher()

doms = domfetcher.get_running_doms()

for dom in doms:
    print dom.name()

for dom in doms:
    print dom.info()

for dom in doms:
    print dom.shutdown()
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;As always thanks for reading!&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/></entry><entry><title>How to overload default function arguments in python using lambda</title><link href="https://russell.ballestrini.net/how-to-overload-default-function-arguments-in-python-using-lambda/" rel="alternate"/><published>2013-01-12T11:22:00-05:00</published><updated>2013-01-12T11:22:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-01-12:/how-to-overload-default-function-arguments-in-python-using-lambda/</id><summary type="html">&lt;p&gt;Python Lambda functions are very powerful but I often forget how they
work or the fun things they do. This post will document how to use a
lambda to provide different default arguments to a function.&lt;/p&gt;
&lt;p&gt;We will use the &lt;a class="reference external" href="https://bitbucket.org/russellballestrini/ago/overview"&gt;human function found in
ago.py&lt;/a&gt; as an
example - because …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Python Lambda functions are very powerful but I often forget how they
work or the fun things they do. This post will document how to use a
lambda to provide different default arguments to a function.&lt;/p&gt;
&lt;p&gt;We will use the &lt;a class="reference external" href="https://bitbucket.org/russellballestrini/ago/overview"&gt;human function found in
ago.py&lt;/a&gt; as an
example - because I'm the module author and I really like it. Lets use
the interactive python interpreter to run help on the human function.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; from ago import human
&amp;gt;&amp;gt;&amp;gt; help( human )

Help on function human in module ago:

human(dt, precision=2, past_tense='{} ago', future_tense='in {}')
    Accept a datetime or timedelta, return a human readable delta string
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Shown above the human function accepts 1 argument and 3 named keyword
arguments. The dt argument must be a datetime or timedelta object, the
precision must be an integer, and the other two must be strings. If we
didn't like the default arguments, we would need to specify (or pass in)
new values each time we invoked the function.&lt;/p&gt;
&lt;p&gt;Example: &lt;tt class="docutils literal"&gt;human(dt, 3, 'this happened {} &lt;span class="pre"&gt;ago!',&lt;/span&gt; 'in {} from &lt;span class="pre"&gt;now!')&lt;/span&gt;&lt;/tt&gt;.
If we know we will always want different default arguments we can create
a lambda function to shorten the invocation length.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; h = lambda dt : human(dt, 3, 'this happened {} ago!', 'in {} from now!')
&amp;gt;&amp;gt;&amp;gt; print h( dt ) # h is much shorter then human, and still reusable!
&lt;/pre&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Above creates a new function h who only accepts one argument dt. This
function calls human with our default arguments.&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;This lambda is equivalent to this regular python function:&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; def h( dt ):
...    return human(d, 3, 'this happened {} ago!', 'in {} from now!')
&amp;gt;&amp;gt;&amp;gt; print h( dt ) # h is much shorter then human, and still reusable!
&lt;/pre&gt;
&lt;p&gt;Here is a working example to show the new lambda function h in action:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; from datetime import datetime
&amp;gt;&amp;gt;&amp;gt; from datetime import timedelta
&amp;gt;&amp;gt;&amp;gt; from ago import human
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; h = lambda dt : human(dt, 3, 'this happened {} ago!', 'in {} from now!')
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; present = datetime.now()
&amp;gt;&amp;gt;&amp;gt;
&amp;gt;&amp;gt;&amp;gt; for i in range( 1, 15 ):
...     if i % 2 == 0:
...         new_date = present - timedelta( i, i * i, i * i * i )
...     else:
...         new_date = present + timedelta( i, i * i, i * i * i )
...     h( new_date )
...
'in 22 hours, 9 minutes, 30 seconds from now!'
'this happened 2 days, 1 hour, 50 minutes ago!'
'in 2 days, 22 hours, 9 minutes from now!'
'this happened 4 days, 1 hour, 50 minutes ago!'
'in 4 days, 22 hours, 9 minutes from now!'
'this happened 6 days, 1 hour, 51 minutes ago!'
'in 6 days, 22 hours, 10 minutes from now!'
'this happened 8 days, 1 hour, 51 minutes ago!'
'in 8 days, 22 hours, 10 minutes from now!'
'this happened 10 days, 1 hour, 52 minutes ago!'
'in 10 days, 22 hours, 11 minutes from now!'
'this happened 12 days, 1 hour, 52 minutes ago!'
'in 12 days, 22 hours, 12 minutes from now!'
'this happened 14 days, 1 hour, 53 minutes ago!'
&lt;/pre&gt;
</content><category term="misc"/><category term="Code"/><category term="Greatest Hits"/><category term="Guide"/><category term="Python"/><category term="AWS"/></entry><entry><title>ago.py human readable timedelta 0.0.4 release</title><link href="https://russell.ballestrini.net/ago-py-human-readable-timedelta0-0-4-release/" rel="alternate"/><published>2013-01-09T00:04:00-05:00</published><updated>2013-01-09T00:04:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2013-01-09:/ago-py-human-readable-timedelta0-0-4-release/</id><summary type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;We have released ago.py 0.0.4&lt;/strong&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;pypi: &lt;a class="reference external" href="https://pypi.python.org/pypi/ago"&gt;https://pypi.python.org/pypi/ago&lt;/a&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;repo: &lt;a class="reference external" href="https://bitbucket.org/russellballestrini/ago"&gt;https://bitbucket.org/russellballestrini/ago&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Special thanks to David Beitey for supplying &lt;a class="reference external" href="https://davidjb.com/"&gt;ideas and python
code&lt;/a&gt; for this update!&lt;/p&gt;
&lt;p&gt;All changes are backward compatible.&lt;/p&gt;
&lt;p&gt;Change log:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;added support for future dates&lt;/li&gt;
&lt;li&gt;added optional past_tense …&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;We have released ago.py 0.0.4&lt;/strong&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;pypi: &lt;a class="reference external" href="https://pypi.python.org/pypi/ago"&gt;https://pypi.python.org/pypi/ago&lt;/a&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;repo: &lt;a class="reference external" href="https://bitbucket.org/russellballestrini/ago"&gt;https://bitbucket.org/russellballestrini/ago&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Special thanks to David Beitey for supplying &lt;a class="reference external" href="https://davidjb.com/"&gt;ideas and python
code&lt;/a&gt; for this update!&lt;/p&gt;
&lt;p&gt;All changes are backward compatible.&lt;/p&gt;
&lt;p&gt;Change log:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;added support for future dates&lt;/li&gt;
&lt;li&gt;added optional past_tense keyword argument string for custom output&lt;/li&gt;
&lt;li&gt;added optional future_tense keyword argument string for custom
output&lt;/li&gt;
&lt;li&gt;added 13 tests to help prevent regressions&lt;/li&gt;
&lt;li&gt;Updated the README.rst documentation for better coverage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We were just shy of 800 downloads on pypi for ago-0.0.3, I hope
ago-0.0.4 will perform even better!&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="Python"/></entry><entry><title>Tips for getting pull requests approved</title><link href="https://russell.ballestrini.net/tips-for-getting-pull-requests-approved/" rel="alternate"/><published>2012-12-12T12:50:00-05:00</published><updated>2012-12-12T12:50:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-12-12:/tips-for-getting-pull-requests-approved/</id><summary type="html">&lt;div class="section" id="pull-rejection-sucks"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Pull rejection sucks!&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You have just coded, implemented, and submitted a pull request. A short
while later the request is declined by an upstream maintainer and you
feel crushed. We have all been there. Today I'm going to show you a
better way. This article will teach you how to …&lt;/p&gt;&lt;/div&gt;</summary><content type="html">&lt;div class="section" id="pull-rejection-sucks"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Pull rejection sucks!&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You have just coded, implemented, and submitted a pull request. A short
while later the request is declined by an upstream maintainer and you
feel crushed. We have all been there. Today I'm going to show you a
better way. This article will teach you how to create pull requests that
get approved.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="start-small"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Start small&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You need to earn trust with the maintainers. Your first commit should be
a small change which they &lt;em&gt;cannot&lt;/em&gt; reject. Try to write a missing test
or re-factor duplicate code. Correct a comment's accuracy or rename a
variable to better reflect its purpose. Your first pull request should
not alter how the program works.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2012/12/start-small.xcf_.png"&gt;&lt;img alt="start-small.xcf" src="/uploads/2012/12/start-small.xcf_.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="don-t-add-leading-or-trailing-white-space"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Don't add leading or trailing white space&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Additional whitespace will alter the diff output. This causes version
control systems to flag lines as changed which is irritating and
sometimes misleading.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2012/12/spot-the-diff.xcf_.png"&gt;&lt;img alt="spot-the-diff.xcf" src="/uploads/2012/12/spot-the-diff.xcf_.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="less-is-more"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Less is more&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Minimize the number of changes to accomplish your goal. People are busy
and at times lazy. Reduce the work the maintainers must do to perform a
merge. Lowering the amount of lines to review should increase the chance
of approval.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2012/12/less-is-more.xcf_.png"&gt;&lt;img alt="less-is-more.xcf" src="/uploads/2012/12/less-is-more.xcf_.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="only-commit-working-code"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;Only commit working code&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Do not break the program, only commit working code. If the project has
tests make sure they work before you commit.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2012/12/commit-working-code.xcf_.png"&gt;&lt;img alt="commit-working-code.xcf" src="/uploads/2012/12/commit-working-code.xcf_.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="follow-the-leader"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-6"&gt;Follow the leader&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Try to mimic the maintainers. Follow the coding style and project layout
even if it seems wrong. This is not your playground... yet. Before you
commit, review the VCS logs to learn how verbose or terse your commit
messages should be. When in Rome do as the romans do. This silly game of
follow the leader reduces friction of an outsider committing to the
project.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2012/12/follow-the-leader.xcf_.png"&gt;&lt;img alt="follow-the-leader.xcf" src="/uploads/2012/12/follow-the-leader.xcf_.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="comments-docs-and-tests-oh-my"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-7"&gt;Comments, docs, and tests oh my!&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Your first pull request should clarify or add to the existing
documentation. Fix the README, adjust a comment, or write a test. These
tasks might appear small but they serve to prove that you possess
comprehension of the source code. They also do not alter the program's
functionality.&lt;/p&gt;
&lt;p&gt;Having a few of these pull requests under-your-belt will
earn you trust which will eventuality translate to more responsibility in
the future. You will also differentiate yourself from the rest because
most people do not enjoy working on documentation.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2012/12/comments-docs-tests-oh-my.xcf_.png"&gt;&lt;img alt="comments-docs-tests-oh-my.xcf" src="/uploads/2012/12/comments-docs-tests-oh-my.xcf_.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="blog-about-the-change"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-8"&gt;Blog about the change&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Write about the change, give reasons and examples. Include a link to
your blog post in the pull request.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2012/12/ideas-blog-get-heard.xcf_.png"&gt;&lt;img alt="ideas-blog-get-heard.xcf" src="/uploads/2012/12/ideas-blog-get-heard.xcf_.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="pull-requests-are-like-paragraphs"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-9"&gt;Pull requests are like paragraphs&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;If you were writing an essay, you would split up your ideas into
separate paragraphs. A pull request has many qualities similar to a
paragraph. Each commit should be related to the pull request's main
objective. Commits of a pull request should stay focused and on topic.&lt;/p&gt;
&lt;blockquote&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;A pull request is to a program as a paragraph is to an essay.&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;A commit is to a pull request as a sentence is to a paragraph.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;p&gt;In an essay, if you have more then one topic you should have more then
one paragraph. Likewise when coding, only one idea or change per pull
request.&lt;/p&gt;
&lt;p&gt;Separating your ideas into different pull requests will grant the
maintainers greater flexibility when they begin to integrate. They will
have the ability to pick-and-choose which requests to merge and
everybody wins!&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2012/12/pull-request-stay-focused.xcf_.png"&gt;&lt;img alt="pull-request-stay-focused.xcf" src="/uploads/2012/12/pull-request-stay-focused.xcf_.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#pull-rejection-sucks" id="toc-entry-1"&gt;Pull rejection sucks!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#start-small" id="toc-entry-2"&gt;Start small&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#don-t-add-leading-or-trailing-white-space" id="toc-entry-3"&gt;Don't add leading or trailing white space&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#less-is-more" id="toc-entry-4"&gt;Less is more&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#only-commit-working-code" id="toc-entry-5"&gt;Only commit working code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#follow-the-leader" id="toc-entry-6"&gt;Follow the leader&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#comments-docs-and-tests-oh-my" id="toc-entry-7"&gt;Comments, docs, and tests oh my!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#blog-about-the-change" id="toc-entry-8"&gt;Blog about the change&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#pull-requests-are-like-paragraphs" id="toc-entry-9"&gt;Pull requests are like paragraphs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Greatest Hits"/><category term="Opinion"/></entry><entry><title>how to drain an iPhone battery without needing passcode</title><link href="https://russell.ballestrini.net/how-to-drain-an-iphone-battery-without-needing-passcode/" rel="alternate"/><published>2012-11-03T19:27:00-04:00</published><updated>2012-11-03T19:27:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-11-03:/how-to-drain-an-iphone-battery-without-needing-passcode/</id><content type="html">&lt;ol class="arabic simple"&gt;
&lt;li&gt;Press home button [ ]&lt;/li&gt;
&lt;li&gt;Slide camera button up&lt;/li&gt;
&lt;li&gt;Slide mode to video&lt;/li&gt;
&lt;li&gt;Turn on flash&lt;/li&gt;
&lt;li&gt;Put iPhone on table with light pointed down&lt;/li&gt;
&lt;li&gt;Walk away inconspicuously&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Extra points if you set the camera to record (just don't record yourself
or sounds) which will fill up the iPhone capacity.&lt;/p&gt;
</content><category term="misc"/><category term="Guide"/><category term="Security"/></entry><entry><title>Explaining cache with python</title><link href="https://russell.ballestrini.net/explaining-cache-with-python/" rel="alternate"/><published>2012-10-02T15:22:00-04:00</published><updated>2012-10-02T15:22:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-10-02:/explaining-cache-with-python/</id><summary type="html">&lt;p&gt;&lt;strong&gt;What is cache?&lt;/strong&gt; I define cache as &amp;quot;a saved answer to a question&amp;quot;.
Caching can speed up an application if a computationally complex
question is asked frequently. Instead of the computing the answer over
and over, we can use the previously cached answer. This post will
present one method of …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;What is cache?&lt;/strong&gt; I define cache as &amp;quot;a saved answer to a question&amp;quot;.
Caching can speed up an application if a computationally complex
question is asked frequently. Instead of the computing the answer over
and over, we can use the previously cached answer. This post will
present one method of adding cache to a python program. Specifically we
will write a program that computes prime numbers and saves the answers
into cache for quick retrieval.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EDIT:&lt;/strong&gt; The kind people of the Internet have expressed concern with my
loose use of the term cache; the techniques that follow are most
accurately described as memoization.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One algorithm for determining if a number is prime follows:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
prime_flag = True # default state
x = 5 # number to test
if x == 1:
    prime_flag = False
else:
    for i in range( 2, x ):
        if x % i == 0:
            prime_flag = False
            break
print prime_flag
&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Create a prime_flag variable to hold the answer and default it to
true. Let x be the number being tested and if x is equal to 1, the x is
not prime. Otherwise iterate over each number in the range of 2 to x.
Let i be the current number to be tested. if x is divided by i without
any remainder, x is not prime. Set the prime_flag to False and break
out of the loop. Print the result.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next we will move the algorithm into a function which will allow for
code reuse:&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
def is_prime( x ):
    &amp;quot;&amp;quot;&amp;quot;Determine if a number is prime, return Boolean&amp;quot;&amp;quot;&amp;quot;
    prime_flag = True
    if x == 1:
        prime_flag = False
    else:
        for i in range( 2, x ):
            if x % i == 0:
                prime_flag = False
                break
    return prime_flag

# invoke function:
print is_prime( 5 ) # True
print is_prime( 4 ) # False
&lt;/pre&gt;
&lt;p&gt;This function saves us a lot of typing and enables the ability to
quickly determine if a given number is prime.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next we will use a python dictionary to implement a result cache.&lt;/strong&gt;
Also by circumstance we introduce objects and classes.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
class Primer( object ):
    def __init__( self ):
        &amp;quot;&amp;quot;&amp;quot;create a cache dictionary&amp;quot;&amp;quot;&amp;quot;
        self.cache = {}

    def is_prime( self, x ):
        &amp;quot;&amp;quot;&amp;quot;Determine if x is prime, cache and return result&amp;quot;&amp;quot;&amp;quot;
        if x in self.cache:
            return self.cache[x] # lookup result

        prime_flag = True

        if x == 1:
            prime_flag = False
        else:
            for i in range( 2, x ):
                if x % i == 0:
                    prime_flag = False
                    break

        self.cache[x] = prime_flag # cache result
        return prime_flag

p = Primer() # create a new primer object
p.is_prime( 5 ) # True
p.is_prime( 4 ) # False
p.is_prime( 5 ) # True and fetched from cache
&lt;/pre&gt;
&lt;p&gt;What is great about this solution is that we can avoid looping and
computation if an answer is already in cache. Looking up a cached result
is much more efficient and will ultimately make a program feel more
responsive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Determining if 97352613 is prime takes my laptop nearly 18 seconds.&lt;/strong&gt;
Fetching the cached result seems to happen instantly.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;gt;&amp;gt;&amp;gt; s1 = time();p.is_prime( 97352613 );e1 = time()
False # not prime
&amp;gt;&amp;gt;&amp;gt; s2 = time();p.is_prime( 97352613 );e2 = time()
False # not prime from cache
&amp;gt;&amp;gt;&amp;gt; e1 - s1
17.970067977905273 # seconds
&amp;gt;&amp;gt;&amp;gt; e2 - s2
2.5987625122070312e-05 # or approx .000026 seconds
&lt;/pre&gt;
&lt;p&gt;A look-up will always beat a computation. Anything that can be cached,
should be cached. I hope this helps clear things up.&lt;/p&gt;
&lt;p&gt;&lt;img alt="2013-03-03-explaining-cache-scaled" src="/uploads/2013/03/2013-03-03-explaining-cache-scaled.png" /&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="Greatest Hits"/><category term="Python"/></entry><entry><title>The Pyramid community taught me the importance of test driven development</title><link href="https://russell.ballestrini.net/the-pyramid-community-taught-me-the-importance-of-test-driven-development/" rel="alternate"/><published>2012-09-22T14:03:00-04:00</published><updated>2012-09-22T14:03:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-09-22:/the-pyramid-community-taught-me-the-importance-of-test-driven-development/</id><summary type="html">&lt;p&gt;&lt;a class="reference external" href="https://github.com/Pylons/pyramid/commit/72561a213ccc456738582551e85fab0f0c8d09ab"&gt;Sontek's patch&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I greeted the UPS man in the middle of the street to sign for my new
Lenovo ThinkPad T430. Because this was My first &lt;em&gt;brand-new&lt;/em&gt; laptop
purchase I rationalized the time I spent tracking the package from the
factory in China to my hands in Connecticut. Once inside …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a class="reference external" href="https://github.com/Pylons/pyramid/commit/72561a213ccc456738582551e85fab0f0c8d09ab"&gt;Sontek's patch&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I greeted the UPS man in the middle of the street to sign for my new
Lenovo ThinkPad T430. Because this was My first &lt;em&gt;brand-new&lt;/em&gt; laptop
purchase I rationalized the time I spent tracking the package from the
factory in China to my hands in Connecticut. Once inside, I opened the
box and started installing Fedora 17. I couldn't help but to take in the
new-electronics smell.&lt;/p&gt;
&lt;p&gt;I've been without a laptop for more than a month so I was eager to get
my development environment configured. Most of my tools ship with
vanilla Linux, vim, python, hg mercurial, chromium browser, etc. My
first goal was to get a development copy of linkpeek.com running
locally. This took about 5 minutes and it seemed to be working fine
until I noticed a few pages had errors. The errors seemed to be caused
by a difference between Pyramid 1.3 and 1.4a1. But what was failing?&lt;/p&gt;
&lt;p&gt;I posted a short message in #pyramid about the bug and minutes later I
had multiple developers prodding for hints. &amp;quot;Could you post the whole
traceback?&amp;quot;, &amp;quot;What does your view look like?&amp;quot;. I answered quickly and
attempted to explain what I thought was going on. Turns out I was close
but before I could finish explaining the problem, sontek had a working
one-character-fix and was in the process finishing the tests to prove
the patch. He also explained what I should do in the interim to patch
locally.&lt;/p&gt;
&lt;p&gt;In the next 4 hours a pull request was submitted to the upstream master
and the patch was peer reviewed, accepted, and integrate by mcdonc. That
impressed me, a lot. All Pylon Projects have strict policies about test
coverage and now I understand why. Tests not only help produce better
bug-free software but also act as a powerful tool when proving the
validity of a patch. I plan to devote the next couple weeks to making
test-driven-development a habit.&lt;/p&gt;
</content><category term="misc"/><category term="Opinion"/><category term="Python"/></entry><entry><title>miniuri parser and ago human timedelta</title><link href="https://russell.ballestrini.net/miniuri-parser-and-ago-human-timedelta/" rel="alternate"/><published>2012-06-29T21:30:00-04:00</published><updated>2012-06-29T21:30:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-06-29:/miniuri-parser-and-ago-human-timedelta/</id><content type="html">&lt;p&gt;I just packaged and published a couple of python modules to pypi:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://pypi.python.org/pypi/ago"&gt;ago&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://pypi.python.org/pypi/miniuri"&gt;miniuri&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To install them, run:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Install:&lt;/em&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;--upgrade&lt;span class="w"&gt; &lt;/span&gt;ago&lt;span class="w"&gt; &lt;/span&gt;miniuri
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you want to view their source code, look here:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://bitbucket.org/russellballestrini/miniuri"&gt;miniuri&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://bitbucket.org/russellballestrini/ago"&gt;ago&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I hope you enjoy them.&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="Project"/><category term="Python"/></entry><entry><title>My top five suggestions for an independent developer creating a new product or service</title><link href="https://russell.ballestrini.net/my-top-five-suggestions-for-an-independent-developer-creating-a-new-product-or-service/" rel="alternate"/><published>2012-06-25T19:50:00-04:00</published><updated>2012-06-25T19:50:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-06-25:/my-top-five-suggestions-for-an-independent-developer-creating-a-new-product-or-service/</id><summary type="html">&lt;p&gt;&lt;strong&gt;1. Write everyday.&lt;/strong&gt; Build a blog for the project and write about
milestones, progress, and hurdles. Also keep a personal blog and write
about hobbies. Read some theory about &amp;quot;copy writing&amp;quot; and search engine
optimization. Write personalized email responses to customers. Great
communication skills will have the most impact on …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;1. Write everyday.&lt;/strong&gt; Build a blog for the project and write about
milestones, progress, and hurdles. Also keep a personal blog and write
about hobbies. Read some theory about &amp;quot;copy writing&amp;quot; and search engine
optimization. Write personalized email responses to customers. Great
communication skills will have the most impact on the success of your
company. The best way to increase communication skills is to do it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Spend at least 15 minutes on the company everyday.&lt;/strong&gt; Even small or
petty tasks will lift your company to the next goal. Over time the
company will grow strong and people will give you attention. Building a
company seems similar to building a character in an RPG. Instead however
you will progress your company to its next level. Also, always look
ahead when working, don't look back at prior achievements. Keep your
eyes on the next milestone and keep moving forward. This type of
behavior will promote growth and prevent getting stuck in the daily
grind.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Do not fear manual processes.&lt;/strong&gt; In the early stages of a company
you should only build automation which will enhance customer experience.
For everything else, build a repeatable manual process.
Coding automation will typically cost more time to build than it will save.
For example on &lt;a class="reference external" href="https://linkpeek.com"&gt;LinkPeek&lt;/a&gt; I have not automated customer account de-activation because fortunately I don't have to do many of them.
This manual process only takes a couple minutes to complete and gives me a chance to write a &amp;quot;thank you, and sorry to see you go&amp;quot; email to the fleeting customer.
Manually de-activating (loosing) a customer is also a humbling experience.
Keep a list of nice-to-have automations and review them in the far future.
How far in the future? Far, like when your idea is validated and your company appears successful.
Don't waste valuable time building automation for a service nobody will pay for or use.
Time is your most scarce resource.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Always attempt to increase your luck.&lt;/strong&gt; This might sound funny but
the the following suggestions should reduce risk and also increase luck:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Meet fellow lucky people&lt;/li&gt;
&lt;li&gt;Write on a blog&lt;/li&gt;
&lt;li&gt;Advertise your company on side-projects that lack monetization&lt;/li&gt;
&lt;li&gt;Think before wasting money using adsense or similar networks&lt;/li&gt;
&lt;li&gt;Do market research before committing to an idea&lt;/li&gt;
&lt;li&gt;Figure out how to make money before building product&lt;/li&gt;
&lt;li&gt;Build the smallest version of the product that could still be sold&lt;/li&gt;
&lt;li&gt;Try to stay focused on the things your customers will see&lt;/li&gt;
&lt;li&gt;Don't build an admin dashboard until successful (profitable?)&lt;/li&gt;
&lt;li&gt;Choose tasks that require the least effort but have the biggest
impact&lt;/li&gt;
&lt;li&gt;Don't be afraid to do things in the early stages that won't scale in
the long run (example: personal email responses)&lt;/li&gt;
&lt;li&gt;Listen to how early adopters describe your product; then on your
website, marketing and emails reuse their words.&lt;/li&gt;
&lt;li&gt;Attempt to keep marketing, newsletter, and support emails only few
sentences long.&lt;/li&gt;
&lt;li&gt;Start maintaining an opt-in email list&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You might have the best mouse-trap but without luck it will never gain
traction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Have an unhealthy passion to support your customers and build your
company.&lt;/strong&gt; Six days ago I suffered a major chemical burn to my right
eye. With my wife's help I continued to respond to support emails. Since
I launched &lt;a class="reference external" href="https://linkpeek.com"&gt;LinkPeek&lt;/a&gt; my
focus and attention to my customers and company have never faltered.
Dedication plays a key role to success.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2012/06/dedication-eye-chemical-burn.jpg"&gt;&lt;img alt="image0" src="/uploads/2012/06/dedication-eye-chemical-burn.jpg" /&gt;&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Greatest Hits"/><category term="LinkPeek"/><category term="Opinion"/></entry><entry><title>Prevent a certain program from running too long in bash</title><link href="https://russell.ballestrini.net/prevent-a-certain-program-from-running-to-long-in-bash/" rel="alternate"/><published>2012-06-13T09:45:00-04:00</published><updated>2012-06-13T09:45:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-06-13:/prevent-a-certain-program-from-running-to-long-in-bash/</id><summary type="html">&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt; - I opensourced this script here: &lt;a class="reference external" href="https://github.com/russellballestrini/bash-kira"&gt;bash kira&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I came up this this script to kill certain programs after they run for
too long. This works like similar to a timeout. Warning this script is
pretty harsh and kills the program.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
#!/bin/bash
PROGRAM=replace-with-program-name
PIDSFILE=/tmp/kill-these.pids

for …&lt;/pre&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt; - I opensourced this script here: &lt;a class="reference external" href="https://github.com/russellballestrini/bash-kira"&gt;bash kira&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I came up this this script to kill certain programs after they run for
too long. This works like similar to a timeout. Warning this script is
pretty harsh and kills the program.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
#!/bin/bash
PROGRAM=replace-with-program-name
PIDSFILE=/tmp/kill-these.pids

for pid in `pidof $PROGRAM`
  do
    if grep -q $pid $PIDSFILE
      then
        kill $pid
    fi
  done

&amp;gt; $PIDSFILE

for pid in `pidof $PROGRAM`
  do
    echo $pid &amp;gt;&amp;gt; $PIDSFILE
  done
&lt;/pre&gt;
&lt;p&gt;Then I wrote a cronjob to kill hung programs:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
* * * * * /usr/local/sbin/killprogs.sh
&lt;/pre&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/></entry><entry><title>Always attempt to scale vertically first</title><link href="https://russell.ballestrini.net/always-attempt-to-scale-vertically-first/" rel="alternate"/><published>2012-06-10T21:52:00-04:00</published><updated>2012-06-10T21:52:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-06-10:/always-attempt-to-scale-vertically-first/</id><summary type="html">&lt;p&gt;I spent the weekend fretting because one of my servers was basically
being DOS'd by paying customers. During the outage I started thinking
about the best way to scale and how I could make the code-base more
efficient.&lt;/p&gt;
&lt;p&gt;Linux top reported high load, in the 20's. Eventually I figured out …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I spent the weekend fretting because one of my servers was basically
being DOS'd by paying customers. During the outage I started thinking
about the best way to scale and how I could make the code-base more
efficient.&lt;/p&gt;
&lt;p&gt;Linux top reported high load, in the 20's. Eventually I figured out that
the server was having IO performance issues.&lt;/p&gt;
&lt;p&gt;I wasted a bunch of time attempting to fight fires. After about an hour
of that I decided to scale my VPS vertically by giving it an extra 256mb
of memory and a larger swap file (256mb to 1024mb).&lt;/p&gt;
&lt;p&gt;These two changes were surprisingly effective and the IO issues
resolved. Apparently the server was starving for memory which caused the
host to swap which brought things to a crawl waiting for IO.&lt;/p&gt;
&lt;p&gt;Crisis averted for the moment. Now I am free to think clearly and
engineer a proper solution instead of attempting to put out fires.&lt;/p&gt;
&lt;p&gt;If you ever encounter a similar situation, attempt the simplest fix.
There is no shame in throwing more money at a problem if it will buy you
time. In this case, an extra $10.00 a month relieved the performance
issues and bought myself some time, for the moment.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="https://russell.ballestrini.net/always-attempt-to-scale-vertically-first/vertical-scale-marked/"&gt;&lt;img alt="image0" src="/uploads/2012/06/vertical-scale-marked.png" /&gt;&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/><category term="LinkPeek"/></entry><entry><title>High load on web server after updating from Ubuntu 10.04 to Ubuntu 12.04 LTS</title><link href="https://russell.ballestrini.net/high-load-on-web-server-after-updating-from-ubuntu-10-04-to-ubuntu-12-04-lts/" rel="alternate"/><published>2012-05-19T18:40:00-04:00</published><updated>2012-05-19T18:40:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-05-19:/high-load-on-web-server-after-updating-from-ubuntu-10-04-to-ubuntu-12-04-lts/</id><summary type="html">&lt;p class="first last"&gt;Charts that show the load difference.&lt;/p&gt;
</summary><content type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;High load on web server after updating from Ubuntu 10.04 to Ubuntu
12.04 LTS&lt;/strong&gt;&lt;/div&gt;
&lt;div class="line"&gt;Check out charts which lineup to when I upgraded:&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;a class="reference external image-reference" href="https://russell.ballestrini.net/high-load-on-web-server-after-updating-from-ubuntu-10-04-to-ubuntu-12-04-lts/high-load-after-updating-ubuntu-from-10-04-lts-to-12-04-lts/"&gt;&lt;img alt="image0" src="/uploads/2012/05/high-load-after-updating-ubuntu-from-10.04-LTS-to-12.04-LTS.png" /&gt;&lt;/a&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;I couldn't determine the cause of the load average increase...&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; The issue might be memory bound. Check out this graph that
show much higher swap.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="https://russell.ballestrini.net/high-load-on-web-server-after-updating-from-ubuntu-10-04-to-ubuntu-12-04-lts/ubuntu-12-04-swap-year/"&gt;&lt;img alt="image1" src="/uploads/2012/05/ubuntu.12.04.swap_.year_.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;After much research this appears to be a load calculation and display
problem with the newer Linux kernels. The community has found Commit-ID:
c308b56b5398779cd3da0f62ab26b0453494c3d4 to be the problem. The commit
causes incorrect high reported load averages can be reported under
conditions of light load and high enter/exit idle frequency conditions
(greater then 25 hertz).&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A nice fellow named Doug from smythies.com researched
the topic between &lt;cite&gt;tick and tickless linux kernels and the effect they
have on load averages &amp;lt;http://www.smythies.com/~doug/network/load_average/new.html&amp;gt;&lt;/cite&gt;. You should check it out.&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/></entry><entry><title>How to rescue logs and config from a failed Citrix NetScaler App Gateway</title><link href="https://russell.ballestrini.net/how-to-rescue-logs-and-config-from-a-failed-citrix-netscaler-app-gateway/" rel="alternate"/><published>2012-05-11T00:05:00-04:00</published><updated>2012-05-11T00:05:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-05-11:/how-to-rescue-logs-and-config-from-a-failed-citrix-netscaler-app-gateway/</id><summary type="html">&lt;p&gt;Today our production Citrix NetScaler broke. The box wouldn't boot and
our only backup copy of the config was on the NetScaler itself.&lt;/p&gt;
&lt;p&gt;Being the only Unix guy around I attempted to help out the admins
working the outage. I SSH'd into the development NetScaler and noticed
it runs on …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Today our production Citrix NetScaler broke. The box wouldn't boot and
our only backup copy of the config was on the NetScaler itself.&lt;/p&gt;
&lt;p&gt;Being the only Unix guy around I attempted to help out the admins
working the outage. I SSH'd into the development NetScaler and noticed
it runs on FreeBSD.&lt;/p&gt;
&lt;p&gt;I suggested fetching the Hard drive and mounting it on a Linux computer.
The NetScaler has one SATA (not SAS) disk so my desktop was compatible.&lt;/p&gt;
&lt;p&gt;I installed the disk in the Linux tower and mounted the filesystem using
the following command:&lt;/p&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;mount &lt;span class="pre"&gt;--read-only&lt;/span&gt; &lt;span class="pre"&gt;--type=ufs&lt;/span&gt;&amp;nbsp; &lt;span class="pre"&gt;--test-opts&lt;/span&gt; ufstype=44bsd /dev/sda5 /mnt&lt;/tt&gt;&lt;/p&gt;
&lt;p&gt;Once mounted I was able to SCP interesting files to a safe location.&lt;/p&gt;
&lt;p&gt;Warning, this procedure might void your warranty. If in doubt, call
support first.&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/></entry><entry><title>The most valuable registration field: How did you hear about us?</title><link href="https://russell.ballestrini.net/the-most-valuable-registration-field-how-did-you-hear-about-us/" rel="alternate"/><published>2012-04-26T22:21:00-04:00</published><updated>2012-04-26T22:21:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-04-26:/the-most-valuable-registration-field-how-did-you-hear-about-us/</id><summary type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;How did you hear about us?&lt;/strong&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;I first answered this question when joining Linode.
I remember thinking &amp;quot;Wow, this is a great time to ask me!&amp;quot; because the real answer was still in my short term memory.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;When I launched &lt;a class="reference external" href="https://linkpeek.com/signup?plan=better"&gt;LinkPeek&lt;/a&gt; I applied this technique.
After an amazing launch (thank …&lt;/p&gt;</summary><content type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;How did you hear about us?&lt;/strong&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;I first answered this question when joining Linode.
I remember thinking &amp;quot;Wow, this is a great time to ask me!&amp;quot; because the real answer was still in my short term memory.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;When I launched &lt;a class="reference external" href="https://linkpeek.com/signup?plan=better"&gt;LinkPeek&lt;/a&gt; I applied this technique.
After an amazing launch (thank you colleagues from HackerNews) the true significance of this field was exposed.
I would argue that the answer to this simple question holds more value than collecting a username.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Here is why:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;It may seem obvious after reading this post, but this is what I learned
and I am sharing to help other startups.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Asking &amp;quot;How did you hear about us?&amp;quot; will help you determine:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;how your customers found you&lt;/li&gt;
&lt;li&gt;which of your marketing efforts are working&lt;/li&gt;
&lt;li&gt;where to spend money or time marketing (and where to stop)&lt;/li&gt;
&lt;li&gt;your true target market&lt;/li&gt;
&lt;li&gt;how your customers will use your product&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here are some other benefits to this technique. It opens a dialog or
conversation with the customer, which in return should help lower
friction during the sale. You already have the the customer engaged
during the signup process. I am firmly against the traditional method of
surveying customers. I believe the same data can be collected without
being abrasive or wasting the customers time.&lt;/p&gt;
&lt;blockquote&gt;
&amp;quot;How did you hear about us?&amp;quot; is the gift that keeps on giving.&lt;/blockquote&gt;
&lt;p&gt;It is commonly accepted that shorter registration forms lead to better
conversions. I totally agree, but in this case the sacrifice is worth
learning more about my target market and customer needs.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;How to ask this question properly:&lt;/strong&gt;&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;strong&gt;Just ask!&lt;/strong&gt; I second guessed my decision to add this field right
before launch but that anxiety promptly faded after reading the first
answer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don't use pre-filled answers!&lt;/strong&gt; Having pre-filled answers is
counter productive because you will not learn anything new. You are
forcing the user into choosing one of your answers...&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Allow the user to type as much as they like!&lt;/strong&gt; A few of my
customers nearly wrote a book in the text field.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;If you liked this post, you should follow me on twitter
`here &amp;lt;https://twitter.com/RussellBal&amp;gt;`__.&lt;/strong&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Greatest Hits"/><category term="LinkPeek"/><category term="Opinion"/></entry><entry><title>I just purchased Instagram for 1B and all I got was this lousy image filter</title><link href="https://russell.ballestrini.net/i-just-purchased-instagram-for-1b-and-all-i-got-was-this-lousy-image-filter/" rel="alternate"/><published>2012-04-09T17:47:00-04:00</published><updated>2012-04-09T17:47:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-04-09:/i-just-purchased-instagram-for-1b-and-all-i-got-was-this-lousy-image-filter/</id><summary type="html">&lt;div class="section" id="warning-update"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Warning / Update!&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This post was originally written from a place of jealousy and bitterness.&lt;/p&gt;
&lt;p&gt;Turns out I was wrong about this transaction and for better-or-worse, Facebook
(and Mark Zuckerburg) solidified their edge as the king of social for the last 6 years.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="original-post"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Original Post&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Facebook purchased Instagram on Monday, April …&lt;/strong&gt;&lt;/p&gt;&lt;/div&gt;</summary><content type="html">&lt;div class="section" id="warning-update"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Warning / Update!&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This post was originally written from a place of jealousy and bitterness.&lt;/p&gt;
&lt;p&gt;Turns out I was wrong about this transaction and for better-or-worse, Facebook
(and Mark Zuckerburg) solidified their edge as the king of social for the last 6 years.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="original-post"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Original Post&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Facebook purchased Instagram on Monday, April 9th 2012 for
$1,000,000,000 US.&lt;/strong&gt; Instagram, a free to use image sharing iPhone
application, has absolutely NO REVENUE STREAM. The application is free
to download, free to use, and has no monetization support from
advertisements.&lt;/p&gt;
&lt;blockquote&gt;
$1000000000.00 / 30000000 users = &lt;strong&gt;33.33 per user&lt;/strong&gt;&lt;/blockquote&gt;
&lt;p&gt;At the time of the sale Instagram had 30 million users in total
(30,000,000). That means Facebook just paid heavy &lt;strong&gt;$33.33&lt;/strong&gt; per user...
This is obviously a bad deal for Facebook.&lt;/p&gt;
&lt;p&gt;Instagram did not have a method to monetize its application. I hedge my
bets that Facebook will never recoup the one billion dollars spent on
this deal.&lt;/p&gt;
&lt;p&gt;Mark Zuckerburg, I just thought up the perfect T-Shirt for you:&lt;/p&gt;
&lt;blockquote&gt;
&lt;strong&gt;I just purchased Instagram for 1B and all I got was this lousy
image filter.&lt;/strong&gt;&lt;/blockquote&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#warning-update" id="toc-entry-1"&gt;Warning / Update!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#original-post" id="toc-entry-2"&gt;Original Post&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Opinion"/></entry><entry><title>Trouble mounting filesystem on KVM guest after reboot</title><link href="https://russell.ballestrini.net/trouble-mounting-filesystem-on-kvm-guest-after-reboot/" rel="alternate"/><published>2012-03-27T12:04:00-04:00</published><updated>2012-03-27T12:04:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-03-27:/trouble-mounting-filesystem-on-kvm-guest-after-reboot/</id><summary type="html">&lt;p&gt;&lt;strong&gt;Just found this out the hard way...&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It looks like the attachment of &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;/KVMROOT/guest-dev-app.img&lt;/span&gt;&lt;/tt&gt; on
guest-dev did not persist when the KVM host rebooted for patching.&lt;/p&gt;
&lt;p&gt;As it appears the &lt;tt class="docutils literal"&gt;virsh &lt;span class="pre"&gt;attach-disk&lt;/span&gt;&lt;/tt&gt; command works a lot like the
&lt;tt class="docutils literal"&gt;mount&lt;/tt&gt; command.&lt;/p&gt;
&lt;p&gt;In order to have a disk attachment persist …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;Just found this out the hard way...&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It looks like the attachment of &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;/KVMROOT/guest-dev-app.img&lt;/span&gt;&lt;/tt&gt; on
guest-dev did not persist when the KVM host rebooted for patching.&lt;/p&gt;
&lt;p&gt;As it appears the &lt;tt class="docutils literal"&gt;virsh &lt;span class="pre"&gt;attach-disk&lt;/span&gt;&lt;/tt&gt; command works a lot like the
&lt;tt class="docutils literal"&gt;mount&lt;/tt&gt; command.&lt;/p&gt;
&lt;p&gt;In order to have a disk attachment persist after a reboot, I think we still need to do a &lt;tt class="docutils literal"&gt;virsh edit&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;the &lt;tt class="docutils literal"&gt;virsh &lt;span class="pre"&gt;attach-disk&lt;/span&gt;&lt;/tt&gt; command is useful because it allows us to
attach disk images to guests without restarting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tldr;&lt;/strong&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;tt class="docutils literal"&gt;virsh &lt;span class="pre"&gt;attach-disk&lt;/span&gt;&lt;/tt&gt; is to &lt;tt class="docutils literal"&gt;mount&lt;/tt&gt; as&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;tt class="docutils literal"&gt;virsh edit&lt;/tt&gt; is to &lt;tt class="docutils literal"&gt;vim /etc/fstab&lt;/tt&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="DevOps"/></entry><entry><title>nosslsearch cname is a bad idea and solution</title><link href="https://russell.ballestrini.net/nosslsearch-cname-is-a-bad-idea-and-solution/" rel="alternate"/><published>2012-03-26T11:51:00-04:00</published><updated>2012-03-26T11:51:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-03-26:/nosslsearch-cname-is-a-bad-idea-and-solution/</id><summary type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;Google SafeSearch and SSL Search for Schools suggests implementing
the following changes to the network:&lt;/strong&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote&gt;
To utilize the no SSL option for your network, configure the DNS
entry for www.google.com to be a CNAME for nosslsearch.google.com.&lt;/blockquote&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;Here are the reasons why this is a bad idea …&lt;/strong&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</summary><content type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;Google SafeSearch and SSL Search for Schools suggests implementing
the following changes to the network:&lt;/strong&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote&gt;
To utilize the no SSL option for your network, configure the DNS
entry for www.google.com to be a CNAME for nosslsearch.google.com.&lt;/blockquote&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;Here are the reasons why this is a bad idea and solution:&lt;/strong&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;In order to create a CNAME record for www.google.com we need to
become an authoritative master of that zone.&lt;/li&gt;
&lt;li&gt;If you become an authoritative master you need to host all of
Google's DNS resource records for the domain.&lt;/li&gt;
&lt;li&gt;Google is asking us to DNS poison it's flag ship product on our
networks.&lt;/li&gt;
&lt;li&gt;If other companies follow suit the internet will quickly become
unmanageable. DNS was not ment to work this way.&lt;/li&gt;
&lt;li&gt;Not all networks have a local DNS server&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;This is a bad idea. Please change your stance on this matter.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Reference:
&lt;a class="reference external" href="https://support.google.com/websearch/bin/answer.py?hl=en&amp;amp;answer=186669"&gt;https://support.google.com/websearch/bin/answer.py?hl=en&amp;amp;answer=186669&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Opinion"/><category term="Security"/></entry><entry><title>"I see" said the blind man, to the deaf dog, as he walked off the cliff.</title><link href="https://russell.ballestrini.net/i-see-said-the-blind-man-to-the-deaf-dog-as-he-walked-off-the-cliff/" rel="alternate"/><published>2012-03-23T22:04:00-04:00</published><updated>2012-03-23T22:04:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-03-23:/i-see-said-the-blind-man-to-the-deaf-dog-as-he-walked-off-the-cliff/</id><content type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;&amp;quot;I see&amp;quot; said the blind man, to the deaf dog, as he walked off the
cliff.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;As far as I can tell, I am the originator of this version of this quote.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EDIT:&lt;/strong&gt; changed &amp;quot;originator of the quote&amp;quot; to &amp;quot;originator of this
version of this quote&amp;quot;.&lt;/p&gt;
</content><category term="misc"/><category term="Opinion"/></entry><entry><title>What do you name your python virtualenv?</title><link href="https://russell.ballestrini.net/what-do-you-name-your-python-virtualenv/" rel="alternate"/><published>2012-03-07T21:42:00-05:00</published><updated>2012-03-07T21:42:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-03-07:/what-do-you-name-your-python-virtualenv/</id><content type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;What do you name your python virtualenv?&lt;/strong&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;I name my virtualenv 'virtpy'. Is there a standard name being used out
there?&lt;/p&gt;
&lt;p&gt;Maybe we can come to a consensus as a standard name? Please feel free to
post your virtualenv names here as a sort of poll.&lt;/p&gt;
</content><category term="misc"/><category term="Opinion"/><category term="Python"/></entry><entry><title>How to save hundreds of dollars on groceries without clipping coupons</title><link href="https://russell.ballestrini.net/how-to-save-hundreds-of-dollars-on-groceries-without-clipping-coupons/" rel="alternate"/><published>2012-03-03T19:54:00-05:00</published><updated>2012-03-03T19:54:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-03-03:/how-to-save-hundreds-of-dollars-on-groceries-without-clipping-coupons/</id><summary type="html">&lt;p class="first last"&gt;Jenn explains how she drastically decreased our grocery bill.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;strong&gt;This is the first time I've had a guest blogger on my site. It may
sound campy but my wife Jenn wrote this article after explaining how our
grocery bill decreased so drastically.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="https://russell.ballestrini.net/how-to-save-hundreds-of-dollars-on-groceries-without-clipping-coupons/3247325203_6108897833_o/"&gt;&lt;img alt="image0" src="/uploads/2012/03/3247325203_6108897833_o.jpg" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to save hundreds of dollars on groceries without clipping
coupons&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I was recently re-working the cabinets of the kitchen, anticipating a
new and organized year. With a growing family and limited space, I
decided to do some consolidating.&lt;/p&gt;
&lt;p&gt;I allocated a whole section of cabinet space to arts and craft supplies
for the kids (play-doh, paints, etc), and another to bottles and
formula. I suddenly realized that my food products remained scattered
across the counter and there was only one lonely cabinet left to fill.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="https://russell.ballestrini.net/how-to-save-hundreds-of-dollars-on-groceries-without-clipping-coupons/6876071849_b82bca1076_o/"&gt;&lt;img alt="image1" src="/uploads/2012/03/6876071849_b82bca1076_o.jpg" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Among the food items on the counter, many were far expired, forgotten in
the dusty back corners of the cabinets. I thought to myself “Hey! I
worked hard to find coupons and sales for all these things.” and I
regretfully filled a garbage bag with old food.&lt;/p&gt;
&lt;p&gt;I had been clipping coupons and striving to match the savings of super
couponers. These couponers, featured on popular reality shows, often
recommend stock-piling when food items go on sale and you have coupons
to purchase them.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="https://russell.ballestrini.net/how-to-save-hundreds-of-dollars-on-groceries-without-clipping-coupons/6876069657_5a2f4ae487_o/"&gt;&lt;img alt="image2" src="/uploads/2012/03/6876069657_5a2f4ae487_o.jpg" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I opened the refrigerator to find a similar and familiar situation: a
head of lettuce that had seen better days, a tomato that had lost it's
freshness, and other food items that were past the date of recommended
consumption.&lt;/p&gt;
&lt;p&gt;Looking into the garbage bag, I realized that... every week I was
wastefully throwing food and money away. I decided to make a commitment
that has cut my grocery bill in half! Do I still use coupons? YES, if
they fit into my weekly plan. Do I still look at the sale flyers?
ABSOLUTELY! But...&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;These two strategies have saved me much more money:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;1. Cut down food storage spaces&lt;/strong&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;If you currently use 4 food cabinets try designating only 2. In my
case I consolidated my food items to 1 cabinet. This consolidation
saved money in two distinct ways:&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;em&gt;First,&lt;/em&gt; I am easily able to assess which food items I have and when
they expire. This helps prevent re-buying a product we already have
at home (Remember the time you were at the grocery store buying all
the ingredients for those chocolate chip cookies and couldn't
remember if you had any brown sugar? Undoubtebly you re-bought,
probably to discover you had a brand new, unopened package in the
cabinet).&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Secondly,&lt;/em&gt; it prevents stock piling. I can buy only what I need for
the week ahead, after all, it is all I have room for. If you generate
the sense that “my kitchen is full of food”, it eliminates the need
to buy for the sake of filling the cabinets.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;2. Let your menu dictate your shopping list&lt;/strong&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;My son complains “There are no more crackers.&amp;quot; My husband sighs,
“We're all out of bananas.&amp;quot; My answer used to be probably a lot like
yours “OK, I'll put it on my list.&amp;quot; I had the perception that I
constantly needed to replenish. It is unnecessary and wasteful to have
three different brands of crackers, five types of cereal, and every
kind of fruit known to the produce section. Without fail, I was
throwing away money each week.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Instead, I sit down each week with two sheets of paper. One serves as
dinner menu for the week and the other as my shopping list. When
building my menu, I consider the sales and coupons I have on hand. If I
notice a great deal on pasta sauce, we might have spaghetti one night
and meatball subs another. I immediately begin a shopping list writing
ONLY items used for the companion menu. Additionally, I add two
breakfast choices and two lunch choices for the week if needed (some
breakfast items such as pancake batter or a family size box of cereal
often last longer). I also include 2 choices of fresh fruit or
vegetable. To reduce waste and cost I ONLY add the staples (eggs, milk,
and bread) when it is on the menu. If none of my breakfast, lunch, or
dinner choices call for bread, I skip that aisle for this trip.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It took me only a few short weeks to notice that by cutting my food
storage space in half, and adopting a disciplined approach to menu and
list making, I had saved hundreds of dollars. I challenge you to cut
down your food storage spaces and let your menu dictate your shopping
list!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="https://russell.ballestrini.net/how-to-save-hundreds-of-dollars-on-groceries-without-clipping-coupons/6876068965_db8a5d12b2_o/"&gt;&lt;img alt="image3" src="/uploads/2012/03/6876068965_db8a5d12b2_o.jpg" /&gt;&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Greatest Hits"/><category term="Opinion"/></entry><entry><title>How to capture HTTPS SSL TLS packets with wireshark</title><link href="https://russell.ballestrini.net/how-to-capture-https-ssl-tls-packets-with-wireshark/" rel="alternate"/><published>2012-02-29T19:14:00-05:00</published><updated>2012-02-29T19:14:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-02-29:/how-to-capture-https-ssl-tls-packets-with-wireshark/</id><summary type="html">&lt;p&gt;This article will explain how to use wireshark to capture TCP/IP
packets. Specifically I will show how to capture encrypted (HTTPS)
packets and attempt to document the &amp;quot;dance&amp;quot; a client and server do to
build an SSL tunnel.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is Wireshark?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Wireshark is a network protocol analyzer for Windows …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This article will explain how to use wireshark to capture TCP/IP
packets. Specifically I will show how to capture encrypted (HTTPS)
packets and attempt to document the &amp;quot;dance&amp;quot; a client and server do to
build an SSL tunnel.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is Wireshark?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Wireshark is a network protocol analyzer for Windows, OSX, and Linux. It
lets you capture and interactively browse the traffic running on a
computer network. Similar software includes tcpdump on Linux.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Install Wireshark&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;First step, acquire Wireshark for your operating system.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Ubuntu Linux:&lt;/em&gt; &lt;tt class="docutils literal"&gt;sudo &lt;span class="pre"&gt;apt-get&lt;/span&gt; install wireshark&lt;/tt&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Windows or Mac OSX:&lt;/em&gt; search for wireshark and download the binary.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to capture packets&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is Wireshark's main menu:&lt;/p&gt;
&lt;p&gt;&lt;img alt="image0" src="/uploads/2012/02/wireshark.png" /&gt;&lt;/p&gt;
&lt;p&gt;To start a capture, click the following icon:&lt;/p&gt;
&lt;p&gt;&lt;img alt="image1" src="/uploads/2012/02/wireshark-start-capture.png" /&gt;&lt;/p&gt;
&lt;p&gt;A new dialog box should have appeared. Click start on your preferred
interface:&lt;/p&gt;
&lt;p&gt;&lt;img alt="image2" src="/uploads/2012/02/wireshark-sniff.png" /&gt;&lt;/p&gt;
&lt;p&gt;You are now capturing packets. The packet information is displayed in
the table below the main menu:&lt;/p&gt;
&lt;p&gt;&lt;img alt="image3" src="/uploads/2012/02/wireshark-packets.png" /&gt;&lt;/p&gt;
&lt;p&gt;Now browse to an HTTPS website with your browser. I went to
&lt;a class="reference external" href="https://linkpeek.com"&gt;https://linkpeek.com&lt;/a&gt; and after the page completely loaded, I stopped the
Wireshark capture:&lt;/p&gt;
&lt;p&gt;&lt;img alt="image4" src="/uploads/2012/02/wireshark-stop-capture.png" /&gt;&lt;/p&gt;
&lt;p&gt;Depending on your network, you could have just captured MANY packets. To
limit our view to only interesting packets you may apply a filter.
Filter the captured packets by ssl and hit Apply:&lt;/p&gt;
&lt;p&gt;&lt;img alt="image5" src="/uploads/2012/02/wireshark-filter.png" /&gt;&lt;/p&gt;
&lt;p&gt;Now we should be only looking at SSL packets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next we will analyze the SSL packets and answer a few questions&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; For each of the first 8 Ethernet frames, specify the source of
the frame (client or server), determine the number of SSL records that
are included in the frame, and list the SSL record types that are
included in the frame. Draw a timing diagram between client and server,
with one arrow for each SSL record.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Frame 1 client | 1 record | Arrival Time: Feb 15, 2012
15:38:55.601588000&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Frame 2 server | 1 record | Arrival Time: Feb 15, 2012
15:38:55.688170000&lt;/div&gt;
&lt;div class="line"&gt;Frame 3 server | 2 record | Arrival Time: Feb 15, 2012
15:38:55.688628000&lt;/div&gt;
&lt;div class="line"&gt;Frame 4 client | 3 record | Arrival Time: Feb 15, 2012
15:38:55.697705000&lt;/div&gt;
&lt;div class="line"&gt;frame 5 server | 2 record | Arrival Time: Feb 15, 2012
15:38:55.713139000&lt;/div&gt;
&lt;div class="line"&gt;frame 6 client | 1 record | Arrival Time: Feb 15, 2012
15:38:55.713347000&lt;/div&gt;
&lt;div class="line"&gt;frame 7 server | 0 record | Arrival Time: Feb 15, 2012
15:38:55.713753000&lt;/div&gt;
&lt;div class="line"&gt;frame 8 server | 1 record | Arrival Time: Feb 15, 2012
15:38:55.715003000&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; Each of the SSL records begins with the same three fields (with
possibly different values). One of these fields is “content type” and
has length of one byte. List all three fields and their lengths.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Each hexadecimal digit (also called a &amp;quot;nibble&amp;quot;) represents four binary
digits (bits) so each pair of hexadecimal digits equals 1 byte.&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;a. Destination mac address | 6 btyes | 00 21 9b 31 99 51&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;b. Source mac address | 6 bytes | 00 10 db ff 20&lt;/div&gt;
&lt;div class="line"&gt;c. Type: IP | 2 byte | 08 00&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;ClientHello Records&lt;/strong&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;3.&lt;/strong&gt;Expand the ClientHello record. (If your trace contains
multiple ClientHello&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;records, expand the frame that contains the first one.) What is the
value of the&lt;/div&gt;
&lt;div class="line"&gt;content type?&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line"&gt;hex: 16 (16+6=22) Handshake&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;4.&lt;/strong&gt; Does the ClientHello record advertise the cipher suites it
supports? If so, in the first listed suite, what are the public-key
algorithm, the symmetric-key algorithm, and the hash algorithm?&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;MD5, SHA, RSA, DSS, DES, AES&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;ServertHello Records&lt;/strong&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;5.&lt;/strong&gt; Look to the ServerHello packet. What cipher suite does it
choose?&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA (0x002f)&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;6.&lt;/strong&gt; Does this record include a nonce? If so, how long is it? What
is the purpose of the&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;client and server nonces in SSL?&lt;/div&gt;
&lt;div class="line"&gt;Yes, 28 bytes. The ClientHello packet also generated a nonces. They
are used to make the session communication between the two nodes
unique. It &amp;quot;salts&amp;quot; the communication to prevent replay attacks. A
replay attack happens when data from old communications is used to
&amp;quot;crack&amp;quot; a current communication.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;7.&lt;/strong&gt;Does this record include a session ID? What is the purpose of
the session ID?&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Yes, This is to make things efficient, in case the client has any
plans of closing the current connection and reconnect in the near
future.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;8.&lt;/strong&gt;How many frames does the SSL certificate take to send?&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;In this case it took 4 frames&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Guide"/><category term="Security"/></entry><entry><title>Zenoss or Nagios monitoring of HTTPS using client certificate authentication</title><link href="https://russell.ballestrini.net/zenoss-or-nagios-monitoring-of-https-using-client-certificate-authentication/" rel="alternate"/><published>2012-02-26T19:21:00-05:00</published><updated>2012-02-26T19:21:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-02-26:/zenoss-or-nagios-monitoring-of-https-using-client-certificate-authentication/</id><summary type="html">&lt;p&gt;I recently needed to monitor an HTTPS API for response time and
availability. At first I planned to just use the Nagios check_http
command.&lt;/p&gt;
&lt;p&gt;After gathering more requirements I learned that the API was protected
by client certificate authentication. After some research I quickly
found that no solution existed to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I recently needed to monitor an HTTPS API for response time and
availability. At first I planned to just use the Nagios check_http
command.&lt;/p&gt;
&lt;p&gt;After gathering more requirements I learned that the API was protected
by client certificate authentication. After some research I quickly
found that no solution existed to monitor HTTP protected by client
certs. I needed to write my own plugin.&lt;/p&gt;
&lt;p&gt;This is the python plugin I came up with:
&lt;strong&gt;check_http_client_cert.py&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
#!/usr/bin/python

&amp;quot;&amp;quot;&amp;quot;Nagios/Zenoss client cert https checker&amp;quot;&amp;quot;&amp;quot;

import httplib
from optparse import OptionParser
from time import time
from sys import exit

def request( hostname, port, cert_file, path ):
    &amp;quot;&amp;quot;&amp;quot;request a resource and return response object&amp;quot;&amp;quot;&amp;quot;
    try:
        c = httplib.HTTPSConnection( hostname, port, cert_file=cert_file )
        c.request( &amp;quot;GET&amp;quot;, path )
        return c.getresponse()
    except:
        return False

if __name__ == '__main__':
    parser = OptionParser()
    parser.add_option('-H', '--hostname', dest='hostname')
    parser.add_option('-p', '--port', dest='port')
    parser.add_option('-c', '--cert_file', dest='cert_file')
    parser.add_option('-P', '--path', dest='path',
    help=&amp;quot;Path relative to root, like /image/search&amp;quot;)

    o, args = parser.parse_args()
    #print o

    start = time()
    r = request( o.hostname, o.port, o.cert_file, o.path )
    elapse = time() - start

    if r:
        if r.status &amp;gt;= 200 and r.status &amp;lt; 400:
            print &amp;quot;HTTP OK:&amp;quot;, r.status, r.reason, &amp;quot;|time=&amp;quot; + str(elapse) + &amp;quot;s;;;&amp;quot;
            exit( 0 )
        print &amp;quot;HTTP Critical:&amp;quot;, r.status, r.reason

    exit( 2 )
&lt;/pre&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Python"/></entry><entry><title>Jake, Finn, and Ice King</title><link href="https://russell.ballestrini.net/jake/" rel="alternate"/><published>2012-02-18T13:34:00-05:00</published><updated>2012-02-18T13:34:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-02-18:/jake/</id><summary type="html">&lt;p class="first last"&gt;My Adventure Time art, painted with MyPaint and wacom tablet.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;strong&gt;Jake from Adventure Time, painted with MyPaint and wacom tablet.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I did this because my 4 month old &amp;quot;cracks-up&amp;quot; laughing whenever this
character is on screen. I think the combination of colors and Jake's
voice, played by John William DiMaggio, get him going.&lt;/p&gt;
&lt;p&gt;&lt;img alt="I drew Jake from Adventure Time" src="/uploads/2012/02/mypaint-jake-adventure-time.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Finn from Adventure Time.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Took a shot at painting Finn.&lt;/p&gt;
&lt;p&gt;&lt;img alt="I drew Finn from Adventure Time" src="/uploads/2012/02/mypaint-finn-adventure-time.png" /&gt;&lt;/p&gt;
&lt;p&gt;I'm on a roll! Here is Ice King.&lt;/p&gt;
&lt;p&gt;&lt;img alt="I drew Ice King from Adventure Time" src="/uploads/2012/02/ice-king-adventure-time.png" /&gt;&lt;/p&gt;
&lt;p&gt;Jake standing up, painted this this morning&lt;/p&gt;
&lt;p&gt;&lt;img alt="I drew Jake again from Adventure Time" src="/uploads/2012/02/jake-standing.png" /&gt;&lt;/p&gt;
&lt;p&gt;Lumpy princess&lt;/p&gt;
&lt;p&gt;&lt;img alt="I drew lumpy princess from Adventure Time" src="/uploads/2012/02/lump-princess.png" /&gt;&lt;/p&gt;
&lt;p&gt;I'm getting faster, this was completed in 20 minutes&lt;/p&gt;
&lt;p&gt;&lt;img alt="I drew sad Jake from Adventure Time" src="/uploads/2012/02/sad-jake.png" /&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Art"/></entry><entry><title>My 4 month old's 15 minutes of fame</title><link href="https://russell.ballestrini.net/my-4-month-olds-15-minutes-of-fame/" rel="alternate"/><published>2012-02-16T18:42:00-05:00</published><updated>2012-02-16T18:42:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-02-16:/my-4-month-olds-15-minutes-of-fame/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;a class="reference external image-reference" href="https://russell.ballestrini.net/my-4-month-olds-15-minutes-of-fame/cutest-redsox-fan-ever/"&gt;&lt;img alt="image0" src="/uploads/2012/02/cutest-redsox-fan-ever.jpg" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;My sister &lt;a class="reference external" href="https://twitter.com/#!/VeronicaBal"&gt;&amp;#64;VeronicaBal&lt;/a&gt;
was watching my son (&lt;a class="reference external" href="https://twitter.com/#!/RussellBal"&gt;&amp;#64;RussellBal&lt;/a&gt;)
tweeted a picture of him.&lt;/p&gt;
&lt;p&gt;The official &lt;a class="reference external" href="https://twitter.com/#!/RedSox"&gt;&amp;#64;RedSox&lt;/a&gt; re-tweeted the image to all 197,035 followers!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I'm a proud daddy!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="https://russell.ballestrini.net/my-4-month-olds-15-minutes-of-fame/screenshot-at-2012-02-16-172630/"&gt;&lt;img alt="image1" src="/uploads/2012/02/Screenshot-at-2012-02-16-172630.png" /&gt;&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Opinion"/></entry><entry><title>Block cipher lab</title><link href="https://russell.ballestrini.net/block-cipher-lab/" rel="alternate"/><published>2012-02-14T01:36:00-05:00</published><updated>2012-02-14T01:36:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-02-14:/block-cipher-lab/</id><summary type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;Consider the following block cipher.&lt;/strong&gt; Suppose that each block
cipher T simply reverses the order of the eight input bits (so that,
for example 11110000 becomes 00001111).&lt;/div&gt;
&lt;div class="line"&gt;Further suppose that the 64-bit scrambler does not modify any bits.
With n = 3 iterations and the original 64-bit input equal to 10100000 …&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;</summary><content type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;Consider the following block cipher.&lt;/strong&gt; Suppose that each block
cipher T simply reverses the order of the eight input bits (so that,
for example 11110000 becomes 00001111).&lt;/div&gt;
&lt;div class="line"&gt;Further suppose that the 64-bit scrambler does not modify any bits.
With n = 3 iterations and the original 64-bit input equal to 10100000
repeated eight times, what is the value of the output?&lt;/div&gt;
&lt;div class="line"&gt;Now change the last bit of the original 64-bit input from 0 to a 1.
Now suppose that the 64-bit scrambler inverses the order of the 64
bits.&lt;/div&gt;
&lt;div class="line"&gt;&lt;strong&gt;Solution in python:&lt;/strong&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre class="literal-block"&gt;
def chunks( l, n ):
    &amp;quot;&amp;quot;&amp;quot;accept a list and chuck size, return chunks&amp;quot;&amp;quot;&amp;quot;
    return [ l[ i:i+n ] for i in range( 0, len(l), n ) ]


def T( blocks ):
    &amp;quot;&amp;quot;&amp;quot;for each block, reverse block, return blocks&amp;quot;&amp;quot;&amp;quot;
    result = []
    for block in blocks:
        result.append( ''.join( [bit for bit in reversed( block )] ) )

    return result

def scrambler( input ):
    &amp;quot;&amp;quot;&amp;quot;inverse the order of input&amp;quot;&amp;quot;&amp;quot;
    return ''.join( [i for i in reversed( input ) ] )


def cipher1( input, n = 3, chunk_length = 8 ):
    &amp;quot;&amp;quot;&amp;quot;make chucks out of input, reverse each chunk return result&amp;quot;&amp;quot;&amp;quot;
    blocks = chunks( input, chunk_length )
    for i in range( 0, n ): blocks = T( blocks )
    return ''.join( blocks )


def cipher2( input, n = 3, chunk_length = 8 ):
    &amp;quot;&amp;quot;&amp;quot;same as cipher1 but with scrambler&amp;quot;&amp;quot;&amp;quot;
    blocks = chunks( input, chunk_length )
    for i in range( 0, n ):
        blocks = T( blocks )
        blocks = chunks( scrambler( ''.join( blocks ) ), chunk_length )
    return ''.join( blocks )


if __name__ == &amp;quot;__main__&amp;quot;:

    input = &amp;quot;1010000010100000101000001010000010100000101000001010000010100000&amp;quot;
    print cipher1( input )
    # output: 0000010100000101000001010000010100000101000001010000010100000101

    input = &amp;quot;1010000010100000101000001010000010100000101000001010000010100001&amp;quot;
    print cipher1( input )
    # output: 0000010100000101000001010000010100000101000001010000010110000101

    input = &amp;quot;1010000010100000101000001010000010100000101000001010000010100000&amp;quot;
    print cipher2( input )
    # output: 1010000010100000101000001010000010100000101000001010000010100000

    input = &amp;quot;1010000010100000101000001010000010100000101000001010000010100001&amp;quot;
    print cipher2( input )
    # output: 1010000110100000101000001010000010100000101000001010000010100000
&lt;/pre&gt;
</content><category term="misc"/><category term="Code"/><category term="Security"/><category term="Python"/></entry><entry><title>Monoalphabetic Cipher and Inverse Written in Python</title><link href="https://russell.ballestrini.net/monoalphabetic-cipher-and-inverse-written-in-python/" rel="alternate"/><published>2012-02-13T22:39:00-05:00</published><updated>2012-02-13T22:39:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-02-13:/monoalphabetic-cipher-and-inverse-written-in-python/</id><summary type="html">&lt;div class="section" id="introduction-and-background"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;introduction and background&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A monoalphabetic cipher uses fixed substitution over the entire message.&lt;/p&gt;
&lt;p&gt;You can build a monoalphabetic cipher using a Python dictionary, like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;monoalpha_cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;m&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;d&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;v&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;e&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;f&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;x&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;g&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;z&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;h&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;i&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;s&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;j&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;d&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;k&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;f&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;l&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;g …&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;</summary><content type="html">&lt;div class="section" id="introduction-and-background"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;introduction and background&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A monoalphabetic cipher uses fixed substitution over the entire message.&lt;/p&gt;
&lt;p&gt;You can build a monoalphabetic cipher using a Python dictionary, like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;monoalpha_cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;m&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;d&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;v&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;e&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;f&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;x&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;g&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;z&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;h&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;i&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;s&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;j&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;d&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;k&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;f&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;l&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;g&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;m&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;h&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;j&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;o&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;k&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;p&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;l&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;q&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;p&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;r&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;o&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;s&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;i&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;t&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;u&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;u&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;v&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;t&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;w&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;r&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;x&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;e&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;w&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;z&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;q&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can create an inverse of this cipher dictionary by switching the key and value places:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;inverse_monoalpha_cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;monoalpha_cipher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iteritems&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;inverse_monoalpha_cipher&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now that we have both the cipher and the inverse_cipher, we may encrypt a message.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Encryption example:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;This is an easy problem&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;encrypted_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;letter&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;encrypted_message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monoalpha_cipher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encrypted_message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;Result:&lt;/dt&gt;
&lt;dd&gt;&lt;tt class="docutils literal"&gt;Tasi si mj cmiw lokngch&lt;/tt&gt;&lt;/dd&gt;
&lt;/dl&gt;
&lt;p&gt;Using the inverse_cipher, We may &lt;em&gt;decrypt&lt;/em&gt; a message.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decryption example:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;encrypted_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;rmij&amp;#39;u uamu xyj?&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;decrypted_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;letter&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;encrypted_message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
     &lt;span class="n"&gt;decrypted_message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;inverse_monoalpha_cipher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;decrypted_message&lt;/span&gt; &lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;Result:&lt;/dt&gt;
&lt;dd&gt;&lt;tt class="docutils literal"&gt;wasn't that fun?&lt;/tt&gt;&lt;/dd&gt;
&lt;/dl&gt;
&lt;/div&gt;
&lt;div class="section" id="monoalphabetic-cipher-py"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;monoalphabetic_cipher.py&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here is a toy library I wrote to make the process repeatable -&lt;/p&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;monoalphabetic_cipher.py&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;string&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;letters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digits&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;random&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;shuffle&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;random_monoalpha_cipher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Generate a Monoalphabetic Cipher&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;letters&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;digits&lt;/span&gt;
    &lt;span class="n"&gt;original_pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;shuffled_pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shuffled_pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original_pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shuffled_pool&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;inverse_monoalpha_cipher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monoalpha_cipher&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Given a Monoalphabetic Cipher (dictionary) return the inverse.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;inverse_monoalpha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;monoalpha_cipher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iteritems&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;inverse_monoalpha&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;inverse_monoalpha&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;encrypt_with_monoalpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monoalpha_cipher&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;encrypted_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;letter&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;encrypted_message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monoalpha_cipher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encrypted_message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decrypt_with_monoalpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encrypted_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monoalpha_cipher&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;encrypt_with_monoalpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="n"&gt;encrypted_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="n"&gt;inverse_monoalpha_cipher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monoalpha_cipher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="monoalphabetic-cipher-py-example-usage"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;monoalphabetic_cipher.py example usage&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Here I show how to use the library:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;# load the module / library as &amp;#39;mc&amp;#39;.&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;monoalphabetic_cipher&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;mc&lt;/span&gt;


&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;# generate a random cipher (only if needed).&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random_monoalpha_cipher&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;# output the cipher (store for safe keeping).&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;# encrypt a message with the cipher.&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encrypt_with_monoalpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Hello all you hackers out there!&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="s1"&gt;&amp;#39;sXGGt SGG Nt0 HSrLXFC t0U UHXFX!&amp;#39;&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;# decrypt a message with the cipher.&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decrypt_with_monoalpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;sXGGt SGG Nt0 HSrLXFC t0U UHXFX!&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="s1"&gt;&amp;#39;Hello all you hackers out there!&amp;#39;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#introduction-and-background" id="toc-entry-1"&gt;introduction and background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#monoalphabetic-cipher-py" id="toc-entry-2"&gt;monoalphabetic_cipher.py&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#monoalphabetic-cipher-py-example-usage" id="toc-entry-3"&gt;monoalphabetic_cipher.py example usage&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Security"/><category term="Python"/></entry><entry><title>Why does a Hash provide better message integrity then an Internet checksum?</title><link href="https://russell.ballestrini.net/why-does-a-hash-provide-better-message-integrity-then-an-internet-checksum/" rel="alternate"/><published>2012-02-13T19:19:00-05:00</published><updated>2012-02-13T19:19:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-02-13:/why-does-a-hash-provide-better-message-integrity-then-an-internet-checksum/</id><summary type="html">&lt;p&gt;&lt;strong&gt;Why does a Hash provide better message integrity then an Internet
checksum?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Hash function and checksum function both return a value which cannot be
reversed.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;An Internet checksum (TCP checksum or IP checksum) is designed to
detect common errors quickly and efficiently. An Internet checksum
does not attempt to prevent …&lt;/div&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;Why does a Hash provide better message integrity then an Internet
checksum?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Hash function and checksum function both return a value which cannot be
reversed.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;An Internet checksum (TCP checksum or IP checksum) is designed to
detect common errors quickly and efficiently. An Internet checksum
does not attempt to prevent collisions.&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;em&gt;Man cksum for more info.&lt;/em&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;A Hash provides better message integrity because it has less collisions
then an Internet checksum. A collision means there is more then one way
to produce the same sum. A great hash function aims to reduce the
occurrence of collisions. &lt;em&gt;Man md5 and sha for more info.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is a collision&lt;/strong&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Let H() be a hash function. Let x and y be two differing messages.&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;em&gt;H(x) = H(y)&lt;/em&gt; would be a collision.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;I like to use python to show examples of hash functions&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In this example I pass a message into the MD5 hash function to produce a
resulting hash of the message. You can think of this hashed output as a
finger print of the message.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
import hashlib as h
message = &amp;quot;This message will be placed into an MD5 hash function to authenticate its integrity.&amp;quot;
print h.md5(message).hexdigest()
&lt;/pre&gt;
&lt;/p&gt;&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;em&gt;Hash Output:&lt;/em&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;pre class="literal-block"&gt;
18f189f94b245ad8566206c199b4f60a
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Now If I passed that message to you along with its MD5 hash hex
representation, you could put the message into your own MD5 hash
function and compare the resulting hash. This method is used to validate
the message or verify data integrity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can you &amp;quot;decrypt&amp;quot; a hash of a message to get the original message&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No! A hash may &lt;em&gt;not&lt;/em&gt; be reversed, which means it cannot be decrypted.&lt;/p&gt;
&lt;/p&gt;&lt;p&gt;By design a hash algorithm has no inverse, there is no way to get the
original message from the hash. This is good news, turns out we have
some really great applications for this type of function. We can
validate messages, we can securely store passwords, and we can quickly
determine if a message or file has been tampered with.&lt;/p&gt;
&lt;p&gt;When using a publicly known hash function for storing password hashes,
make sure to always use a salt or shared secret. Failure to do so will
make your storage scheme susceptible to a rainbow table attack. A
rainbow table allows a cracker to quickly match a list of hashes with a
table of previously computed hash values and correlated passwords.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is salt or a shared secret?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can use salt or a shared secret to add extra data to a message
before hashing with a publicly known algorithm. Below I will document
how to properly add salt to a message before generating a SHA256 hash.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
import hashlib as h
message = &amp;quot;This message and some salt will be hashed with SHA 256.&amp;quot;
salt = &amp;quot;This is some secret salt data&amp;quot;
print h.sha256(message+salt).hexdigest()
&lt;/pre&gt;
&lt;/p&gt;&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;em&gt;Hash Output:&lt;/em&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;pre class="literal-block"&gt;
5e8d86bab9604620f19cfbc5f836f47feb9e8c9e74264fff1f4938bdaab1eeaa
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;Adding a salt to the message allows us to use a publicly know algorithm
in a more protected manner.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can you spot the error in the python code below?&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="literal-block"&gt;
import hashlib as h
message = &amp;quot;This message and some salt will be hashed with SHA 256.&amp;quot;
salt = &amp;quot;This is some secret salt data&amp;quot;
print h.sha256(message).hexdigest()+salt
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;If you guessed that the message and salt BOTH need to be hashed together
then you are correct!&lt;/p&gt;
&lt;p&gt;The above code would have produced the following invalid hash:&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;em&gt;Hash Output:&lt;/em&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;pre class="literal-block"&gt;
79cd4bfa1bcb71a7a1b5bfd5e8cfc8368a6cc6cb836d24bf04f2ef2bd0e81261This is some secret salt data
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;&lt;strong&gt;You should follow me on twitter:&lt;/strong&gt; &lt;a class="reference external" href="https://twitter.com/russellbal"&gt;&amp;#64;russellbal&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="Security"/><category term="Python"/><category term="Salt"/></entry><entry><title>Symmetric Encryption vs Public Key Encryption</title><link href="https://russell.ballestrini.net/symmetric-encryption-vs-public-key-encryption/" rel="alternate"/><published>2012-02-13T17:25:00-05:00</published><updated>2012-02-13T17:25:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-02-13:/symmetric-encryption-vs-public-key-encryption/</id><summary type="html">&lt;p&gt;&lt;strong&gt;How many keys are involved for symmetric key encryption? How about
public key encryption?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Suppose you have N people who want to communicate with each other using
symmetric keys. All communication between any two people, i and j, is
visible to group N. Only person i and person j can …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;How many keys are involved for symmetric key encryption? How about
public key encryption?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Suppose you have N people who want to communicate with each other using
symmetric keys. All communication between any two people, i and j, is
visible to group N. Only person i and person j can decrypt each others
messages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How many keys would Symmetric Encryption require to protect group N?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I solved this with the following python function:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
def count_symmetric_keys( N=2 ):
    &amp;quot;&amp;quot;&amp;quot;Provide the number of entities in group N.
    return the number of symmetric keys needed for this group&amp;quot;&amp;quot;&amp;quot;
    keys = 0
    for i in range( 0, N ): keys += i
    return keys
&lt;/pre&gt;
&lt;p&gt;A reader suggested the following optimized formula:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
def calc_symmetric_keys( N=2 ):
    &amp;quot;&amp;quot;&amp;quot;Provide the number of entities in group N.
    return the number of symmetric keys needed for this group&amp;quot;&amp;quot;&amp;quot;
    return N*(N-1)/2
&lt;/pre&gt;
&lt;/p&gt;&lt;p&gt;If group N had 10 members, it would need to generate and maintain 45
Symmetric Keys.&lt;/p&gt;
&lt;p&gt;If group N had 50 members, it would need to generate and maintain 1225
Symmetric Keys.&lt;/p&gt;
&lt;p&gt;Symmetric keys are also susceptible to man-in-the-middle attacks. This
attack occurs when an entity poses as a trusted entity. Let i and j be
trusted entities. Let k be an untrusted attacker. If k determined the
Symmetric key it could send or receive messages posing as i or j.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How many keys would Public-key Encryption require to protect group
N?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Public Key Encryption requires 2n keys or two keys per person in group
N. Public key encryption also does not require 'pre sharing' the secret
key before communication may start. Each member would need 1 public key
and 1 private key.&lt;/p&gt;
&lt;p&gt;If group N had 10 members, it would need to generate and maintain 20
Public/Private Keys.&lt;/p&gt;
&lt;p&gt;If group N had 50 members, it would need to generate and maintain 100
Public/Private Keys.&lt;/p&gt;
</content><category term="misc"/><category term="Security"/><category term="Python"/></entry><entry><title>Attributes of an 8-block cipher</title><link href="https://russell.ballestrini.net/attributes-of-an-8-block-cipher/" rel="alternate"/><published>2012-02-13T00:50:00-05:00</published><updated>2012-02-13T00:50:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-02-13:/attributes-of-an-8-block-cipher/</id><summary type="html">&lt;p&gt;&lt;strong&gt;Consider an 8-block cipher and answer the following:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;How many possible input blocks does this cipher have?&lt;/p&gt;
&lt;p&gt;How many possible mappings are there?&lt;/p&gt;
&lt;p&gt;If we view each mapping as a key, then how many possible keys does this cipher have?&lt;/p&gt;
&lt;p&gt;To find the input blocks of this cipher we raise …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;Consider an 8-block cipher and answer the following:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;How many possible input blocks does this cipher have?&lt;/p&gt;
&lt;p&gt;How many possible mappings are there?&lt;/p&gt;
&lt;p&gt;If we view each mapping as a key, then how many possible keys does this cipher have?&lt;/p&gt;
&lt;p&gt;To find the input blocks of this cipher we raise 2 to the 8th power. 2^8 = 256 possible inputs.&lt;/p&gt;
&lt;p&gt;To find the number of possible mappings we take the 256 input blocks and
find it's factorial. There are 256! possible mappings.&lt;/p&gt;
&lt;p&gt;We can view each of these mappings as a key, so this cipher has 256! keys.&lt;/p&gt;
</content><category term="misc"/><category term="Security"/></entry><entry><title>Reasons why some Internet entities might want secure communication</title><link href="https://russell.ballestrini.net/reasons-why-some-internet-entities-might-want-secure-communication/" rel="alternate"/><published>2012-02-08T18:23:00-05:00</published><updated>2012-02-08T18:23:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-02-08:/reasons-why-some-internet-entities-might-want-secure-communication/</id><summary type="html"/><content type="html">&lt;p&gt;Internet entities often desire to communicate securely, some reasons include:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;&lt;strong&gt;Web Servers:&lt;/strong&gt; Communication on the Internet, or any network for that
matter, should be encrypted before transmitting sensitive data. This
will help prevent snooping from unauthorized parties. Most often SSL or
HTTPS may be used to create a secure communication “tunnel” between a
web server and a web client (browser).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Server Administration:&lt;/strong&gt;
Operators should always use a secure protocal when working on a server or remote computer.
SSH (Secure Shell) wins the popularity contest with server admins.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS Servers:&lt;/strong&gt;
Using DNSSEC could help prevent DNS poisoning and certifies DNS data.
DNS was first conceived as a distributed and highly scalable address lookup system.
Security was not its top priority.
Since then we have new DNSSEC extensions which allow for origin authentication of DNS data,
authenticated denial of existence, and data integrity.
DNSSEC does not attempt to solve availability and confidentiality.&lt;/li&gt;
&lt;/ol&gt;
</content><category term="misc"/><category term="Security"/></entry><entry><title>What are the differences between message confidentiality and message integrity</title><link href="https://russell.ballestrini.net/what-are-the-differences-between-message-confidentiality-and-message-integrity/" rel="alternate"/><published>2012-02-08T17:47:00-05:00</published><updated>2012-02-08T17:47:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-02-08:/what-are-the-differences-between-message-confidentiality-and-message-integrity/</id><summary type="html"/><content type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;What are the differences between message confidentiality and message
integrity? Can you have confidentiality without integrity? Can you have
integrity without confidentiality?&lt;/strong&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;message confidentiality&lt;/dt&gt;
&lt;dd&gt;Two or more hosts communicate securely, typically using encryption.
The communication cannot be monitored (sniffed) by untrusted hosts.
The communication between trusted parties is confidential.&lt;/dd&gt;
&lt;dt&gt;message integrity&lt;/dt&gt;
&lt;dd&gt;The message transported has not been tampered with or altered. A
message has integrity when the payload sent is the same as the
payload received.&lt;/dd&gt;
&lt;/dl&gt;
&lt;p&gt;Sending a message confidentially does not guarantee data integrity. Even
when two nodes have authenticated each other, the integrity of a message
could be compromised during the transmission of a message.&lt;/p&gt;
&lt;p&gt;Yes, you can have integrity of a message without confidentiality. One
can take a hash or sum of the message on both sides to compare. Often we
share downloadable files and provide data integrity using md5 hash sums.&lt;/p&gt;
</content><category term="misc"/><category term="Security"/></entry><entry><title>Today I lost a customer</title><link href="https://russell.ballestrini.net/today-i-lost-a-customer/" rel="alternate"/><published>2012-01-18T23:30:00-05:00</published><updated>2012-01-18T23:30:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2012-01-18:/today-i-lost-a-customer/</id><summary type="html">&lt;p&gt;Today I lost a customer.&lt;/p&gt;
&lt;p&gt;I added some new code to &lt;a class="reference external" href="https://linkpeek.com/website-thumbnail-generator"&gt;LinkPeek&lt;/a&gt; to accept coupons and I didn't think of an edge case.
This ended up creating an uncaught exception in my server side code which ultimatly served the newly subscribing customer an HTTP 500 error page.&lt;/p&gt;
&lt;p&gt;The damage was …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Today I lost a customer.&lt;/p&gt;
&lt;p&gt;I added some new code to &lt;a class="reference external" href="https://linkpeek.com/website-thumbnail-generator"&gt;LinkPeek&lt;/a&gt; to accept coupons and I didn't think of an edge case.
This ended up creating an uncaught exception in my server side code which ultimatly served the newly subscribing customer an HTTP 500 error page.&lt;/p&gt;
&lt;p&gt;The damage was done.&lt;/p&gt;
&lt;p&gt;This error was catastrophic and ultimately killed the conversion. Here
is an excerpt of how the user felt after the experience:&lt;/p&gt;
&lt;blockquote&gt;
At this point the website has failed spectacularly enough that I can
no longer trust you with my business. Please void the charge on my
American Express card before it's processed. I need to hear from you
ASAP.&lt;/blockquote&gt;
&lt;p&gt;Customers and prospects are forgiving for normal bugs in software.
However, customers are intolerant to bugs in the sign up or payment
flow. An error in payment flow will cause friction and friction will
kill the sale.&lt;/p&gt;
&lt;p&gt;Running a start up is hard work, and negative (but justified) feedback
hurts more then I thought it would. This email made me feel awful.&lt;/p&gt;
&lt;p&gt;Fortunately I learned from this mistake and the user's card was never
charged.&lt;/p&gt;
&lt;p&gt;Always make sure to extensively test payment and sign up code. Try all
the edge cases. Try to make your software break. Don't inflict friction
on your potential customers.&lt;/p&gt;
</content><category term="misc"/><category term="LinkPeek"/><category term="Opinion"/></entry><entry><title>LinkPeek.com, webpage to image, was a by-product</title><link href="https://russell.ballestrini.net/linkpeek-com-webpage-to-image-was-a-by-product/" rel="alternate"/><published>2011-12-19T00:23:00-05:00</published><updated>2011-12-19T00:23:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-12-19:/linkpeek-com-webpage-to-image-was-a-by-product/</id><summary type="html">&lt;p&gt;&lt;strong&gt;tldr;&lt;/strong&gt; When faced with pivoting or killing a project, take a good
look at all possible by-products. Don't miss the hidden gem in a
project's slag!&lt;/p&gt;
&lt;p&gt;Last year I built yoursitemakesmebarf.com, a novelty web application
which allowed anonymous link submission. The software would
automatically take
&lt;a class="reference external" href="https://russell.ballestrini.net/linkpeek-com-web-address-thumbnail-api-alpha-release/"&gt;screenshots&lt;/a&gt;
of submitted links …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;tldr;&lt;/strong&gt; When faced with pivoting or killing a project, take a good
look at all possible by-products. Don't miss the hidden gem in a
project's slag!&lt;/p&gt;
&lt;p&gt;Last year I built yoursitemakesmebarf.com, a novelty web application
which allowed anonymous link submission. The software would
automatically take
&lt;a class="reference external" href="https://russell.ballestrini.net/linkpeek-com-web-address-thumbnail-api-alpha-release/"&gt;screenshots&lt;/a&gt;
of submitted links and curate a blog. I enjoyed building the site and
the project served as my first Pyramid application.&lt;/p&gt;
&lt;p&gt;The project's original intent was to jokingly poke fun at ugly design.
The idea never caught on. Instead the application angered website owners
and attracted undesirable people. Eventually, I decided to take it down
and come up with less combative idea.&lt;/p&gt;
&lt;p&gt;After witnessing the Goog release of &amp;quot;instant previews&amp;quot;, I knew there
was a market for a fast and reliable web screen shot service.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://linkpeek.com"&gt;https://linkpeek.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So I decided to bring instant previews to anyone who needed them. I
wanted to build a fast, flexible, and easy to use screenshot API. After
a few months I had a working prototype. I named the product LinkPeek
because it described what the service was, and the domain was available.&lt;/p&gt;
&lt;p&gt;Next I built a website thumbnail generator to show off the software. The
generator application helped reinforce the simplicity of the underlying
LinkPeek API. It didn't require any downloading, installation, or
waiting.&lt;/p&gt;
&lt;p&gt;About a week later on a whim I posted the generator to hacker news with an honest title (&lt;a class="reference external" href="https://linkpeek.com/website-thumbnail-generator"&gt;Convert Any Webpage to an Image&lt;/a&gt;) and in about 30 minutes it reached the number 1 spot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Remember, when faced with pivoting or killing a project, take a good
look at all possible by-products. Don't miss the hidden gem in a
project's slag!&lt;/strong&gt;&lt;/p&gt;
</content><category term="misc"/><category term="LinkPeek"/><category term="Opinion"/><category term="Python"/></entry><entry><title>flash mob office meeting definition</title><link href="https://russell.ballestrini.net/flash-mob-office-meeting-definition/" rel="alternate"/><published>2011-12-12T15:34:00-05:00</published><updated>2011-12-12T15:34:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-12-12:/flash-mob-office-meeting-definition/</id><content type="html">&lt;p&gt;&lt;strong&gt;Flash Meeting&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In a office or cubicle environment a group of uninvited people gather
and hover around your desk to talk to you. A meeting forms in immaculate
conception as you sit bewildered at your desk.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Other names:&lt;/strong&gt; &lt;em&gt;Flash Meeting&lt;/em&gt;, &lt;em&gt;Flash Mob Meeting&lt;/em&gt;, &lt;em&gt;Flash Office
Meeting&lt;/em&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Uncategorized"/></entry><entry><title>LinkPeek.com Number One on Hacker News</title><link href="https://russell.ballestrini.net/linkpeek-com-number-one-on-hacker-news/" rel="alternate"/><published>2011-12-05T00:39:00-05:00</published><updated>2011-12-05T00:39:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-12-05:/linkpeek-com-number-one-on-hacker-news/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;strong&gt;We made the number one spot on Hacker News.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;LinkPeek was used to take a snapshot of the event:&lt;/div&gt;
&lt;div class="line"&gt;&lt;img alt="image0" src="/uploads/2011/12/linkpeek-number-1-on-hacker-news.png" /&gt;&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="LinkPeek"/><category term="Opinion"/></entry><entry><title>How to Incorporate Custom Configuration in a Pyramid Application</title><link href="https://russell.ballestrini.net/how-to-incorporate-custom-configuration-in-a-pyramid-application/" rel="alternate"/><published>2011-11-30T22:47:00-05:00</published><updated>2011-11-30T22:47:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-11-30:/how-to-incorporate-custom-configuration-in-a-pyramid-application/</id><summary type="html">&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This post shows an old way to modify the request object.
I discuss a new way in my &lt;a class="reference external" href="/register-super-powers-with-pyramid-add-request-method/"&gt;Pyramid add_request_method&lt;/a&gt; post.&lt;/p&gt;
&lt;p&gt;Imagine that you have just built a wiki, blog, or cms web application
that will be deployed multiple times by different people. You would like
to provide the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This post shows an old way to modify the request object.
I discuss a new way in my &lt;a class="reference external" href="/register-super-powers-with-pyramid-add-request-method/"&gt;Pyramid add_request_method&lt;/a&gt; post.&lt;/p&gt;
&lt;p&gt;Imagine that you have just built a wiki, blog, or cms web application
that will be deployed multiple times by different people. You would like
to provide the ability to configure some aspects of the program without
the end user altering your python or template code. This guide explains
how to incorporate custom configuration from the project's .ini file
with the rest of the application.&lt;/p&gt;
&lt;p&gt;To explain this process we will add a customizable Google Analytics key
to our project.&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Make a configuration key/value pair for Google Analytics&lt;/li&gt;
&lt;li&gt;Add the Google Analytics javascript code to the template&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Make a configuration key/value pair for Google Analytics&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Inside production.ini place the following in the &lt;tt class="docutils literal"&gt;[app:main]&lt;/tt&gt; section:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
#google_analytics_key = UA-55555555-1
google_analytics_key =
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Add the Google Analytics javascript code to the template&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In this case I will show an example in mako. Other template solutions
should look similar.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;lt;%def name=&amp;quot;google\_analytics()&amp;quot;&amp;gt;
% if request.registry.settings['google_analytics_key']:


 &amp;lt;script type=&amp;quot;text/javascript&amp;quot;&amp;gt;&amp;lt;/p&amp;gt;
 &amp;lt;p&amp;gt;var _gaq = _gaq || [];&amp;lt;br /&amp;gt;
           _gaq.push(['_setAccount', &amp;quot;${request.registry.settings['google_analytics_key']}&amp;quot;]);&amp;lt;br /&amp;gt;
           _gaq.push(['_trackPageview']);&amp;lt;/p&amp;gt;
 &amp;lt;p&amp;gt;(function() {&amp;lt;br /&amp;gt;
           var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;&amp;lt;br /&amp;gt;
           ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'https://www') + '.google-analytics.com/ga.js';&amp;lt;br /&amp;gt;
           var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);&amp;lt;br /&amp;gt;
           })();&amp;lt;/p&amp;gt;
 &amp;lt;p&amp;gt;&amp;lt;/script&amp;gt;

% endif
&lt;/pre&gt;
&lt;p&gt;Then in the head section call the function:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
${ google_analytics() }
&lt;/pre&gt;
&lt;p&gt;That was easy because the configuration string just needed to be
substituted into the JavaScript in the template. What if you needed to
do something with the provided key/value before using it? Next I will
show you a method for building renderer globals again showing a
different way to configure Google Analytics:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Make an inject_renderer_globals subscriber&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Define &lt;tt class="docutils literal"&gt;inject_renderer_globals(event)&lt;/tt&gt; function in the project's
&lt;tt class="docutils literal"&gt;__init__.py&lt;/tt&gt; file.&lt;/p&gt;
&lt;p&gt;I normally place it at the bottom of the file and it looks like this:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
def inject_renderer_globals(event):
    &amp;quot;&amp;quot;&amp;quot;Inject some renderer globals before passing to template&amp;quot;&amp;quot;&amp;quot;

    request = event['request']

    # Build ${google_analytics_key} from the configuration file
    event['google_analytics_key'] = request.registry.settings[ 'google_analytics_key' ]
&lt;/pre&gt;
&lt;p&gt;Import BeforeRender at the top of the &lt;tt class="docutils literal"&gt;__init__.py&lt;/tt&gt;:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
from pyramid.events import BeforeRender
&lt;/pre&gt;
&lt;p&gt;In the main function, add the &lt;tt class="docutils literal"&gt;inject_renderer_globals&lt;/tt&gt; to the
subscribers:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
config.add_subscriber(inject_renderer_globals, BeforeRender)
&lt;/pre&gt;
&lt;p&gt;Now you can use &lt;tt class="docutils literal"&gt;${google_analytics_key}&lt;/tt&gt; anywhere in your template.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Thank you for reading, and feel free to leave comments&lt;/strong&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="Guide"/><category term="Python"/></entry><entry><title>Career development is a game of chutes and ladders</title><link href="https://russell.ballestrini.net/career-development-is-a-game-of-chutes-and-ladders/" rel="alternate"/><published>2011-11-29T20:42:00-05:00</published><updated>2011-11-29T20:42:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-11-29:/career-development-is-a-game-of-chutes-and-ladders/</id><summary type="html">&lt;p class="first last"&gt;find out what is the ladder.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;If career development was a game of chutes and ladders, job networking
would be the ladder. Ladders provide a shortcut to the top, a direct route
to win, in this case you win your dream job.&lt;/p&gt;
&lt;img alt="chutes and ladders" class="align-right" src="/uploads/2011/11/job-networking-chutes-and-ladders.gif" /&gt;
&lt;p&gt;At work today, a colleague was reviewing resumes for an open requisition
within the Unix group. Later that night I decided to clean up &lt;a class="reference external" href="/uploads/russell.ballestrini.resume.pdf"&gt;my
resume&lt;/a&gt;
to make it more relevant.&lt;/p&gt;
&lt;p&gt;I felt like I did something positive for my career.
After coming down from the high of resume writing I began to question my rational and came to the contradictory conclusion that a great resume holds less importance than a mediocre recommendation.&lt;/p&gt;
&lt;p&gt;It is better for an employer to learn about you from a recommendation then your resume.&lt;/p&gt;
&lt;p&gt;Why?&lt;/p&gt;
&lt;p&gt;Hiring new people holds risk.
A hiring manager will reduce risk by promoting from within or using recommendations.
In both cases the resume becomes a document of formality instead of a document of credentials.&lt;/p&gt;
&lt;blockquote&gt;
&amp;quot;Its not what you know, its who you know.&amp;quot;&lt;/blockquote&gt;
&lt;p&gt;What can we assume about the position they are extending to the public?&lt;/p&gt;
&lt;p&gt;Can we assume that the manager has already promoted somebody internally for the high risk, enjoyable position?
Depending on the industry, they might be looking for bottom feeder (with a resume) to fill the newly vacant position.
Keep this in mind the next time a head hunter sends you a proposition.&lt;/p&gt;
&lt;p&gt;Taking shortcuts normally goes against the conventional wisdom for success.&lt;/p&gt;
&lt;p&gt;Job networking however, allows candidates to skip ahead and arrive at their dream occupation more directly.
Meeting new people seems like the single best way to land a job doing what you love.
Get out there and meet people with similar interests!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Did you like this post?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You should checkout the first chapter of my e-book, &lt;em&gt;How-to Work From Home&lt;/em&gt; titled &lt;a class="reference external" href="/how-to-work-from-home-the-road-to-remote-chapter-1/"&gt;The Road to Remote&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Greatest Hits"/><category term="Opinion"/></entry><entry><title>I'm petrified of launching my web application</title><link href="https://russell.ballestrini.net/im-petrified-of-launching-my-web-application/" rel="alternate"/><published>2011-11-03T21:35:00-04:00</published><updated>2011-11-03T21:35:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-11-03:/im-petrified-of-launching-my-web-application/</id><summary type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;I'm petrified of launching my web application because I'm fearful
that I won't ...&lt;/strong&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;acquire users&lt;/li&gt;
&lt;li&gt;support my users well&lt;/li&gt;
&lt;li&gt;scale in a timely manner&lt;/li&gt;
&lt;li&gt;react quickly to feedback&lt;/li&gt;
&lt;li&gt;monetize the application&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But most of all I'm scared that nobody will like me. I'm scared of
failure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Now that I got …&lt;/strong&gt;&lt;/p&gt;</summary><content type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;strong&gt;I'm petrified of launching my web application because I'm fearful
that I won't ...&lt;/strong&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;acquire users&lt;/li&gt;
&lt;li&gt;support my users well&lt;/li&gt;
&lt;li&gt;scale in a timely manner&lt;/li&gt;
&lt;li&gt;react quickly to feedback&lt;/li&gt;
&lt;li&gt;monetize the application&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But most of all I'm scared that nobody will like me. I'm scared of
failure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Now that I got that out of my system please check out https://linkpeek.com&lt;/strong&gt;&lt;/p&gt;
</content><category term="misc"/><category term="LinkPeek"/><category term="Opinion"/></entry><entry><title>Webmaster tools alerted issue turned out Pylon session files flooded inodes</title><link href="https://russell.ballestrini.net/webmaster-tools-alerted-issue-turned-out-pylon-session-files-flooded-inodes/" rel="alternate"/><published>2011-10-29T15:30:00-04:00</published><updated>2011-10-29T15:30:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-10-29:/webmaster-tools-alerted-issue-turned-out-pylon-session-files-flooded-inodes/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;strong&gt;This graph could happen to you if you ever forget to configure munin
email alerting:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="image0" src="/uploads/2011/10/df_inode-year.png" /&gt;&lt;/p&gt;
&lt;p&gt;It only took approximately 1 hour to diagnoses and resolve this issue
however most of my web applications hosted on this server were down for
about 11 hours. I was lucky that this outage fell on a weekend otherwise
I would not have known about the problem till around 6:30pm!&lt;/p&gt;
&lt;p&gt;&lt;img alt="image1" src="/uploads/2011/10/df_inode-day.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Diagnosis:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Two of my pylons apps had session files that slowly ran away on me. The
session files don't consume much capacity however the shear quantity of
them caused my inode usage to hit 100%.&lt;/p&gt;
&lt;p&gt;Had I properly configured Munin's email alerting this issue would have
been identified well before it was a problem.&lt;/p&gt;
&lt;p&gt;Want to know what alerted me to the problem? G Webmaster's tools claimed
it could not read my robots.txt on a few of my sites... After
investigating I learned the site was down. Checking the Apache error
logs pointed me to disk space issues. &lt;tt class="docutils literal"&gt;df &lt;span class="pre"&gt;-ha&lt;/span&gt;&lt;/tt&gt; reported everything was
fine, however &lt;tt class="docutils literal"&gt;df &lt;span class="pre"&gt;-hi&lt;/span&gt;&lt;/tt&gt; reported 100% inode usage! At this point I
started looking to cache and log locations to find lots of files, which
lead me to my pylons web applications data/sessions directories.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Resolution:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Delete the session cache tree directories and allow the applications to
rebuild them.&lt;/p&gt;
&lt;p&gt;Todo: move /www off the root disk partition. This issue could have been
much worse if I was unable to boot or login to remedy. Moving /www off
root should prevent the web server from effecting the systems ability to
boot.&lt;/p&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Opinion"/></entry><entry><title>Occupy Wall Street Stack vs Queue</title><link href="https://russell.ballestrini.net/occupy-wall-street-stack-vs-queue/" rel="alternate"/><published>2011-10-21T13:31:00-04:00</published><updated>2011-10-21T13:31:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-10-21:/occupy-wall-street-stack-vs-queue/</id><summary type="html">&lt;p&gt;&lt;strong&gt;Occupy Wall Street contributors claim to use a &amp;quot;stack&amp;quot; to determine
speaking arrangements.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I plan to explain how the term &amp;quot;stack&amp;quot; used in this scenario does not
align itself with the mathematical or computer science definition.&lt;/p&gt;
&lt;p&gt;The term stack means First In Last Out or &amp;quot;FILO&amp;quot;. For example: a person …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;Occupy Wall Street contributors claim to use a &amp;quot;stack&amp;quot; to determine
speaking arrangements.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I plan to explain how the term &amp;quot;stack&amp;quot; used in this scenario does not
align itself with the mathematical or computer science definition.&lt;/p&gt;
&lt;p&gt;The term stack means First In Last Out or &amp;quot;FILO&amp;quot;. For example: a person
placed on the stack in the morning would be the last to speak at the end
of the day. This isn't happening like that...&lt;/p&gt;
&lt;p&gt;Occupy Wall Street compatriots really use a technique called &amp;quot;queue&amp;quot;.
Mathematicians and Computer Scientists define a queue as First In First
Out or &amp;quot;FIFO&amp;quot;. A real world example of a queue would be a line at the
grocery store. The first person in line is the first person to leave the
line.&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;An object enters a stack from the rear and exits the rear.&lt;/li&gt;
&lt;li&gt;An object enters a queue from the rear and exits from the front.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;img alt="image0" src="/uploads/2011/10/stack-vs-queue.png" /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Greatest Hits"/><category term="Opinion"/></entry><entry><title>Adding inline image support to a gmail messages</title><link href="https://russell.ballestrini.net/adding-inline-image-support-to-a-gmail-messages/" rel="alternate"/><published>2011-10-20T17:46:00-04:00</published><updated>2011-10-20T17:46:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-10-20:/adding-inline-image-support-to-a-gmail-messages/</id><summary type="html">&lt;p&gt;Enable ability to insert images into a message body. You can upload and
insert image files in your computer, or insert images by URLs. This lab
will not work if you have offline enabled.&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Go to google labs: &lt;a class="reference external" href="https://mail.google.com/mail/#settings/labs"&gt;https://mail.google.com/mail/#settings/labs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Search &amp;quot;images&amp;quot;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Enable&lt;/em&gt; &amp;quot;Inserting images …&lt;/li&gt;&lt;/ol&gt;</summary><content type="html">&lt;p&gt;Enable ability to insert images into a message body. You can upload and
insert image files in your computer, or insert images by URLs. This lab
will not work if you have offline enabled.&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Go to google labs: &lt;a class="reference external" href="https://mail.google.com/mail/#settings/labs"&gt;https://mail.google.com/mail/#settings/labs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Search &amp;quot;images&amp;quot;.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Enable&lt;/em&gt; &amp;quot;Inserting images - by Kent T&amp;quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Welcome!&lt;/strong&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Guide"/></entry><entry><title>Cell shading practice, A sketched skull, and an old wind mill photograph</title><link href="https://russell.ballestrini.net/cell-shading-practice-a-sketched-skull-and-wind-mill-photograph/" rel="alternate"/><published>2011-10-16T12:56:00-04:00</published><updated>2011-10-16T12:56:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-10-16:/cell-shading-practice-a-sketched-skull-and-wind-mill-photograph/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;strong&gt;Some of my resent works using my Ubuntu + Wacom Bamboo + MyPaint +
Gimp Workflow!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;First some cell shading practice. Inside MyPaint I only used two colors
in my palate but to gain shadow and depth I used variations of opacity.
This technique produces a neat cartoon effect.&lt;/p&gt;
&lt;p&gt;&lt;img alt="image0" src="/uploads/2011/10/smile-guy.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Next I sketched a skull. I attempted to keep all my strokes in the same
direction. This piece is monotone. I used only one color and deviated
each layer with different opacity settings.&lt;/p&gt;
&lt;p&gt;&lt;img alt="image1" src="/uploads/2011/10/skull.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Last is a photograph I snapped during a family outing. I had to lower
the quality of this image to prepare for use on the web, but I still
adore the colors spectrum of this shot. The white birch tree frames the
right side while the grass and brush frame the bottom. The wind mill was
intentionally placed in the center with the apex extending upward. The
ivy creeps up the structure and the cloud filled sky appears to stretch
forever. The foliage emits a euphoric glow under the sun saturated rays.&lt;/p&gt;
&lt;p&gt;&lt;img alt="image2" src="/uploads/2011/10/wind.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Come back soon!&lt;/strong&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Art"/></entry><entry><title>I cancelled my xbox live automatic renewal</title><link href="https://russell.ballestrini.net/i-cancelled-my-xbox-live-automatic-renewal/" rel="alternate"/><published>2011-10-15T19:26:00-04:00</published><updated>2011-10-15T19:26:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-10-15:/i-cancelled-my-xbox-live-automatic-renewal/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;strong&gt;I cancelled my xbox live automatic renewal today because I no longer
use the service.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I find humor in Microsoft's list of reasons to keep Xbox live:&lt;/p&gt;
&lt;p&gt;&lt;img alt="image0" src="/uploads/2011/10/microsoft-resells-free-internet-services.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6 out of 8 services above are FREE Internet services!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If I want to use those free Internet services on my TV,
I'd rather use my tiny media center PC (&lt;a class="reference external" href="https://www.foxhop.net/nT330i"&gt;nT330i&lt;/a&gt;)
which is amazing and near silent.&lt;/p&gt;
&lt;p&gt;Microsoft prevents subscribers from cancelling their service over the
web, and thus they forced me to call their service center. After waiting
5 minutes I finally spoke to a sales representative who seemed friendly
and understanding.&lt;/p&gt;
&lt;p&gt;She asked me why I wanted to cancel automatic payments. I explained &amp;quot;I
want to pay month to month&amp;quot;. After listening to my position the sales
representative offered &lt;strong&gt;1 month of Xbox Live for $1.00.&lt;/strong&gt; So I'll have
Xbox live until December 2011 after all.&lt;/p&gt;
&lt;p&gt;Microsoft, give your users a web browser on the Xbox and stop trying to
sell subscriptions to Free Internet services.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;*PlayStation 3 launched with a web browser and a FREE network.*&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UPDATE:&lt;/strong&gt; If you opt into the $1 for one month deal, you are also
opting back into the automatic renewal program... Even if the person on
the phone tells you otherwise.&lt;/p&gt;
&lt;p&gt;Call us directly by dialing:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
Toll free:
(800) 4MY-XBOX or (800) 469-9269
Hearing impaired (TDD device):
(866) 740-9269 or (425) 635-7102
9 am to 1 am Eastern Time
6 am to 10 pm Pacific Time
&lt;/pre&gt;
</content><category term="misc"/><category term="Opinion"/></entry><entry><title>r8168 driver issues after Ubuntu 11.10 upgrade kernel linux 3.0</title><link href="https://russell.ballestrini.net/r8168-driver-issues-after-ubuntu-11-10-upgrade-kernel-linux-3-0/" rel="alternate"/><published>2011-10-15T11:15:00-04:00</published><updated>2011-10-15T11:15:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-10-15:/r8168-driver-issues-after-ubuntu-11-10-upgrade-kernel-linux-3-0/</id><summary type="html">&lt;p&gt;&lt;strong&gt;I had network issues after upgrading to Ubuntu 11.10 which has the
linux 3.0 kernel.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I used &lt;a class="reference external" href="https://www.foxhop.net/realtek-dropping-packets-on-linux-ubuntu-and-fedora"&gt;this guide&lt;/a&gt;
to compile the r8168 driver however, I needed to alter the Makefile to support for linux 3.0 kernel.&lt;/p&gt;
&lt;p&gt;Edit &lt;em&gt;src/Makefile&lt;/em&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;#KEXT  := $(shell echo $(KVER) | sed -ne &amp;#39;s …&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;I had network issues after upgrading to Ubuntu 11.10 which has the
linux 3.0 kernel.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I used &lt;a class="reference external" href="https://www.foxhop.net/realtek-dropping-packets-on-linux-ubuntu-and-fedora"&gt;this guide&lt;/a&gt;
to compile the r8168 driver however, I needed to alter the Makefile to support for linux 3.0 kernel.&lt;/p&gt;
&lt;p&gt;Edit &lt;em&gt;src/Makefile&lt;/em&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;#KEXT  := $(shell echo $(KVER) | sed -ne &amp;#39;s/^2\.[567]\..*/k/p&amp;#39;)o&lt;/span&gt;
KEXT&lt;span class="w"&gt; &lt;/span&gt;:&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;shell&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;KVER&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;-ne&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s/^[23]\.[1-9]\..*/k/p&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;o-
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Now you should follow the rest of the guide.&lt;/strong&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Guide"/></entry><entry><title>A better way to show website backlinks</title><link href="https://russell.ballestrini.net/a-better-way-to-show-website-backlinks/" rel="alternate"/><published>2011-10-12T20:05:00-04:00</published><updated>2011-10-12T20:05:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-10-12:/a-better-way-to-show-website-backlinks/</id><content type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Early web pilgrims of the Internet fashioned search queries like&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;tt class="docutils literal"&gt;link:russell.ballestrini.net&lt;/tt&gt; to gather backlinks for a domain.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;I however advocate a revolutionary search pattern like -&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;quot;russell.ballestrini.net&amp;quot; -site:russell.ballestrini.net
&lt;/pre&gt;
&lt;p&gt;to gather an improved representation of backlinks.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://www.google.com/#sclient=psy-ab&amp;amp;hl=en&amp;amp;source=hp&amp;amp;q=%22school.yohdah.com%22+-site:school.yohdah.com&amp;amp;pbx=1&amp;amp;oq=%22school.yohdah.com%22+-site:school.yohdah.com&amp;amp;aq=f&amp;amp;aqi=&amp;amp;aql=1&amp;amp;gs_sm=e&amp;amp;gs_upl=1836l15311l0l17007l43l35l0l0l0l8l274l5507l4.23.8l35l0&amp;amp;bav=on.2,or.r_gc.r_pw.r_cp.,cf.osb&amp;amp;fp=18aa9f7b4fee5b6d&amp;amp;biw=1485&amp;amp;bih=912"&gt;Click here to try
now!&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Guide"/><category term="Opinion"/></entry><entry><title>CSS frameworks not rendering properly on all browsers</title><link href="https://russell.ballestrini.net/css-frameworks-not-rendering-properly-on-all-browsers/" rel="alternate"/><published>2011-10-11T00:07:00-04:00</published><updated>2011-10-11T00:07:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-10-11:/css-frameworks-not-rendering-properly-on-all-browsers/</id><summary type="html">&lt;p class="first last"&gt;Read on for the one line fix.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;I ran into an issue when testing skeleton css framework and twitter
bootstrap where some browsers were not rendering the pages properly.
After some research I determined that I forgot the DOCTYPE declaration
tag.&lt;/p&gt;
&lt;p&gt;The DOCTYPE tag tells the browser what &amp;quot;rules&amp;quot; or standards to use when
rendering markup.&lt;/p&gt;
&lt;p&gt;The following code block displays the minimal approach to setting the
document type:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This doctype tag forces the broswer into standards mode which allows the pages to render properly.&lt;/p&gt;
&lt;p&gt;Thanks!&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="Guide"/></entry><entry><title>LinkPeek.com web address thumbnail api alpha release</title><link href="https://russell.ballestrini.net/linkpeek-com-web-address-thumbnail-api-alpha-release/" rel="alternate"/><published>2011-10-05T20:58:00-04:00</published><updated>2011-10-05T20:58:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-10-05:/linkpeek-com-web-address-thumbnail-api-alpha-release/</id><summary type="html">&lt;p class="first last"&gt;Convert any webpage to an image.&lt;/p&gt;
</summary><content type="html">&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a class="reference external" href="https://linkpeek.com"&gt;https://linkpeek.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The &lt;a class="reference external" href="https://linkpeek.com/"&gt;LinkPeek&lt;/a&gt; API had an alpha release!&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;LinkPeek is a website screenshot service. Convert any webpage to an
image. No software, no downloading and absolutely no waiting! Unlimited
free 140 pixel thumbnails!&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="LinkPeek"/></entry><entry><title>How do I calculate the M in my MVP?</title><link href="https://russell.ballestrini.net/how-do-i-calculate-the-m-in-my-mvp/" rel="alternate"/><published>2011-10-01T15:32:00-04:00</published><updated>2011-10-01T15:32:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-10-01:/how-do-i-calculate-the-m-in-my-mvp/</id><summary type="html">&lt;p class="first last"&gt;Is my idea too minimal?&lt;/p&gt;
</summary><content type="html">&lt;dl class="docutils"&gt;
&lt;dt&gt;MVP (Minimal Viable Product):&lt;/dt&gt;
&lt;dd&gt;The smallest version of your idea that produces value for customers.&lt;/dd&gt;
&lt;/dl&gt;
&lt;p&gt;&lt;a class="reference external" href="https://www.remarkbox.com"&gt;https://www.remarkbox.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Recently I have contemplated the idea of building a screen capture
service for websites. I anticipate the service would provide
functionality similar to Google's &amp;quot;Instant Preview&amp;quot;. I also pondered the
idea of archiving the screen captures and providing a history similar to
the internet Wayback Machine.&lt;/p&gt;
&lt;p&gt;After floundering on my first web application,
&lt;a class="reference external" href="https://four2go.gumyum.com"&gt;https://four2go.gumyum.com&lt;/a&gt;, I'm afraid to spend lots of time and energy
on another project if I cannot earn users or customers.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://four2go.gumyum.com"&gt;https://four2go.gumyum.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I feel the market segment for this type of service would include website
directories, live portfolio generation for website designers, and
websites in general for people who wish to keep track of the overall
look of their site over time.&lt;/p&gt;
&lt;p&gt;As an MVP I plan to build a free live web capture application to
basically provide a &amp;quot;Gravatar&amp;quot; for website addresses.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://linkpeek.com"&gt;https://linkpeek.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do you think anyone would use my MVP or service?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LinkPeek provides an easy to use API which enables anyone to instantly
create live web page thumbnails. We provide the API free of charge,
think of LinkPeek as Gravatar for web addresses.&lt;/p&gt;
&lt;p&gt;Have I missed any other market segments?&lt;/p&gt;
</content><category term="misc"/><category term="LinkPeek"/><category term="Opinion"/></entry><entry><title>New Baby Homecoming</title><link href="https://russell.ballestrini.net/new-baby-homecoming/" rel="alternate"/><published>2011-09-27T16:53:00-04:00</published><updated>2011-09-27T16:53:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-09-27:/new-baby-homecoming/</id><summary type="html">&lt;p class="first last"&gt;family += 1&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;img alt="image0" src="/uploads/2011/09/RJ-and-Carter.jpg" /&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Uncategorized"/></entry><entry><title>Hacker Olive Oil Lamp Crafted From Home Materials</title><link href="https://russell.ballestrini.net/hacker-olive-oil-lamp-crafted-from-home-materials/" rel="alternate"/><published>2011-09-09T19:01:00-04:00</published><updated>2011-09-09T19:01:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-09-09:/hacker-olive-oil-lamp-crafted-from-home-materials/</id><summary type="html">&lt;p class="first last"&gt;Really fast and clean design.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;img alt="image0" src="/uploads/2011/09/hacker-olive-oil-lamp.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;In loom of the recent and persisting hurricane season I present a
&lt;strong&gt;&amp;quot;Hacker's Olive Oil Lamp&amp;quot;!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;YES, this lamp is ...&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;Fun and simple to build&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fun and simple to use&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Easy to Cleanup&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gentle on environment&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Energy efficient&lt;/strong&gt; (1 cup ~= 16 hours)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Safe&lt;/strong&gt; (flame smothers if lamp tips over)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;NO&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;Odor&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smoke&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Heat near base&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost&lt;/strong&gt; (household materials)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Waste&lt;/strong&gt; (Store and seal oil in lamp with jar top)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Materials&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;1x Pickle jar with lid&lt;/li&gt;
&lt;li&gt;1x Washcloth 100% cotton&lt;/li&gt;
&lt;li&gt;1x Steel wire (salvaged from paint can handle)&lt;/li&gt;
&lt;li&gt;2x Cups of olive oil&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Tools&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Needle nose plyers&lt;/li&gt;
&lt;li&gt;Scissors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;
&lt;center&gt;
&lt;iframe width="420" height="345" src="https://www.youtube.com/embed/3I1W2ddJaAs" frameborder="0" allowfullscreen&gt;
&lt;/iframe&gt;
&lt;/center&gt;
&lt;/p&gt;&lt;p&gt;[gallery link=&amp;quot;file&amp;quot;]&lt;/p&gt;
</content><category term="misc"/><category term="Art"/><category term="Guide"/><category term="Project"/></entry><entry><title>A system administrators guide to installing and maintaining multiple python environments</title><link href="https://russell.ballestrini.net/a-system-administrators-guide-to-installing-and-maintaining-multiple-python-environments/" rel="alternate"/><published>2011-09-08T19:28:00-04:00</published><updated>2011-09-08T19:28:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-09-08:/a-system-administrators-guide-to-installing-and-maintaining-multiple-python-environments/</id><summary type="html">&lt;p&gt;Some operating systems depend on a specific version of python to
function properly. For example, Yum on Redhat Enterprise Linux 5 (RHEL5)
depends on python 2.4.3. This version of python lacks support from many
utilities and 3rd party libraries. This guide will cover installing an
alternative python instance …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Some operating systems depend on a specific version of python to
function properly. For example, Yum on Redhat Enterprise Linux 5 (RHEL5)
depends on python 2.4.3. This version of python lacks support from many
utilities and 3rd party libraries. This guide will cover installing an
alternative python instance while leaving the system's python alone.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This guide supports the following operating systems: Redhat, CentOS,
and Fedora. As of this publication the latest Python version was 2.7.8;
You might want to determine if a newer version exists.&lt;/em&gt;&lt;/p&gt;
&lt;div class="section" id="install-python-2-7"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Install Python 2.7&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;I found two methods which work for installing Python 2.7 side-by-side with the system's Python.&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;compile from source&lt;/li&gt;
&lt;li&gt;install package via the &lt;em&gt;scl&lt;/em&gt; repo&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Choose which ever strategy you find easier.&lt;/p&gt;
&lt;div class="section" id="compile-from-source"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Compile from source&lt;/a&gt;&lt;/h3&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;Gather the dependencies:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
yum install gcc zlib-devel python-setuptools readline-devel
&lt;/pre&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;strong&gt;gcc&lt;/strong&gt; is a compiler used to build python&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;zlib-devel&lt;/strong&gt; allows the python zlib module to be built&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;python-setuptools&lt;/strong&gt; provides the easy_install application&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;readline-devel&lt;/strong&gt; arrows readline and history handling in python shell&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Download and untar the python sourcecode:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
wget https://www.python.org/ftp/python/2.7.8/Python-2.7.8.tgz
tar -xzvf Python-2.7.8.tgz
cd Python-2.7.8
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Compile the sourcecode:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
./configure
make altinstall
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Test new alternative python:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
python2.7 --version
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Document path to new python2.7, you will need it if you plan to use a virtualenv:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
which python2.7
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Now we can install third party libraries into our alternative python:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
python2.7 -m easy_install
&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="section" id="install-package-from-scl"&gt;
&lt;h3&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Install package from scl&lt;/a&gt;&lt;/h3&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;install centos-release-scl repolists:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo yum install centos-release-scl
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;install python27:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo yum install python27
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;enable python27 via scl:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
scl enable python27 bash
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Document path to new python2.7, you will need it if you plan to use a virtualenv:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
which python2.7
&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="virtualenv"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;virtualenv&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Optionally we can create a virtualenv (for development) based on the
python 2.7 install. Virtual environments appear useful for testing
packages and libraries without installing them to the system owned
python site-packages directory.&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;Install pip using easy_install:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo easy_install pip
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Install virtualenv using pip:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo pip install virtualenv
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Create a new virtual python environment named virtpy:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
cd ~
virtualenv -p /usr/local/bin/python2.7 virtpy
&lt;/pre&gt;
&lt;p&gt;This will create a virtual python 2.7.2 environment named virtpy in your present working directory.&lt;/p&gt;
&lt;p&gt;To invoke this environment run source virtpy/bin/activate and your prompt should change to reflect the active virtualenv.&lt;/p&gt;
&lt;p&gt;Now you can run easy_install to install packages into virtpy/lib/python2.7/site-packages.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Thanks for reading, that's all for now.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#install-python-2-7" id="toc-entry-1"&gt;Install Python 2.7&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="reference internal" href="#compile-from-source" id="toc-entry-2"&gt;Compile from source&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#install-package-from-scl" id="toc-entry-3"&gt;Install package from scl&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#virtualenv" id="toc-entry-4"&gt;virtualenv&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="DevOps"/><category term="Guide"/><category term="Python"/></entry><entry><title>Deliberating the Viewers vs. Doers concept</title><link href="https://russell.ballestrini.net/deliberating-the-viewers-vs-doers-concept/" rel="alternate"/><published>2011-09-06T10:20:00-04:00</published><updated>2011-09-06T10:20:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-09-06:/deliberating-the-viewers-vs-doers-concept/</id><summary type="html">&lt;p class="first last"&gt;America has succumbed to &amp;quot;spectatoritis&amp;quot;.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;img alt="Viewers vs Doers Lecture" src="/uploads/2011/09/lecture-viewers-vs-doers.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://artofmanliness.com/2011/08/28/viewers-vs-doers-the-rise-of-spectatoritis/"&gt;Brett &amp;amp; Kate McKay&lt;/a&gt; from artofmanliness.com recently subscribed to an idea that America has succumbed to
&amp;quot;spectatoritis, a blanket description to cover all kinds of passive
amusement, an entering into the handiest activity merely to escape
boredom.&amp;quot; -Jay B. Nash, 1938&lt;/p&gt;
&lt;p&gt;The basic concept discussed how people can fall into one of two groups
when interacting in life, the viewers and the doers. Viewers or
spectators watch an activity while doers perform the said activity. For
example:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Viewers&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Sports Fans&lt;/li&gt;
&lt;li&gt;Audience&lt;/li&gt;
&lt;li&gt;Couch Potatoes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Doers&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Athletes&lt;/li&gt;
&lt;li&gt;Musicians&lt;/li&gt;
&lt;li&gt;Actors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Although the above patterns persists in the physical world, intellectual
situations operate differently. For instance on the surface reading
seems like a viewer or spectator task and writing appears to behave like
a doer task. But what happens when a reader learns or responds to a blog
post? We seem to need a stronger definition what constitutes doing a
task.&lt;/p&gt;
&lt;blockquote&gt;
We should not regard spectating as bad, however we should try to
stay lucid of our current rolls when engaging in activities.&lt;/blockquote&gt;
&lt;p&gt;Spectating has some important purposes in our society and culture.
Viewing a performance grants power to the actor, whether that person
plays football or plays guitar. For this power and influence to occur a
viewer must witness and pay attention to the event. What if the next
great political leader took the podium and no one bothered to view his
speech?&lt;/p&gt;
&lt;p&gt;The truth is our American society has conditioned us to spend much of
our lives spectating. During school a good student will view and listen
to the lecture. While driving to work one radio DJ broadcasts his
thoughts to many.&lt;/p&gt;
&lt;p&gt;Spectating also plays and integral roll in learning. Human infants learn
by first watching and then imitating. Experts also agree that
inspiration for a creative works often occur after observation of prior
productions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; We should not battle the viewers vs the doers because both
hold importance and need each other.&lt;/p&gt;
&lt;p&gt;If you liked this article, you should follow me &lt;a class="reference external" href="https://plus.google.com/101342467879466559261/posts"&gt;here&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Opinion"/></entry><entry><title>Python Image Grabber pig.py</title><link href="https://russell.ballestrini.net/python-image-grabber-pig-py/" rel="alternate"/><published>2011-08-22T18:48:00-04:00</published><updated>2011-08-22T18:48:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-08-22:/python-image-grabber-pig-py/</id><summary type="html">&lt;p class="first last"&gt;Download all the images from a URI with this simple tool!&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;img alt="pig.py" class="wordwrap-left" src="/uploads/2011/08/pig1-color1.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Python Image Grabber pig.py&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Python Image Grabber or pig.py is a &lt;em&gt;very&lt;/em&gt; simple python command line
tool to download all the images from a given uri.&lt;/p&gt;
&lt;p&gt;Enjoy!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Download and Installation:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://bitbucket.org/russellballestrini/pig/raw/tip/pig.py"&gt;https://bitbucket.org/russellballestrini/pig/raw/tip/pig.py&lt;/a&gt;&lt;/p&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;&lt;strong&gt;Usage:&lt;/strong&gt;&lt;/dt&gt;
&lt;dd&gt;&lt;tt class="docutils literal"&gt;python pig.py&lt;/tt&gt;&lt;/dd&gt;
&lt;/dl&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/dt&gt;
&lt;dd&gt;&lt;tt class="docutils literal"&gt;python pig.py &lt;span class="pre"&gt;https://www.foxhop.net&lt;/span&gt;&lt;/tt&gt;&lt;/dd&gt;
&lt;/dl&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;&lt;strong&gt;Open Source:&lt;/strong&gt;&lt;/dt&gt;
&lt;dd&gt;&lt;a class="reference external" href="https://bitbucket.org/russellballestrini/pig/raw/tip/pig.py"&gt;pig.py&lt;/a&gt; has been placed in the public domain and its sourcecode may be viewed or branched here here: &lt;a class="reference external" href="https://bitbucket.org/russellballestrini/pig"&gt;https://bitbucket.org/russellballestrini/pig&lt;/a&gt;&lt;/dd&gt;
&lt;/dl&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;dl class="docutils"&gt;
&lt;dt&gt;&lt;strong&gt;Pigpy Gimp Project&lt;/strong&gt;&lt;/dt&gt;
&lt;dd&gt;&lt;a class="reference external" href="/uploads/2011/08/pigpy.xcf"&gt;pigpy.xcf&lt;/a&gt;&lt;/dd&gt;
&lt;/dl&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="Guide"/><category term="Project"/><category term="Python"/></entry><entry><title>Security Professionals: Yes we appear vulnerable but that attack vector will never happen</title><link href="https://russell.ballestrini.net/security-professionals-yes-we-appear-vulnerable-but-that-attack-vector-will-never-happen/" rel="alternate"/><published>2011-08-18T21:36:00-04:00</published><updated>2011-08-18T21:36:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-08-18:/security-professionals-yes-we-appear-vulnerable-but-that-attack-vector-will-never-happen/</id><summary type="html">&lt;p&gt;In loom of recent internet attacks many institutions have started
scrambling in attempt to &amp;quot;strengthen&amp;quot; their security stance. I agree
that auditing our systems and networks for potential flaws seems
appropriate at this time to prevent getting &amp;quot;caught with our pants
down&amp;quot;. Incidentally, I have recently witnessed the introduction of …&lt;/p&gt;</summary><content type="html">&lt;p&gt;In loom of recent internet attacks many institutions have started
scrambling in attempt to &amp;quot;strengthen&amp;quot; their security stance. I agree
that auditing our systems and networks for potential flaws seems
appropriate at this time to prevent getting &amp;quot;caught with our pants
down&amp;quot;. Incidentally, I have recently witnessed the introduction of silly
and at times ineffective security adjustments. Many of these new
procedures, rules, and requirements do not make us more secure and worse
instill a false sense of security.&lt;/p&gt;
&lt;p&gt;I have previously addressed the fallacy of absolute security. No system
is perfect. A successful security model accomplishes fortitude by
implementing layers like an onion. Through the use of security
layers we can significantly hamper attack vectors and create a safer
complex.&lt;/p&gt;
&lt;p&gt;When analyzing a potential attack vector we must first determine our
current location in the security layers. This step serves two purposes:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;to prevent wasting time and energy on vulnerabilities that don't
matter at that point in our matrix.&lt;/li&gt;
&lt;li&gt;to prevent causing outages and unneeded administrator and customer
heartache.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a vulnerability requires root or elevated privileges to occur, don't
waste your time resolving it. If the attacker already has root, you have
bigger problems on your hands.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Some real life examples:&lt;/strong&gt;&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Firewall denying a large range of IP addresses (like entire
countries). This truly does not increase security, it just creates
headaches for users. An attacker could just proxy to an open range
(like a VPS based in a more trusted zone) and gain access from there.
Also if you decide to ignore this advice and create blanket IP range
deny rules, DON'T also block services intended to be internet-facing.
For example, don't block your Internet-facing DNS server if it is
authoritative for public domains. This will cause countless
intermittent issues and will be a nightmare to diagnose.&lt;/li&gt;
&lt;li&gt;Weekly Scanning for Windows viruses on network shares or data at
rest. This hammers the servers for no reason. If all the desktops run
antivirus then the file was already scanned when it was downloaded.
That same file will be scanned again when retrieved on the share. If
you want the warm and fuzzys of virus scanning network file shares,
do it once a year. These scans waste time and resources. I feel even
more outraged when asked to virus scan network shares hosted on UNIX
servers or NAS.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I speculate that most of these arbitrary ideas come about because the
people in charge make uninformed decisions out of fear without first
consulting the appropriate subject matter experts.&lt;/p&gt;
&lt;p&gt;Unfortunately, once a security mandate occurs it seems difficult to
expunge. People are just not willing to put their neck on the chopping
block to banish a legacy or silly mandates; So we end up living with
nonsensical rules and procedures.&lt;/p&gt;
&lt;p&gt;&lt;img alt="https://xkcd.com/936/" src="/uploads/2011/08/password-strength.png" /&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Greatest Hits"/><category term="Opinion"/><category term="Security"/></entry><entry><title>virt-back: restoring from backups</title><link href="https://russell.ballestrini.net/virt-back-restoring-from-backups/" rel="alternate"/><published>2011-08-10T23:20:00-04:00</published><updated>2011-08-10T23:20:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-08-10:/virt-back-restoring-from-backups/</id><summary type="html">&lt;p&gt;&lt;strong&gt;In a perfect world we should create backups but never need them.&lt;/strong&gt;
Although this statement holds truth, creating guest backups provides
many more benefits.&lt;/p&gt;
&lt;p&gt;The most common reasons system administrators restore from a virt-back
guest backup:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;recovering from data corruption&lt;/li&gt;
&lt;li&gt;recovering deleted files&lt;/li&gt;
&lt;li&gt;recovering from a virus infection&lt;/li&gt;
&lt;li&gt;recovering from …&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;In a perfect world we should create backups but never need them.&lt;/strong&gt;
Although this statement holds truth, creating guest backups provides
many more benefits.&lt;/p&gt;
&lt;p&gt;The most common reasons system administrators restore from a virt-back
guest backup:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;recovering from data corruption&lt;/li&gt;
&lt;li&gt;recovering deleted files&lt;/li&gt;
&lt;li&gt;recovering from a virus infection&lt;/li&gt;
&lt;li&gt;recovering from a compromised server&lt;/li&gt;
&lt;li&gt;backing out a failed change&lt;/li&gt;
&lt;li&gt;rolling back to a previous state&lt;/li&gt;
&lt;li&gt;testing disaster recovery plans&lt;/li&gt;
&lt;li&gt;cloning a server&lt;/li&gt;
&lt;li&gt;building test environments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;During this article we will cover how to restore a system from a
virt-back guest backup. This article will not cover how to restore a VM
host server.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Virt-back guest restore procedure&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In this guide our guest mbison has failed with a major corruption and we
would like to restore from our backups. We have our running production
guest images in /KVMROOT and our virt-back guest backups in /KVMBACK. We
will be restoring the backup on the same hypervisor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Overview:&lt;/strong&gt;&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Ensure the guest is shut off.&lt;/li&gt;
&lt;li&gt;move the bad image file out of the way&lt;/li&gt;
&lt;li&gt;untar the virt-back backup into place&lt;/li&gt;
&lt;li&gt;power up the guest&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Detailed Procedure:&lt;/strong&gt;&lt;/p&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;&lt;p class="first"&gt;Verify the guest is shut off by running:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
virt-back --info-all
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;We noticed that mbison was still running so we invoked:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
virt-back --shutdown mbison
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Move the corrupted image file out of the way:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
mv /KVMROOT/mbison.img /KVMROOT/mbison.img.NFG
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Unzip and unarchive the backup using the following command:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo tar -xzvf /KVMBACK/mbison.tar.gz -C /KVMROOT --strip 1
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;When the untar completes, start the guest:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
virt-back --create mbison
&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Connect to the guest over SSH and verify that all required services
and applications start. Determine if the restore was successful.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Restore guest backup on new hypervisor:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The details in this section were adapted from a tutorial given by
&lt;a class="reference external" href="http://fabianrodriguez.com/"&gt;Fabian Rodriguez&lt;/a&gt;.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Re-create any bridge network interfaces on new hypervisor
(/etc/network/interfaces for Debian)&lt;/li&gt;
&lt;li&gt;Adjust mbison.xml if needed (for example if you are changing paths)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="literal-block"&gt;
sudo mkdir /KVMROOT
sudo tar -xvzf mbison.tar.gz -C /KVMROOT --strip 1
virsh create /KVMROOT/mbison.xml
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; We use virsh create instead of virt-back create. While both
commands start guest DOMs, virsh create will also register the DOM into
the hypervisor.&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;&lt;br /&gt;&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Guide"/><category term="Project"/></entry><entry><title>Programming is like Alchemy</title><link href="https://russell.ballestrini.net/programming-is-like-alchemy/" rel="alternate"/><published>2011-08-08T21:52:00-04:00</published><updated>2011-08-08T21:52:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-08-08:/programming-is-like-alchemy/</id><summary type="html">&lt;p class="first last"&gt;except instead of exchanging matter, we programmers exchange time.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;img alt="This image comes from Michael Maier's 1618 treatise on alchemy. It combines music, image and text to communicate alchemical knowledge to adepts. Note the combination of Pythagorean imagery with alchemical practice." class="wordwrap-right" src="/uploads/2011/08/Alchemy_2.jpg" style="width: 270px;" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Programming is like Alchemy&lt;/strong&gt; except instead of exchanging matter, we
programmers exchange time. Also depending on the program the exchange of
time worked (coding) increases the productivity (time) of its users.&lt;/p&gt;
&lt;p&gt;On second thought, perhaps programmers correlate less with Alchemists
and more with Time Travelers; Or at the very least time manipulators.
For example, on a good day a programmer can easily complete a task that
would take one thousand men. See, time created! On a bad week we can
procrastinate and do nothing at all. Time lost!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Golem-soul-caliber" class="wordwrap-left" src="/uploads/2011/08/Golem-soul-caliber1.jpg" style="width: 220px;" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Programming embodies other magic like wizardry.&lt;/strong&gt; For example,
our programs typically live as golems performing one task,
repeatedly, over and over. Golem programs, without a soul, stuck in a
loop of servitude.&lt;/p&gt;
&lt;p&gt;Recently we have started coding creations with artificial intelligence.
These smart programs act like familiar spirits (Wikipedia) and assist
their creator in conjuring even more magic.&lt;/p&gt;
&lt;p&gt;So we settled it! Programmers are like bad ass, time travelling, wizard
alchemists!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Or maybe not ...&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It is more accurate to group programs with technology then magic, but
less fun. Programs are leveraged tools born to save people time and energy.&lt;/p&gt;
&lt;p&gt;Thanks for reading, you should follow me on twitter &lt;a class="reference external" href="https://twitter.com/russellbal"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="time traveling wizard alchemist" src="/uploads/2011/08/time-travelling-wizard-alchemists.jpg" /&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Greatest Hits"/><category term="Opinion"/></entry><entry><title>The Great Gist Heist</title><link href="https://russell.ballestrini.net/the-great-gist-heist/" rel="alternate"/><published>2011-08-07T02:30:00-04:00</published><updated>2011-08-07T02:30:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-08-07:/the-great-gist-heist/</id><summary type="html">&lt;p class="first last"&gt;Please listen to my story before jumping to conclusions.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;strong&gt;The Great Gist Heist&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I have crawled, downloaded, and archived all of gist.github.com. Please
hear my story before jumping to conclusions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I'm currently building software that requires a large corpus of source
code.&lt;/p&gt;
&lt;p&gt;I began to search for a collection of source code documents but my
pursuit appeared fruitless. Feeling displeased I attempted to gather all
of my own source code. My collection lacked fidelity perhaps because of
my revere for the python language.&lt;/p&gt;
&lt;p&gt;Regardless of the reasoning, I needed a higher quantity of samples. I
needed unbiased samples from all programming languages. I needed, most
importantly, samples in a variation of quality that only the most
popular paste sites have... sites like gist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why are you sharing it?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I feel a little bad about using Github's bandwidth.&lt;/p&gt;
&lt;p&gt;Sharing this collection should reduce the chances that others will crawl
for the same data. If you need a large collection of source code,
&lt;a class="reference external" href="/uploads/2011/08/the-great-gist-heist.torrent"&gt;download this torrent&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How did you do it?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I wrote a short, 30 line, python script. The script is part of the
torrent.&lt;/p&gt;
&lt;p&gt;At the peak of the scrape I had 14 threads running of the script, using
approximately 580Kbps (I used iftop).&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="Project"/><category term="Python"/></entry><entry><title>a hack to gain 80 percent efficiency when creating github projects</title><link href="https://russell.ballestrini.net/a-hack-to-gain-80-percent-efficiency-when-creating-github-projects/" rel="alternate"/><published>2011-07-11T21:47:00-04:00</published><updated>2011-07-11T21:47:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-07-11:/a-hack-to-gain-80-percent-efficiency-when-creating-github-projects/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;img alt="image0" src="/uploads/2011/07/80-percent-github-hack.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Last night I decided to learn how to use git for its popularity and
github to code more socially.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I have to admit early on that I enjoy hg and bitbucket so it came to a
surprise that github would have me jump through hoops to create a new
repository...&lt;/p&gt;
&lt;p&gt;Below I have copied the seemingly bloated github instructions for
creating a new project.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Github:&lt;/strong&gt;&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;create project/repo using the form&lt;/li&gt;
&lt;li&gt;mkdir scratch&lt;/li&gt;
&lt;li&gt;cd scratch&lt;/li&gt;
&lt;li&gt;git init&lt;/li&gt;
&lt;li&gt;touch README&lt;/li&gt;
&lt;li&gt;git add README&lt;/li&gt;
&lt;li&gt;git commit -m 'first commit'&lt;/li&gt;
&lt;li&gt;git remote add origin&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="mailto:git&amp;#64;github.com"&gt;git&amp;#64;github.com&lt;/a&gt;:russellballestrini/scratch.git&lt;/li&gt;
&lt;li&gt;git push -u origin master&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Wow...&lt;/p&gt;
&lt;p&gt;I like my method better.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;My Github clone hack:&lt;/strong&gt;&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;create project/repo using the form&lt;/li&gt;
&lt;li&gt;git clone
&lt;a class="reference external" href="https://russellballestrini&amp;#64;github.com/russellballestrini/scratch.git"&gt;https://russellballestrini&amp;#64;github.com/russellballestrini/scratch.git&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Awesome, 2 steps versus 10. That hack appears 80% more efficient! Now if
only my name was shorter... LOL&lt;/p&gt;
&lt;p&gt;You should follow me on twitter &lt;a class="reference external" href="https://twitter.com/russellbal"&gt;here&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="Guide"/></entry><entry><title>Add a Breadcrumb Subscriber to a Pyramid project using 4 simple steps</title><link href="https://russell.ballestrini.net/add-a-breadcrumb-subscriber-to-a-pyramid-project-using-4-simple-steps/" rel="alternate"/><published>2011-07-02T17:25:00-04:00</published><updated>2011-07-02T17:25:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-07-02:/add-a-breadcrumb-subscriber-to-a-pyramid-project-using-4-simple-steps/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This post shows an old way to modify the request object.
I discuss a new way in my &lt;a class="reference external" href="/register-super-powers-with-pyramid-add-request-method/"&gt;Pyramid add_request_method&lt;/a&gt; post.&lt;/p&gt;
&lt;p&gt;This article will explain how to add a breadcrumb subscriber to a
Pyramid project using 4 simple steps.&lt;/p&gt;
&lt;p&gt;While programming &lt;a class="reference external" href="https://school.yohdah.com/"&gt;https://school.yohdah.com/&lt;/a&gt; I needed the ability to easily create
breadcrumb links from the current url. You may view the &lt;a class="reference external" href="https://bitbucket.org/russellballestrini/bread/src/tip/bread.py"&gt;bread.py source
code
here&lt;/a&gt;.
The following guide describes the process I took to add this
functionality.&lt;/p&gt;
&lt;p&gt;1. Download and include
&lt;a class="reference external" href="https://bitbucket.org/russellballestrini/bread/raw/tip/bread.py"&gt;bread.py&lt;/a&gt;
at the top of your Pyramid project's __init__.py file:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
from yourproject.bread import Bread
from pyramid.events import subscriber, NewRequest
&lt;/pre&gt;
&lt;p&gt;2. At the bottom of the projects __init__.py file create the
following subscriber function as follows:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
def bread_subscriber( event ):
    &amp;quot;&amp;quot;&amp;quot; Build Bread object and add to request &amp;quot;&amp;quot;&amp;quot;
    event.request.bread = Bread( event.request.url )
&lt;/pre&gt;
&lt;p&gt;3. Now we can add our new subscriber to our Pyramid config inside
main and above the routes:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
config.add_subscriber(bread_subscriber, NewRequest)
&lt;/pre&gt;
&lt;p&gt;4. You are done! Now you should have the ability to use the bread
object from your template. Below I have provided a mako template
snippet:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
% for link in request.bread.links:
  ${ link | n } /
% endfor
&lt;/pre&gt;
</content><category term="misc"/><category term="Code"/><category term="Guide"/><category term="Python"/></entry><entry><title>Google Bot Attempts to Crawl Shortest Urls First</title><link href="https://russell.ballestrini.net/google-bot-attempts-to-crawl-shortest-urls-first/" rel="alternate"/><published>2011-06-25T09:37:00-04:00</published><updated>2011-06-25T09:37:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-06-25:/google-bot-attempts-to-crawl-shortest-urls-first/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;strong&gt;Recently I built https://school.yohdah.com a Python, Pyramid, and
mongoDB project during the last couple weekends.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="https://school.yohdah.com"&gt;&lt;img alt="school.yohdah.com" class="wordwrap-left" src="/uploads/2011/06/us-public-schools1.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The site features a directory style navigation of nearly every public
school in the US. We have 61 state pages, approximately 19,000 city
pages, and over 103,000 school pages.&lt;/p&gt;
&lt;p&gt;It seems the Google Bots have noticed
&lt;a class="reference external" href="https://school.yohdah.com"&gt;school.yohdah.com&lt;/a&gt; and started crawling
the site. Since the initial crawl I started reviewing a sample of the
sites apache logs in an attempt to track the bot's activity. After a few
minutes of viewing the logs, I locked onto a pattern; Google Bot's
algorithm appears to crawl the short URLs first!&lt;/p&gt;
&lt;p&gt;PersonalCompute (a user) attached a graph of the fetched URL lengths
here:&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2011/06/school.yohdah.com_.graph_.png"&gt;&lt;img alt="school.yohdah.com.graph" src="/uploads/2011/06/school.yohdah.com_.graph_.png" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I have attached a zip containing the apache google bot crawl logs
here:
&lt;a class="reference external" href="/uploads/2011/06/access-school.yohdah.log_.zip"&gt;access-school.yohdah.log.zip&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I found the pattern by opening the file in vim and scrolling very
quickly down. You will notice the log lines will grow slowly to the
right, as the urls being fetched increase by one character.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why does Google do this? Does anyone have speculation as to what this
means?&lt;/strong&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="Greatest Hits"/><category term="Opinion"/><category term="Project"/><category term="Python"/></entry><entry><title>ATT Uverse Residential Gateway Broadband LED flashes red intermittently</title><link href="https://russell.ballestrini.net/att-uverse-residential-gateway-broadband-led-flashes-red-intermittently/" rel="alternate"/><published>2011-06-18T10:08:00-04:00</published><updated>2011-06-18T10:08:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-06-18:/att-uverse-residential-gateway-broadband-led-flashes-red-intermittently/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;strong&gt;ATT Uverse Residential Gateway Broadband LED flashes red
intermittently&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I have struggled with my uverse Internet and TV service randomly and
intermittently shutting off for the last 3 months. When this occurs the
Residential Gateway's broadband LED flashes red.&lt;/p&gt;
&lt;p&gt;Also I get Uverse DSL link errors:&lt;/p&gt;
&lt;p&gt;
&lt;center&gt;&lt;p&gt;&lt;img alt="uverse link errors" src="/uploads/2011/06/uverse-dsl-link-errors.jpg" /&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;/p&gt;&lt;p&gt;When I 'live support chatted' with the uverse technical support they
claimed I had bridge tap on my lines, and that my lines had other
'issues'.&lt;/p&gt;
&lt;p&gt;They had to dispatch 3 technicians before one of them figured out the
problem. It turns out I had a faulty 'voice/data' splitter.&lt;/p&gt;
&lt;p&gt;I snapped a picture of the demarcation box before and after he replaced
the faulty splitter.&lt;/p&gt;
&lt;p&gt;
&lt;center&gt;&lt;p&gt;&lt;img alt="old uverse splitter" src="/uploads/2011/06/old-splitter.jpg" /&gt;&lt;img alt="new uverse splitter" src="/uploads/2011/06/new-splitter.jpg" /&gt;&lt;/p&gt;
&lt;/center&gt;
&lt;/p&gt;
&lt;p&gt;
&lt;center&gt;&lt;p&gt;&lt;em&gt;Old Uverse filter on the left, New filter on the right&lt;/em&gt;&lt;/p&gt;
&lt;center&gt;
&lt;/p&gt;&lt;p&gt;&lt;strong&gt;So far I have had no Uverse DSL Link Errors!&lt;/strong&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Opinion"/></entry><entry><title>Connecticut killed affliate marketing with Amazon.com</title><link href="https://russell.ballestrini.net/connecticut-killed-affliate-marketing-with-amazon-com/" rel="alternate"/><published>2011-06-11T14:10:00-04:00</published><updated>2011-06-11T14:10:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-06-11:/connecticut-killed-affliate-marketing-with-amazon-com/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;strong&gt;Connecticut killed affliate marketing with Amazon.com&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I just opened my amazon account and read this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Account Closed&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;This account is closed and will not generate referrals. Access to
this site is for historical purposes only.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/blockquote&gt;
&lt;p&gt;When I checked my email, I had this letter from Amazon.com:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hello,&lt;/p&gt;
&lt;p&gt;For well over a decade, the Amazon Associates Program has worked
with thousands of Connecticut residents. &lt;strong&gt;Unfortunately, the budget
signed by Governor Malloy contains a sales tax provision that
compels us to terminate this program for Connecticut-based
participants effective immediately.&lt;/strong&gt; It specifically imposes the
collection of taxes from consumers on sales by online retailers -
including but not limited to those referred by Connecticut-based
affiliates like you - even if those retailers have no physical
presence in the state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;We opposed this new tax law because it is unconstitutional and
counterproductive. It was supported by big-box retailers, most of
which are based outside Connecticut, that seek to harm the affiliate
advertising programs of their competitors.&lt;/strong&gt; Similar legislation in
other states has led to job and income losses, and little, if any,
new tax revenue. We deeply regret that we must take this action.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;As a result of the new law, contracts with all Connecticut
residents participating in the Amazon Associates Program will be
terminated today, June 10, 2011.&lt;/strong&gt; Those Connecticut residents will
no longer receive advertising fees for sales referred to Amazon.com
, Endless.com , MYHABIT.COM or SmallParts.com . Please be assured
that all qualifying advertising fees earned on or before today, June
10, 2011, will be processed and paid in full in accordance with the
regular payment schedule.&lt;/p&gt;
&lt;p&gt;You are receiving this email because our records indicate that you
are a resident of Connecticut. If you are not currently a resident
of Connecticut, or if you are relocating to another state in the
near future, you can manage the details of your Associates account
here . And if you relocate to another state after June 10, 2011,
please contact us for reinstatement into the Amazon Associates
Program.&lt;/p&gt;
&lt;p&gt;To avoid confusion, we would like to clarify that this development
will only impact our ability to offer the Associates Program to
Connecticut residents and will not affect their ability to purchase
from www.amazon.com .&lt;/p&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;We have enjoyed working with you and other Connecticut-based
participants in the Amazon Associates Program and, if this
situation is rectified, would very much welcome the opportunity to
re-open our Associates Program to Connecticut residents.&lt;/div&gt;
&lt;div class="line-block"&gt;
&lt;div class="line"&gt;Regards,&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The Amazon Associates Team&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Amazon.com does not have a facility in CT.&lt;/li&gt;
&lt;li&gt;CT passed a new bill.&lt;/li&gt;
&lt;li&gt;The bill declares: if a customer from any state clicks on a CT
resident's affiliate link and purchases an item, that
customer/Amazon.com should have to pay CT tax.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;My Opinion&lt;/strong&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;This seems bad.&lt;/li&gt;
&lt;li&gt;I have less money to spend in Connecticut.&lt;/li&gt;
&lt;li&gt;This smells like double taxation.&lt;/li&gt;
&lt;li&gt;One more reason to move out of Connecticut when looking for Tech or
IT jobs.&lt;/li&gt;
&lt;/ul&gt;
</content><category term="misc"/><category term="Opinion"/></entry><entry><title>Dropbox Encryption with TrueCrypt</title><link href="https://russell.ballestrini.net/dropbox-encryption-with-truecrypt/" rel="alternate"/><published>2011-04-16T12:50:00-04:00</published><updated>2011-04-16T12:50:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-04-16:/dropbox-encryption-with-truecrypt/</id><summary type="html">&lt;p class="first last"&gt;The best security acts like an onion.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;a class="reference external" href="https://dereknewton.com/2011/04/dropbox-authentication-static-host-ids/"&gt;Derek Newton&lt;/a&gt;
recently invoked discussion about insecurities in Dropbox authentication.
In his article he describes how an attacker could exploit Dropbox and gain access to unshared files.
The concerns he raised do appear accurate however we must remember that security is an onion.&lt;/p&gt;
&lt;p&gt;&lt;img alt="onion" src="/uploads/2011/04/onion.png" /&gt;&lt;/p&gt;
&lt;p&gt;An onion, like security, has layers to protect its vital parts.
The vital parts are more vulnerable when its security model only possess one layer.
As we add layers to our security model, our system's protection grows exponentially.&lt;/p&gt;
&lt;p&gt;In the case of Dropbox, the username and password act as the first
layer. Experts agree that a simple authentication layer will provide
enough protection for nonsensitive data. However when attempting to
protect sensitive data we must pair authorization with encryption.&lt;/p&gt;
&lt;p&gt;Generally speaking file systems have maintained a sense of insecurity,
which makes them useful. Not encrypting files on Dropbox is akin to not
encrypting files on a shared PC. Sensitive data should always be
encrypted &lt;em&gt;regardless&lt;/em&gt; of its location or media. We should treat
sensitive data-at-rest on Dropbox the same way we treat sensitive data
on local, optical or flash disk. &lt;em&gt;We should encrypt it!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;So how does a user encrypt their Dropbox?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;My strongly opinionated solution uses TrueCrypt to create an encrypted
volume in the Dropbox directory. Simply treat the Dropbox like a normal
directory, follow the TrueCrypt documentation to build a volume, and
give Dropbox a chance to sync the data. When the sync completes, the
TrueCrypt volume will be mountable on each of your Dropbox enabled
computers.&lt;/p&gt;
&lt;p&gt;I have to admit at first I was skeptical, but the software cooperates
surprisingly well and after the initial sync proceeding syncs occur
quickly! I prefer TrueCrypt because it is open source, cross platform,
and free (both in freedom and cost). TrueCrypt also functions and
performs better then any other solution including commercial products
like GuardianEdge or PGP both recently acquired by Symantec.&lt;/p&gt;
&lt;p&gt;All security and encryption software should remain open sourced and peer
reviewed to prevent harmful tampering. Commercial software, written in a
black-box vacuum, prevents customers from viewing its code and
procedures. We cannot trust software for security when we cannot view
its source code.&lt;/p&gt;
&lt;p&gt;You should follow me on twitter
&lt;a class="reference external" href="https://twitter.com/russellbal"&gt;here&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Greatest Hits"/><category term="Opinion"/><category term="Security"/></entry><entry><title>Voice Over IP with TeamSpeak</title><link href="https://russell.ballestrini.net/voice-over-ip-with-teamspeak/" rel="alternate"/><published>2011-04-03T12:11:00-04:00</published><updated>2011-04-03T12:11:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-04-03:/voice-over-ip-with-teamspeak/</id><summary type="html">&lt;p&gt;&lt;strong&gt;This article will cover running a Voice Over IP service like TeamSpeak
on a VPS.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Voice Over IP allows users to communicate using audio over the Internet.&lt;/p&gt;
&lt;p&gt;When planning for this article I originally was going to cover ventrilo,
but their download link was obfuscated behind a heinous php session …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;This article will cover running a Voice Over IP service like TeamSpeak
on a VPS.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Voice Over IP allows users to communicate using audio over the Internet.&lt;/p&gt;
&lt;p&gt;When planning for this article I originally was going to cover ventrilo,
but their download link was obfuscated behind a heinous php session
script. Ventrilo also does not have a Linux client although they have
been promising one for quite some time.&lt;/p&gt;
&lt;p&gt;Instead we will cover how to install and configure
&lt;a class="reference external" href="https://www.teamspeak.com/en/"&gt;TeamSpeak&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Installing a teamspeak server on your VPS&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Create a user to run teamspeak.&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo adduser teamspeak
&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;follow the prompts&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Personally I don't want the teamspeak user to have ssh access so I added
the following to /etc/ssh/sshd_config:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
DenyUsers teamspeak
&lt;/pre&gt;
&lt;p&gt;Then reload ssh server config:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo service ssh reload
&lt;/pre&gt;
&lt;p&gt;Setup the installation environment.&lt;/p&gt;
&lt;p&gt;The following 4 commands will create a directory to hold your
installation, change the ownership of the directory to the teamspeak
user, change the working directory to the new folder and then become the
teamspeak user:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo mkdir /opt/teamspeak
sudo chown teamspeak:teamspeak /opt/teamspeak
cd /opt/teamspeak
sudo su teamspeak
&lt;/pre&gt;
&lt;p&gt;Download and extract TeamSpeak server software.&lt;/p&gt;
&lt;p&gt;Find the proper package for your VPS and download it, in my case I ran:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
wget https://teamspeak.gameserver.gamed.de/ts3/releases/beta-30/teamspeak3-server_linux-amd64-3.0.0-beta30.tar.gz
&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;For best results download the latest version of teamspeak.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A teamspeak tarball should now exist in your present working directory.
We can extract the files from the tarball by issuing the following
command:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
tar xvf teamspeak3-server_linux-amd64-3.0.0-beta30.tar.gz --strip-components=1
&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;No you don't have to type that file name out! The bash shell has tab
completion, type 'tar xvf teamsp' and then press tab. : )&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Install the TeamSpeak server software.&lt;/p&gt;
&lt;p&gt;I had success running:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
./ts3server_startscript.sh start
&lt;/pre&gt;
&lt;p&gt;Write down the auto generated serveradmin password.&lt;/p&gt;
&lt;p&gt;Configure TeamSpeak to start at system bootup.&lt;/p&gt;
&lt;p&gt;Create a cronjob under the teamspeak user:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
crontab -e
&lt;/pre&gt;
&lt;p&gt;Place the following into teamspeak's crontab:&lt;/p&gt;
&lt;pre class="literal-block"&gt;
&amp;#64;reboot /opt/teamspeak/ts3server_startscript.sh start
&lt;/pre&gt;
</content><category term="misc"/><category term="Guide"/></entry><entry><title>Porting the ChaosTheory Wordpress theme to Pylowiki</title><link href="https://russell.ballestrini.net/porting-the-chaostheory-wordpress-theme-to-pylowiki/" rel="alternate"/><published>2011-03-26T22:00:00-04:00</published><updated>2011-03-26T22:00:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-03-26:/porting-the-chaostheory-wordpress-theme-to-pylowiki/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;strong&gt;As you may have noticed, I recently switched this blog over to the
ChaosTheory Wordpress theme.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I had to tweak some of my pre-existing articles to make them look
'right' but upon completion I was very pleased with the aesthetics of
each page.&lt;/p&gt;
&lt;p&gt;Later on I reviewed the theme of foxhop.net, my pylowiki demo site.
Pylowiki felt bland in comparison to this wordpress blog.&lt;/p&gt;
&lt;p&gt;So, I made up my mind to build themes for Pylowik, and I started by
porting ChaosTheory. You can view the finished product at
&lt;a class="reference external" href="https://www.foxhop.net"&gt;https://www.foxhop.net&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You should also give Pylowiki a try. Pylowiki has futuristic, live
preview, same page, section edit functionality.&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="Project"/></entry><entry><title>virt-back: a python libvirt backup utility for kvm xen virtualbox</title><link href="https://russell.ballestrini.net/virt-back-a-python-libvirt-backup-utility-for-kvm-xen-virtualbox/" rel="alternate"/><published>2011-03-20T11:32:00-04:00</published><updated>2011-03-20T11:32:00-04:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-03-20:/virt-back-a-python-libvirt-backup-utility-for-kvm-xen-virtualbox/</id><summary type="html">&lt;p class="first last"&gt;backup your virtual maching guests.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;img alt="image0" src="/uploads/2011/03/virt-back.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Over the weekend I wrote virt-back, a backup utility for QEMU, KVM,
XEN, or Virtualbox guests.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;virt-back is a python application that uses the libvirt API to safely
shutdown, gzip, and restart guests.&lt;/p&gt;
&lt;p&gt;The backup process logs to syslog for auditing and virt-back works great
with cron for scheduling outages. Virt-back is in active development so
feel free to give suggestions or branch the source.&lt;/p&gt;
&lt;p&gt;virt-back has been placed in the public domain and the latest version
may be downloaded here:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference external" href="https://git.unturf.com/python/virt-back"&gt;https://git.unturf.com/python/virt-back&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference external" href="https://pypi.org/project/virt-back/"&gt;https://pypi.org/project/virt-back/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="section" id="installation"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-1"&gt;Installation&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The fastest way to install virt-back is to use pip or setuptools.&lt;/p&gt;
&lt;p&gt;Try &lt;cite&gt;sudo pip install virt-back&lt;/cite&gt; or &lt;cite&gt;sudo easy_install virt-back&lt;/cite&gt;&lt;/p&gt;
&lt;p&gt;Otherwise you may manually install virt-back&lt;/p&gt;
&lt;pre class="literal-block"&gt;
sudo wget https://git.unturf.com/python/virt-back/raw/branch/master/virt-back \
-O  /usr/local/bin/virt-back
&lt;/pre&gt;
&lt;pre class="literal-block"&gt;
sudo chmod 755 /usr/local/bin/virt-back
&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="section" id="test-installation"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-2"&gt;Test installation&lt;/a&gt;&lt;/h2&gt;
&lt;pre class="literal-block"&gt;
virt-back --help
&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="section" id="example-cronjob"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-3"&gt;Example cronjob&lt;/a&gt;&lt;/h2&gt;
&lt;pre class="literal-block"&gt;
15  2  *  *  1  /usr/local/bin/virt-back --quiet --backup sagat
15  23 *  *  5  /usr/local/bin/virt-back --quiet --backup mbison
&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="section" id="manual"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-4"&gt;Manual&lt;/a&gt;&lt;/h2&gt;
&lt;pre class="literal-block"&gt;
russell&amp;#64;host:~$ virt-back --help
Usage: virt-back [options]
Options:
  -h, --help            show this help message and exit
  -q, --quiet           prevent output to stdout
  -d, --date            append date to tar filename [default: no date]
  -g, --no-gzip         do not gzip or tar the resulting files
  -a amount, --retention=amount
                        backups to retain [default: 3]
  -p 'PATH', --path='PATH'
                        backup path [default: '/KVMBACK']
  -u 'URI', --uri='URI'
                        optional hypervisor uri

  Actions for info testing:
    These options display info or test a list of guests.

    -i, --info          info/test a list of guests (space delimited dom names)
    --info-all          attempt to show info on ALL guests

  Actions for a list of dom names:
    WARNING:  These options WILL bring down guests!

    -b, --backup        backup a list of guests (space delimited dom names)
    -r, --reboot        reboot a list of guests (space delimited dom names)
    -s, --shutdown      shutdown a list of guests (space delimited dom names)
    -c, --create        start a list of guests (space delimited dom names)

  Actions for all doms:
    WARNING:  These options WILL bring down ALL guests!

    --backup-all        attempt to shutdown, backup, and start ALL guests
    --reboot-all        attempt to shutdown and then start ALL guests
    --shutdown-all      attempt to shutdown ALL guests
    --create-all        attempt to start ALL guests
&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="section" id="restoring"&gt;
&lt;h2&gt;&lt;a class="toc-backref" href="#toc-entry-5"&gt;Restoring&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="reference external" href="/virt-back-restoring-from-backups/"&gt;virt-back: restoring from backups&lt;/a&gt;&lt;/p&gt;
&lt;div class="contents topic" id="contents"&gt;
&lt;p class="topic-title"&gt;&lt;a class="reference internal" href="#top"&gt;Contents&lt;/a&gt;&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;&lt;a class="reference internal" href="#installation" id="toc-entry-1"&gt;Installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#test-installation" id="toc-entry-2"&gt;Test installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#example-cronjob" id="toc-entry-3"&gt;Example cronjob&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#manual" id="toc-entry-4"&gt;Manual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="reference internal" href="#restoring" id="toc-entry-5"&gt;Restoring&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
</content><category term="misc"/><category term="Code"/><category term="DevOps"/><category term="Greatest Hits"/><category term="Guide"/><category term="Project"/><category term="Python"/></entry><entry><title>Response to L-Theanine: a 4000 Year Old Mind-Hack</title><link href="https://russell.ballestrini.net/response-to-l-theanine-a-4000-year-old-mind-hack/" rel="alternate"/><published>2011-01-11T17:06:00-05:00</published><updated>2011-01-11T17:06:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-01-11:/response-to-l-theanine-a-4000-year-old-mind-hack/</id><summary type="html">&lt;p class="first last"&gt;Solve problems and write code in your sleep.&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;strong&gt;RE: https://worldoftea.org/caffeine-and-l-theanine&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2011/01/dream.jpg"&gt;&lt;img alt="dream-solving" class="wordwrap-left" src="/uploads/2011/01/dream.jpg" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;As the original article speculates, the combination and amount of
L-Theanine and caffeine present in tea, appears to have notable affects
on my programming and problem solving abilities. It's difficult to show
conclusive evidence to this claim, but I generally feel most alert and
organized when coding under the influence of 2 - 3 cups. In a typical
day I drink about 4 cups, dosed an hour apart. I feel compelled to
introduce another &amp;quot;mind hack&amp;quot; for problem solving or programming, which
I call &amp;quot;Dream coding&amp;quot;&lt;/p&gt;
&lt;p&gt;&amp;quot;Dream coding&amp;quot; is just that, coding at night during sleep. One can
successfully invoke this &amp;quot;mind hack&amp;quot; by provoking thought about the
problem while drifting to sleep. Once sleeping I'm able to see my code
and my brain seems to iteratively problem solve. Most often I'm lucid
during these events; aware of the problem I'm trying to solve and of the
possible solutions. Other times I don't recall performing the solutions,
but when I wake and after my first cup of tea, I'm able to solve my
problem with a optimal and beautiful solution.&lt;/p&gt;
&lt;p&gt;PBS NOVA published an episode about this phenomenon: &lt;a class="reference external" href="https://www.pbs.org/wgbh/nova/dreams/"&gt;What are
dreams?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Does this happen to you? Have you experienced this?&lt;/p&gt;
&lt;p&gt;You should follow me on twitter &lt;a class="reference external" href="https://twitter.com/russellbal"&gt;here&lt;/a&gt;.&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="Greatest Hits"/><category term="Opinion"/></entry><entry><title>I just used Google to purchase Chinese food takeout for two</title><link href="https://russell.ballestrini.net/i-just-used-google-to-purchase-chinese-food-takeout-for-two/" rel="alternate"/><published>2011-01-10T20:03:00-05:00</published><updated>2011-01-10T20:03:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-01-10:/i-just-used-google-to-purchase-chinese-food-takeout-for-two/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;img alt="china-food" src="/uploads/2011/01/food1.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Yes, it has been done. Using my Google Chromium browser I searched
Google Maps for the closest local Chinese restaurant. The small town
venue doesn't have a website or menu posted online.&lt;/p&gt;
&lt;p&gt;So I used Google Web to search for the restaurant name. I found
takeouttonight.com (no longer works) which had the entire menu and VALID prices
for the restaurant hosted online! I choose 'Sweet and Sour chicken', my
wife selected Chicken and Broccoli.&lt;/p&gt;
&lt;p&gt;I then logged into my Google Gmail account and clicked Call phone. Using
the mouse I dialed the number I previously researched and after 3 rings
an Asian speaking fellow answered the phone.&lt;/p&gt;
&lt;p&gt;I said, &amp;quot;Hello, I'd like to place a delivery order.&amp;quot; His reply, &amp;quot;Ok.&amp;quot;
resonated out of my 5.1 speaker setup.&lt;/p&gt;
&lt;p&gt;I read off our selections and was super excited that it was WORKING! In
my titillated state, I ended up ordering an extra side of egg rolls just
to keep the conversation going.&lt;/p&gt;
&lt;p&gt;He asked for my phone number to complete the order. I obliged by reading
my Google phone number.&lt;/p&gt;
&lt;p&gt;Google might be spamy these days, but it's still useful.&lt;/p&gt;
&lt;p&gt;Who would have thought a lazy hacker could combine six different Google
services to order Chinese food without leaving the desk?&lt;/p&gt;
&lt;p&gt;I know it's possible now, I've seen me do it.&lt;/p&gt;
&lt;p&gt;You should follow me on twitter
&lt;a class="reference external" href="https://twitter.com/russellbal"&gt;here&lt;/a&gt;.&lt;/p&gt;
</content><category term="misc"/><category term="Guide"/><category term="Opinion"/></entry><entry><title>Graphic Design Powerhouse: Wacom Bamboo, Ubuntu, and MyPaint</title><link href="https://russell.ballestrini.net/graphic-design-powerhouse-wacom-bamboo-ubuntu-and-mypaint/" rel="alternate"/><published>2011-01-09T00:14:00-05:00</published><updated>2011-01-09T00:14:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-01-09:/graphic-design-powerhouse-wacom-bamboo-ubuntu-and-mypaint/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2011/01/pot.png"&gt;&lt;img alt="pot" class="wordwrap-left" src="/uploads/2011/01/pot.png" style="width: 150px;" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For Christmas this year my wife gifted me a Wacom Bamboo pen tablet.
She claimed to have researched many products and looked
for a model that worked well with Ubuntu and had positive user reviews.
She ultimately chose the Wacom Bamboo model and I was so excited when I
unwrapped it!&lt;/p&gt;
&lt;p&gt;The driver install was flawless and I was up and running after about 5
mins and the first application I attempted to use was Gimp. Gimp worked
but the flow felt &amp;quot;clunky&amp;quot;.&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external image-reference" href="/uploads/2011/01/treeman1.png"&gt;&lt;img alt="treeman" class="wordwrap-right" src="/uploads/2011/01/treeman1.png" style="width: 300px;" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I wanted an experience that better emulated
a pencil or brush and canvas. I went to bed that night feeling a little
unsatisfied.&lt;/p&gt;
&lt;p&gt;The next day I decided to give the tablet another try, but instead of
using Gimp I decided to try my luck with other software called
&lt;a class="reference external" href="http://mypaint.org/about/"&gt;MyPaint&lt;/a&gt;. MyPaint's interface feels
perfect and it is loaded with brushes, pencils, and inks. MyPaint
implements brush stroke pressure with unmatched precision!&lt;/p&gt;
&lt;p&gt;The art on this page was created with my wacom BAMBOO tablet and
MyPaint.&lt;/p&gt;
&lt;p&gt;You should follow me on twitter &lt;a class="reference external" href="https://twitter.com/russellbal"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Thank you so much MyPaint developers for such a stellar piece of
software!&lt;/p&gt;
&lt;blockquote&gt;
&lt;strong&gt;MyPaint is a fast and easy open-source graphics application for
digital painters. It lets you focus on the art instead of the
program. You work on your canvas with minimum distractions, bringing
up the interface only when you need it.&lt;/strong&gt;&lt;/blockquote&gt;
</content><category term="misc"/><category term="Art"/></entry><entry><title>How did Stack Overflow get initial traction?</title><link href="https://russell.ballestrini.net/how-did-stack-overflow-get-initial-traction/" rel="alternate"/><published>2011-01-05T02:57:00-05:00</published><updated>2011-01-05T02:57:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-01-05:/how-did-stack-overflow-get-initial-traction/</id><summary type="html">&lt;p&gt;&lt;strong&gt;Stack Overflow was a progressive and natural evolution of the standard
clunky forum.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Using ajax it created a more fun and clean user experience.&lt;/p&gt;
&lt;p&gt;Using badges and karma to gain responsibility allow forums post to
become a game. People naturally like to see progression and growth,
being able to watch …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;Stack Overflow was a progressive and natural evolution of the standard
clunky forum.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Using ajax it created a more fun and clean user experience.&lt;/p&gt;
&lt;p&gt;Using badges and karma to gain responsibility allow forums post to
become a game. People naturally like to see progression and growth,
being able to watch my karma go up was a rush, similar to a video game.
(some people plan elaborate flights to rack up frequent flyer miles,
planning a trip properly could cost as little as 50 dollars to fly
around the world in 24 hours, and gaining thousands of miles using point
multipliers)&lt;/p&gt;
&lt;img alt="StackOverflow Logo" class="wordwrap-left" src="/uploads/2011/01/stack1.png" /&gt;
&lt;p&gt;Stack Overflow was search-able! Here is a thought,
create a forum that is easily index by search engines...
One question per page, on topic with the most relevant posts at the top of the page.
Who cares about user bounce backs, if they see your URL enough (StackOverflow.com) they will end up creating an account and becoming a user who creates even more quality content!&lt;/p&gt;
&lt;p&gt;The site was geared toward geeks, and programmers! They adopt new tech
the quickest.&lt;/p&gt;
&lt;p&gt;Did I miss any reasons? Feel free to comment below.&lt;/p&gt;
&lt;p&gt;You should follow me on twitter &lt;a class="reference external" href="https://twitter.com/russellbal"&gt;here&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Opinion"/></entry><entry><title>A homegrown python bread crumb module</title><link href="https://russell.ballestrini.net/a-homegrown-python-bread-crumb-module/" rel="alternate"/><published>2011-01-02T14:14:00-05:00</published><updated>2011-01-02T14:14:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2011-01-02:/a-homegrown-python-bread-crumb-module/</id><summary type="html">&lt;p&gt;I have placed
&lt;a class="reference external" href="https://bitbucket.org/russellballestrini/bread/raw/tip/bread.py"&gt;bread.py&lt;/a&gt;, a python breadcrumb module, into the public domain.&lt;/p&gt;
&lt;p&gt;The bread object accepts a url string and grants access to the url
crumbs (parts) or url links (list of hrefs to each crumb).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tutorial:&lt;/strong&gt; &lt;a class="reference external" href="/add-a-breadcrumb-subscriber-to-a-pyramid-project-using-4-simple-steps/"&gt;Add a Breadcrumb Subscriber to a Pyramid project using 4 simple
steps …&lt;/a&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;I have placed
&lt;a class="reference external" href="https://bitbucket.org/russellballestrini/bread/raw/tip/bread.py"&gt;bread.py&lt;/a&gt;, a python breadcrumb module, into the public domain.&lt;/p&gt;
&lt;p&gt;The bread object accepts a url string and grants access to the url
crumbs (parts) or url links (list of hrefs to each crumb).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tutorial:&lt;/strong&gt; &lt;a class="reference external" href="/add-a-breadcrumb-subscriber-to-a-pyramid-project-using-4-simple-steps/"&gt;Add a Breadcrumb Subscriber to a Pyramid project using 4 simple
steps&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a class="reference external" href="https://school.yohdah.com/"&gt;https://school.yohdah.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You should follow me on twitter &lt;a class="reference external" href="https://twitter.com/russellbal"&gt;here&lt;/a&gt;&lt;/p&gt;
</content><category term="misc"/><category term="Code"/><category term="Project"/><category term="Python"/></entry><entry><title>four2go!: A new spin on an old classic</title><link href="https://russell.ballestrini.net/four2go-a-new-spin-on-an-old-classic/" rel="alternate"/><published>2010-12-31T20:33:00-05:00</published><updated>2010-12-31T20:33:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2010-12-31:/four2go-a-new-spin-on-an-old-classic/</id><summary type="html"/><content type="html">&lt;p&gt;&lt;img alt="four2go! logo" src="/uploads/2010/12/four2go.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For the past two months&lt;/strong&gt; I have feverishly worked on my side project.
Initially I set out to work on this application for submission to
Hacker News, lets make November &amp;quot;Launch an App Month&amp;quot;.&lt;/p&gt;
&lt;p&gt;The whole project took longer than expected but I was please with my
progress so I continued development. Today, a month later, I have added
the finishing touches to the app. It gives me great pleasure to announce
the official launch of &lt;a class="reference external" href="https://four2go.gumyum.com"&gt;four2go!&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A new spin ...&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a class="reference external" href="https://four2go.gumyum.com"&gt;four2go!&lt;/a&gt; brings a new spin to the
classic &amp;quot;four in a row&amp;quot; genre.&lt;/p&gt;
&lt;p&gt;Instead of weighted tokens, &lt;a class="reference external" href="https://four2go.gumyum.com"&gt;four2go!&lt;/a&gt;
uses balloons which float up rather than down. This new game mechanic
requires players to retrain their brains and can easily throw off a
novice player.&lt;/p&gt;
&lt;p&gt;Another new spin, rather than playing on a coffee table, users play
using a computer over the internet. The four2go! web application
requires a simple account registration but does not require any download
or installation!&lt;/p&gt;
&lt;p&gt;Users can play with anyone, at anytime, anywhere in the world. The game
sessions are persistent meaning the board will not disappear. This
persistence allows players to check their games much like they would
check their email.&lt;/p&gt;
&lt;p&gt;&lt;img alt="gumyum logo" src="/uploads/2010/12/gumyumgameslogo.png" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The gumyum framework&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The game itself was completed after the first weekend of coding.
Programming the game was fun and exciting and originally four2go! was a
command prompt application with a text-based gui. That version was not
accessible and in order for people to play we needed to move to a
different medium. So I set out to code a web front end.&lt;/p&gt;
&lt;p&gt;After another weekend of coding I had the game working in the browser.
It was very clunky (It didn't refresh after player moves) and didn't
have any authentication, anyone could play any game.&lt;/p&gt;
&lt;blockquote&gt;
&lt;strong&gt;Coding the game was simple, the framework was the difficult
part...&lt;/strong&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;Enter Stage Left:&lt;/em&gt; The gumyum framework. I needed a way to
authentication users and a way to store and retrieve game sessions. I
also needed a way to track user statistics and player history. Most
importantly I needed a user interface that would be intuitive and fun to
use! The remainder of the project set out to solve each of those needs.
I needed the gumyum framework to make four2go! popular. I spent the rest
of the time (1.5 months) working on gumyum and I feel safe claiming that
the framework appears complete.&lt;/p&gt;
&lt;p&gt;The great part about having a framework is the code reusability, I
should be able to &amp;quot;plug&amp;quot; new games or new mechanics behind the gumyum
framework with little trouble. My labor and efforts should pay off if I
ever decide to build another game.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Artwork and graphic design&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I was very fortunate to work with my brother &lt;a class="reference external" href="https://joey.ballestrini.net"&gt;Joey
Ballestrini&lt;/a&gt; on some of the concept and
game art. We designed the &lt;a class="reference external" href="https://four2go.gumyum.com"&gt;four2go!&lt;/a&gt; logo,
the &lt;a class="reference external" href="https://gumyum.com"&gt;gumyum&lt;/a&gt; logo, and the balloons. Joey designed
each of the &amp;quot;create&amp;quot; game icons. The clouds, the grass, and the dirt
were created for another game and I decided they were a good fit so they
were re-purposed. We both use gimp when creating graphical computer art.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LETS PLAY!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I have attached a video of the game, but I
urge you to try &lt;a class="reference external" href="https://four2go.gumyum.com"&gt;four2go!&lt;/a&gt; out for
yourself!&lt;/p&gt;
&lt;p&gt;
&lt;center&gt;
&lt;iframe title="YouTube video player" width="480" height="338" src="https://www.youtube.com/embed/wmB9PeKBAlA" frameborder="0"&gt;
&lt;/iframe&gt;
&lt;/center&gt;
&lt;/p&gt;</content><category term="misc"/><category term="Project"/><category term="Python"/></entry><entry><title>The Barrymores stole my heart then crushed it</title><link href="https://russell.ballestrini.net/the-barrymores-stole-my-heart-then-crushed-it/" rel="alternate"/><published>2010-11-30T04:37:00-05:00</published><updated>2010-11-30T04:37:00-05:00</updated><author><name>Russell Ballestrini</name></author><id>tag:russell.ballestrini.net,2010-11-30:/the-barrymores-stole-my-heart-then-crushed-it/</id><summary type="html"/><content type="html">&lt;img alt="The Barrymores Ska Band" src="/uploads/2010/11/thebarrymores.png" /&gt;
&lt;p&gt;&lt;strong&gt;Recently while surfing pandora&lt;/strong&gt; I stumbled upon a new favorite song,
&amp;quot;Think Straight Again&amp;quot;, composed by a ska band named The Barrymores. &amp;nbsp;I
embraced the bands alluring melodies after the first play.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What caught my attention?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Well the whole band rocks, &lt;em&gt;hard&lt;/em&gt;. &amp;nbsp;The main components of The
Barrymores shape a full and energetic sound. &amp;nbsp;They have a drummer,
a&amp;nbsp;bassist, &amp;nbsp;a lead&amp;nbsp;guitarist and two horn players (trumpet&amp;nbsp;and
trombone). &amp;nbsp;Oh, I almost forgot to mention they&amp;nbsp;contrast most ska bands
by featuring a kickass lead female singer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What compelled me to write about The Barrymores?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Though The Barrymores are a&amp;nbsp;small town band, their technical abilities
and prowess mimic more a band of a major record label. &amp;nbsp;Their rocking
upbeat style is similar to other ska bands. &amp;nbsp;Each track creates a
euphoric&amp;nbsp;sensation and&amp;nbsp;even the &amp;quot;sad&amp;quot;&amp;nbsp;songs feel uplifting. When The
Barrymores are on my stereo I typically&amp;nbsp;end up chanting the lyrics while
skanking around my office. &amp;nbsp;I feel they bring the ska punk rock genre
into another level of excellence.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;And then I found the depressing news ...&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I started researching The Barrymores because I was&amp;nbsp;fascinated&amp;nbsp;with their music.
This research&amp;nbsp;inevitably&amp;nbsp;lead to some depressing news,
The Barrymores are no longer together!&lt;/p&gt;
&lt;blockquote&gt;
&amp;quot;I Am Jack's Broken Heart&amp;quot; -Fight Club.&lt;/blockquote&gt;
&lt;p&gt;Not often do I get excited about new music.
In moments I grew attached to the group only to get crushed.
I&amp;nbsp;decided&amp;nbsp;to investigate to uncover more.&lt;/p&gt;
&lt;p&gt;Apparently The Barrymores went into&amp;nbsp;hiatus after one of the founding
members passed away.&amp;nbsp;&amp;nbsp;&amp;nbsp;The
Barrymore&amp;nbsp;&lt;a class="reference external" href="https://www.myspace.com/thebarrymores"&gt;myspace&lt;/a&gt; page
still seems active and continues to hosts a few of the bands&amp;nbsp;tracks.&lt;/p&gt;
&lt;p&gt;Sorry about the bummer, but you should still give the music a listen.&lt;/p&gt;
</content><category term="misc"/><category term="Opinion"/></entry></feed>