Mike Slinn's Blog2023-10-01T10:07:27-04:00https://mslinn.github.io/blogMike Slinnmslinn@gmail.comInstalling JDK 17 on Ubuntu2023-09-28T00:00:00-04:00https://mslinn.github.io/blog/2023/09/28/jdk<!-- #region install -->
<p>
The Java Development Kit (JDK) version 17 is compatible with v11 and v8.
Installing the JDK also installs the JRE, as you can see from the highlighted dependency shown below:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id94ac31c3123e'><button class='copyBtn' data-clipboard-target='#id94ac31c3123e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>apt show -a openjdk-17-jdk
<span class='unselectable'>Package: openjdk-17-jdk
Version: 17.0.8.1+1~us1-0ubuntu1~23.04
Priority: optional
Section: java
Source: openjdk-17
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: OpenJDK Team <openjdk-17@packages.debian.org>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 1544 kB
Provides: java-compiler, java-sdk (= 17), java10-sdk, java11-sdk, java12-sdk, java13-sdk, java14-sdk, java15-sdk, java16-sdk, java17-sdk, java2-sdk, java5-sdk, java6-sdk, java7-sdk, java8-sdk, java9-sdk
Depends: <span class="bg_yellow">openjdk-17-jre (= 17.0.8.1+1~us1-0ubuntu1~23.04)</span>, openjdk-17-jdk-headless (= 17.0.8.1+1~us1-0ubuntu1~23.04), libc6 (>= 2.34), zlib1g (>= 1:1.1.4)
Recommends: libxt-dev
Suggests: openjdk-17-demo, openjdk-17-source, visualvm
Homepage: https://openjdk.java.net/
Download-Size: 1486 kB
APT-Sources: http://archive.ubuntu.com/ubuntu lunar-updates/main amd64 Packages
Description: OpenJDK Development Kit (JDK)
OpenJDK is a development environment for building applications,
applets, and components using the Java programming language.<br>
Package: openjdk-17-jdk
Version: 17.0.6+10-1ubuntu2
Priority: optional
Section: java
Source: openjdk-17
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: OpenJDK Team <openjdk-17@packages.debian.org>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 4732 kB
Provides: java-compiler, java-sdk, java10-sdk, java11-sdk, java12-sdk, java13-sdk, java14-sdk, java15-sdk, java16-sdk, java17-sdk, java2-sdk, java5-sdk, java6-sdk, java7-sdk, java8-sdk, java9-sdk
Depends: openjdk-17-jre (= 17.0.6+10-1ubuntu2), openjdk-17-jdk-headless (= 17.0.6+10-1ubuntu2), libc6 (>= 2.34)
Recommends: libxt-dev
Suggests: openjdk-17-demo, openjdk-17-source, visualvm
Homepage: https://openjdk.java.net/
Download-Size: 4585 kB
APT-Sources: http://archive.ubuntu.com/ubuntu lunar/main amd64 Packages
Description: OpenJDK Development Kit (JDK)
OpenJDK is a development environment for building applications,
applets, and components using the Java programming language. </span></pre>
</div>
<!-- endregion -->
<p>
Install OpenJDK 17 and its dependencies:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbc1eebb3bae1'><button class='copyBtn' data-clipboard-target='#idbc1eebb3bae1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo apt install openjdk-17-jdk</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region set default java -->
<h2 id="def">Setting the Default Java Version</h2>
<p>
The man page for <code>update-java-alternatives</code> is:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id948bbdf0540d'><button class='copyBtn' data-clipboard-target='#id948bbdf0540d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>man update-java-alternatives
<span class='unselectable'>UPDATE-JAVA-ALTERNATIVESystem Manager's MUPDATE-JAVA-ALTERNATIVES(8)<br/>
NAME
update-java-alternatives - update alternatives for jre/sdk
installations<br/>
SYNOPSIS
update-java-alternatives [--jre] [--plugin] [-v|--verbose]
-l|--list [<jname>]
-s|--set <jname>
-a|--auto
-h|-?|--help<br/>
DESCRIPTION
update-java-alternatives updates all alternatives belonging
to one runtime or development kit for the Java language. A
package does provide these information of it's alternatives
in /usr/lib/jvm/.<jname>.jinfo.<br/>
OPTIONS
-l|--list [<jname>]
List all installed packages (or just <jname>) provid‐
ing information to set a bunch of java alternatives.
Verbose output shows each alternative provided by the
packages.<br/>
-a|--auto
Switch all alternatives of registered jre/sdk instal‐
lations to automatic mode.<br/>
-s|--set <jname>
Set all alternatives of the registered jre/sdk instal‐
lation to the program path provided by the <jname> in‐
stallation.<br/>
--jre Limit the actions to alternatives belong to a runtime
environment, not a development kit.<br/>
--jre-headless
Limit the actions to alternatives belong to the head‐
less part of a runtime environment.<br/>
--plugin
Limit the actions to alternatives providing browser
plugins.<br/>
-h|--help
Display a help message.<br/>
-v|--verbose
Verbose output.<br/>
FILES
/usr/lib/jvm/.*.jinfo
A text file describing a jre/sdk installation. Con‐
sists of some variables of the form <var>=<value> and
a list of alternatives of the form jre|jdk <name>
<path>.<br/>
AUTHOR
update-java-alternatives and this manual page was written by
Matthias Klose <doko@ubuntu.com>.<br/>
May 2006 UPDATE-JAVA-ALTERNATIVES(8) </span></pre>
</div>
<!-- endregion -->
<p>
The above man page neglects to say that a package version’s priority is set from its version number.
Thus a newer version would normally have a higher priority.
To get a list of the installed Java versions, type:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb5e154f7f65a'><button class='copyBtn' data-clipboard-target='#idb5e154f7f65a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo update-java-alternatives --list
<span class='unselectable'>java-1.11.0-openjdk-amd64 1111 /usr/lib/jvm/java-1.11.0-openjdk-amd64
java-1.17.0-openjdk-amd64 1711 /usr/lib/jvm/java-1.17.0-openjdk-amd64 </span></pre>
</div>
<!-- endregion -->
<p>
As you can see, my machine had Java 11 and 17 installed.
To set the newest version to be the default, type:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id23650d162a72'><button class='copyBtn' data-clipboard-target='#id23650d162a72' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo update-java-alternatives -a
<span class='unselectable'>$ </span>java -version
<span class='unselectable'>openjdk version "17.0.8.1" 2023-08-24
OpenJDK Runtime Environment (build 17.0.8.1+1-Ubuntu-0ubuntu123.04)
OpenJDK 64-Bit Server VM (build 17.0.8.1+1-Ubuntu-0ubuntu123.04, mixed mode, sharing) </span></pre>
</div>
<!-- endregion -->
<!-- endregion -->
C++ Boost library2023-09-14T00:00:00-04:00https://mslinn.github.io/blog/2023/09/14/boost<!-- #region intro -->
<h2 id="about">About Boost</h2>
<p>
<a href='www.boost.org'>Boost</a> is a general-purpose open source library of utility functions for C++.
The <a href='https://www.boost.org/doc/libs/?view=condensed' target='_blank' rel='nofollow'>list of categories of fuctionality</a> is formidable.
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<a href='https://www.boost.org/doc/libs/?view=condensed' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/c/boostCategories.webp" type="image/webp">
<source srcset="/blog/c/boostCategories.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/c/boostCategories.png"
style='width: 100%; '
/>
</picture>
</a>
</div>
<p>
Boost is used in many <a href='https://www.boost.org/users/uses_shrink.html' target='_blank' rel='nofollow'>commercial products</a>,
<a href='https://www.boost.org/users/uses_inhouse.html' target='_blank' rel='nofollow'>in-house projects</a>,
and <a href='https://www.boost.org/users/uses_open.html' target='_blank' rel='nofollow'>open-source projects</a>.
</p>
<p>
Ostensibly a C++ library for wide usage,
it has become an techical underpinning for Python;
that is why Boost documentation also contains information about Python.
</p>
<h2 id="start">Getting Started</h2>
<p>
I started by reading the online <a href='https://www.boost.org/doc/libs/1_83_0/more/getting_started/index.html' target='_blank' rel='nofollow'>Getting Started</a> guide.
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<a href='https://www.boost.org/doc/libs/1_83_0/more/getting_started/index.html' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/c/boost_getting_started.webp" type="image/webp">
<source srcset="/blog/c/boost_getting_started.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/c/boost_getting_started.png"
style='width: 100%; '
/>
</picture>
</a>
</div>
<p>
For most Boost functionality, only headers are required, and libraries need not be built.
The exceptions are described
<a href='https://www.boost.org/doc/libs/1_83_0/more/getting_started/unix-variants.html#header-only-libraries' target='_blank' rel='nofollow'>here</a>.
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<a href='https://www.boost.org/doc/libs/1_83_0/more/getting_started/unix-variants.html#header-only-libraries' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/c/boost_header_only.webp" type="image/webp">
<source srcset="/blog/c/boost_header_only.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/c/boost_header_only.png"
style='width: 100%; '
/>
</picture>
</a>
</div>
<p>
<a href='https://theboostcpplibraries.com/' target='_blank' rel='nofollow'>The Boost C++ Libraries</a> is a good free online book,
and you can order non-free printed copies.
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<a href='https://theBoostCppLibraries.com' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/c/the_boost_cpp_libraries.webp" type="image/webp">
<source srcset="/blog/c/the_boost_cpp_libraries.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/c/the_boost_cpp_libraries.png"
style='width: 100%; '
/>
</picture>
</a>
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="install">Ubuntu Installation</h2>
<h3 id="boost_ver">Discover the Latest Boost Version Number</h3>
<p>
The latest version of Boost is shown on
<a href='https://www.boost.org/users/history/?ref=learnubuntu.com' target='_blank' rel='nofollow'><code>https://www.boost.org/users/history/?ref=learnubuntu.com</code></a>:
</p>
<p>
Let’s scrape the lastest version number from the above web page:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id705ca586bcda'><button class='copyBtn' data-clipboard-target='#id705ca586bcda' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo apt install libxml2-utils
<span class='unselectable'>$ </span>VER=$(
wget -q -O - https://www.boost.org/users/history/?ref=learnubuntu.com | \
xmllint --html --xpath '//*[@id="intro"]/div/h2[1]/a[2]' - 2>/dev/null | \
grep -oEi 'Version ([0-9].)*' | \
cut -d' ' -f 2
)
<span class='unselectable'>$ </span>echo $VER
<span class='unselectable'>1.83 </span></pre>
</div>
<!-- endregion -->
<h3 id="dl">Download Source & Dependencies</h3>
<p>
Download and unpack the library source to a new directory within your home directory as follows:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id04dec748c187'><button class='copyBtn' data-clipboard-target='#id04dec748c187' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cd
<span class='unselectable'>$ </span>V_R=`echo $VER | tr . _`
<span class='unselectable'>$ </span>echo $V_R
<span class='unselectable'>1_83 </span>
<span class='unselectable'>$ </span>NAME=boost_${V_R}_0
<span class='unselectable'>$ </span>echo $NAME
<span class='unselectable'>boost_1_83_0 </span>
<span class='unselectable'>$ </span>wget -O $NAME.tar.gz \
https://sourceforge.net/projects/boost/files/boost/$VER.0/$NAME.tar.gz/download
<span class='unselectable'>$ </span>tar xzvf $NAME.tar.gz
<span class='unselectable'>$ </span>cd $NAME/</pre>
</div>
<!-- endregion -->
<p>
You should now have the following files and directories:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0b01012d312d'><button class='copyBtn' data-clipboard-target='#id0b01012d312d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ls -w72
<span class='unselectable'>INSTALL boost/ boostcpp.jam index.htm rst.css
Jamroot boost-build.jam bootstrap.bat index.html status/
LICENSE_1_0.txt boost.css bootstrap.sh* libs/ tools/
README.md boost.png doc/ more/ </span></pre>
</div>
<!-- endregion -->
<p>
Install the required dependencies for building Boost.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida9e0e6a07b95'><button class='copyBtn' data-clipboard-target='#ida9e0e6a07b95' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo apt-get update
<span class='unselectable'>$ </span>sudo apt install autotools-dev build-essential g++ \
libbz2-dev libicu-dev python3-dev</pre>
</div>
<!-- endregion -->
<h3 id="bootstrap">Build Installer Using <span class="code">Bootstrap</span></h3>
<p>
Two scripts for building the installation program Boost are provided:
<code>bootstrap.sh</code> (for Bash) and <code>bootstrap.bat</code> (for Windows/DOS).
By default the scripts build all possible static/shared debug/release Boost libraries.
Another default is that the script uses all the CPU cores your computer has for the build process.
Even using all the cores on a fast laptop, the build might take 10 minutes.
</p>
<p>
Here is the <code>bootstrap.sh</code> help message:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide4f1c7b13b26'><button class='copyBtn' data-clipboard-target='#ide4f1c7b13b26' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>./bootstrap.sh -h
<span class='unselectable'>`./bootstrap.sh\' builds the Boost build system B2 and prepares Boost for
building. This includes setting defaults in the project-config.jam which you
can adjust prior to invoking B2.<br/>
Usage: ./bootstrap.sh [OPTION]...<br/>
Defaults for the options are specified in brackets.<br/>
Configuration:
-h, --help display this help and exit
--with-bjam=BJAM use existing Boost.Jam executable (bjam)
[automatically built]
--with-toolset=TOOLSET use specific TOOLSET to build B2 and as default
for building Boost
[automatically detected]
--show-libraries show the set of libraries that require build
and installation steps (i.e., those libraries
that can be used with --with-libraries or
--without-libraries), then exit
--with-libraries=list build only a particular set of libraries,
describing using either a comma-separated list of
library names or "all"
[all]
--without-libraries=list build all libraries except the ones listed []
--with-icu enable Unicode/ICU support in Regex
[automatically detected]
--without-icu disable Unicode/ICU support in Regex
--with-icu=DIR specify the root of the ICU library installation
and enable Unicode/ICU support in Regex
[automatically detected]
--with-python=PYTHON specify the Python executable [python]
--with-python-root=DIR specify the root of the Python installation
[automatically detected]
--with-python-version=X.Y specify the Python version as X.Y
[automatically detected]<br/>
Installation directories:
--prefix=PREFIX install Boost into the given PREFIX
[/usr/local]
--exec-prefix=EPREFIX install Boost binaries into the given EPREFIX
[PREFIX]<br/>
More precise control over installation directories:
--libdir=DIR install libraries here [EPREFIX/lib]
--includedir=DIR install headers here [PREFIX/include] </span></pre>
</div>
<p>
<code>Bootstrap.sh</code> compiles and links a build program called <code>b2</code>,
which by default installs all of Boost into <code>/usr/<wbr>local/</code>.
To be more specific, the default location for Boost include files to be installed into is <code>/usr/<wbr>local/<wbr>include/<wbr>boost/</code>,
and Boost libraries are installed by default into <code>/usr/<wbr>local/<wbr>lib/</code>.
</p>
<!-- endregion -->
<p>
Create (and recreate) the <code>b2</code> installation program for Boost
for <i>each</i> <a href='/blog/2021/04/09/python-venvs.html'>Python virtual environment</a>.
By default, <code>bootstrap.sh</code> uses the currently active Python virtual environment, like this:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf1612b7ad120'><button class='copyBtn' data-clipboard-target='#idf1612b7ad120' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>./bootstrap.sh</pre>
</div>
<p>
Following is how to build <code>b2</code> for the virtual environment at <code>~/venv/blah/</code>.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4d0ac63eea6c'><button class='copyBtn' data-clipboard-target='#id4d0ac63eea6c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>./bootstrap.sh --with-python-root=~/venv/blah
<span class='unselectable'>Building B2 engine..<br/>
###
###
### Using 'gcc' toolset.
###
###<br/>
g++ (Ubuntu 12.3.0-1ubuntu1~23.04) 12.3.0
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.<br/><br/>
###
###<br/>
> g++ -x c++ -std=c++11 -O2 -s -DNDEBUG builtins.cpp class.cpp command.cpp compile.cpp constants.cpp cwd.cpp debug.cpp debugger.cpp execcmd.cpp execnt.cpp execunix.cpp filesys.cpp filent.cpp fileunix.cpp frames.cpp function.cpp glob.cpp hash.cpp hcache.cpp hdrmacro.cpp headers.cpp jam_strings.cpp jam.cpp jamgram.cpp lists.cpp make.cpp make1.cpp md5.cpp mem.cpp modules.cpp native.cpp object.cpp option.cpp output.cpp parse.cpp pathnt.cpp pathsys.cpp pathunix.cpp regexp.cpp rules.cpp scan.cpp search.cpp startup.cpp subst.cpp sysinfo.cpp timestamp.cpp variable.cpp w32_getreg.cpp modules/order.cpp modules/path.cpp modules/property-set.cpp modules/regex.cpp modules/sequence.cpp modules/set.cpp -o b2
tools/build/src/engine/b2
Detecting Python version... 3.11
Unicode/ICU support for Boost.Regex?... /usr
Backing up existing B2 configuration in project-config.jam.2
Generating B2 configuration in project-config.jam for gcc...<br/>
Bootstrapping is done. To build, run:<br/>
./b2<br/>
To generate header files, run:<br/>
./b2 headers<br/>
The configuration generated uses gcc to build by default. If that is
unintended either use the --with-toolset option or adjust configuration, by
editing 'project-config.jam'.<br/>
Further information:<br/>
- Command line help:
./b2 --help<br/>
- Getting started guide:
http://www.boost.org/more/getting_started/unix-variants.html<br/>
- B2 documentation:
http://www.boost.org/build/ </span></pre>
</div>
<!-- endregion -->
<h3 id="b2">Running <span class="code">B2</span>, the Boost Installer</h3>
<p>
Following is the help message for the newly created <code>b2</code> Boost installation program.
As you can see, the defaults specified when creating the <code>b2</code> script can be overidden.
Note that not all of the available options are described in this help message;
for example, the <code>-j</code> option is only described in the
<a href='https://www.boost.org/doc/libs/1_83_0/tools/build/doc/html/index.html' target='_blank' rel='nofollow'>online help for <code>b2</code></a>.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idee34a3b77c12'><button class='copyBtn' data-clipboard-target='#idee34a3b77c12' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>./b2 --help
<span class='unselectable'>B2 4.10-git<br/>
Project-specific help:<br/>
Project has jamfile at Jamroot<br/>
Usage:<br/>
b2 [options] [properties] [install|stage]<br/>
Builds and installs Boost.<br/>
Targets and Related Options:<br/>
install Install headers and compiled library files to the
======= configured locations (below).<br/>
--prefix=<PREFIX> Install architecture independent files here.
Default: C:\Boost on Windows
Default: /usr/local on Unix, Linux, etc.<br/>
--exec-prefix=<EPREFIX> Install architecture dependent files here.
Default: <PREFIX><br/>
--libdir=<LIBDIR> Install library files here.
Default: <EPREFIX>/lib<br/>
--includedir=<HDRDIR> Install header files here.
Default: <PREFIX>/include<br/>
--cmakedir=<CMAKEDIR> Install CMake configuration files here.
Default: <LIBDIR>/cmake<br/>
--no-cmake-config Do not install CMake configuration files.<br/>
stage Build and install only compiled library files to the
===== stage directory.<br/>
--stagedir=<STAGEDIR> Install library files here
Default: ./stage<br/>
Other Options:<br/>
--build-type=<type> Build the specified pre-defined set of variations of
the libraries. Note, that which variants get built
depends on what each library supports.<br/>
-- minimal -- (default) Builds a minimal set of
variants. On Windows, these are static
multithreaded libraries in debug and release
modes, using shared runtime. On Linux, these are
static and shared multithreaded libraries in
release mode.<br/>
-- complete -- Build all possible variations.<br/>
--build-dir=DIR Build in this location instead of building within
the distribution tree. Recommended!<br/>
--show-libraries Display the list of Boost libraries that require
build and installation steps, and then exit.<br/>
--layout=<layout> Determine whether to choose library names and header
locations such that multiple versions of Boost or
multiple compilers can be used on the same system.<br/>
-- versioned -- Names of boost binaries include
the Boost version number, name and version of
the compiler and encoded build properties. Boost
headers are installed in a subdirectory of
<HDRDIR> whose name contains the Boost version
number.<br/>
-- tagged -- Names of boost binaries include the
encoded build properties such as variant and
threading, but do not including compiler name
and version, or Boost version. This option is
useful if you build several variants of Boost,
using the same compiler.<br/>
-- system -- Binaries names do not include the
Boost version number or the name and version
number of the compiler. Boost headers are
installed directly into <HDRDIR>. This option is
intended for system integrators building
distribution packages.<br/>
The default value is 'versioned' on Windows, and
'system' on Unix.<br/>
--buildid=ID Add the specified ID to the name of built libraries.
The default is to not add anything.<br/>
--python-buildid=ID Add the specified ID to the name of built libraries
that depend on Python. The default is to not add
anything. This ID is added in addition to --buildid.<br/>
--help This message.<br/>
--with-<library> Build and install the specified <library>. If this
option is used, only libraries specified using this
option will be built.<br/>
--without-<library> Do not build, stage, or install the specified
<library>. By default, all libraries are built.<br/>
Properties:<br/>
toolset=toolset Indicate the toolset to build with.<br/>
variant=debug|release Select the build variant<br/>
link=static|shared Whether to build static or shared libraries<br/>
threading=single|multi Whether to build single or multithreaded binaries<br/>
runtime-link=static|shared
Whether to link to static or shared C and C++
runtime.<br/><br/>
General command line usage:<br/>
b2 [options] [properties] [targets]<br/>
Options, properties and targets can be specified in any order.<br/>
Important Options:<br/>
* --clean Remove targets instead of building
* -a Rebuild everything
* -n Don't execute the commands, only print them
* -d+2 Show commands as they are executed
* -d0 Suppress all informational messages
* -q Stop at first error
* --reconfigure Rerun all configuration checks
* --durations[=N] Report top N targets by execution time
* --debug-configuration Diagnose configuration
* --debug-building Report which targets are built with what properties
* --debug-generator Diagnose generator search/execution<br/>
Further Help:<br/>
The following options can be used to obtain additional documentation.<br/>
* --help-options Print more obscure command line options.
* --help-internal B2 implementation details.
* --help-doc-options Implementation details doc formatting.<br/>
...found 1 target... </span></pre>
</div>
<!-- endregion -->
<p>
The following runs the newly created <code>b2</code> program,
which builds and installs Boost using all CPU cores.
</p>
<ol>
<li>
The Python version is stored into an environment variable called <code>PVER</code>.
</li>
<li>
The <code>CPLUS_INCLUDE_PATH</code> environment variable is pointed at the location of the Python 3 include files.
<code>PVER</code> was used to set the proper value for <code>CPLUS_INCLUDE_PATH</code>.
</li>
<li>
<code>B2</code> generates screen upon screen of compiler warnings.
The <code>-d 1</code> option suppresses them.
</li>
</ol>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id931f922e5181'><button class='copyBtn' data-clipboard-target='#id931f922e5181' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>PVER="$(python --version | cut -d' ' -f 2 | grep -oEi '(3.[0-9]*)')"
<span class='unselectable'>$ </span>echo $PVER
<span class='unselectable'>3.11 </span>
<span class='unselectable'>$ </span>sudo CPLUS_INCLUDE_PATH=/usr/include/python$PVER ./b2 -d 1 install
<span class='unselectable'>Performing configuration checks<br/>
- default address-model : 64-bit (cached) [1]
- default architecture : x86 (cached) [1]
- compiler supports SSE2 : yes (cached) [2]
- compiler supports SSE4.1 : yes (cached) [2]
- has std::atomic_ref : no (cached) [2]
- has -Wl,--no-undefined : yes (cached) [2]
- has statx : yes (cached) [2]
- has init_priority attribute : yes (cached) [2]
- has stat::st_blksize : yes (cached) [2]
- has stat::st_mtim : yes (cached) [2]
- has stat::st_mtimensec : no (cached) [2]
- has stat::st_mtimespec : no (cached) [2]
- has stat::st_birthtim : no (cached) [2]
- has stat::st_birthtimensec : no (cached) [2]
- has stat::st_birthtimespec : no (cached) [2]
- has fdopendir(O_NOFOLLOW) : yes (cached) [2]
- has dirent::d_type : yes (cached) [2]
- has POSIX *at APIs : yes (cached) [2]
- cxx11_auto_declarations : yes (cached) [2]
- cxx11_constexpr : yes (cached) [2]
- cxx11_defaulted_functions : yes (cached) [2]
- cxx11_final : yes (cached) [2]
- cxx11_hdr_mutex : yes (cached) [2]
- cxx11_hdr_tuple : yes (cached) [2]
- cxx11_lambdas : yes (cached) [2]
- cxx11_noexcept : yes (cached) [2]
- cxx11_nullptr : yes (cached) [2]
- cxx11_rvalue_references : yes (cached) [2]
- cxx11_template_aliases : yes (cached) [2]
- cxx11_thread_local : yes (cached) [2]
- cxx11_variadic_templates : yes (cached) [2]
- has_icu builds : yes (cached) [2]
warning: Graph library does not contain MPI-based parallel components.
note: to enable them, add "using mpi ;" to your user-config.jam.
note: to suppress this message, pass "--without-graph_parallel" to bjam.
- zlib : yes (cached)
- bzip2 : yes (cached)
- lzma : yes (cached)
- zstd : yes (cached)
- has_lzma_cputhreads builds : yes (cached) [2]
- cxx11_decltype : yes (cached) [2]
- cxx11_basic_alignas : yes (cached) [2]
- iconv (libc) : yes (cached) [2]
- icu : yes (cached) [2]
- cxx11_defaulted_moves : yes (cached) [2]
- cxx11_hdr_functional : yes (cached) [2]
- cxx11_hdr_type_traits : yes (cached) [2]
- cxx11_override : yes (cached) [2]
- cxx11_range_based_for : yes (cached) [2]
- cxx11_scoped_enums : yes (cached) [2]
- cxx11_smart_ptr : yes (cached) [2]
- cxx11_static_assert : yes (cached) [2]
- lockfree boost::atomic_flag : yes (cached) [2]
- native atomic int32 supported : yes (cached) [2]
- native syslog supported : yes (cached) [2]
- pthread supports robust mutexes : yes (cached) [2]
- compiler supports SSSE3 : yes (cached) [2]
- compiler supports AVX2 : yes (cached) [2]
- gcc visibility : yes (cached) [2]
- sfinae_expr : yes (cached) [2]
- cxx11_unified_initialization_syntax : yes (cached) [2]
- cxx11_hdr_initializer_list : yes (cached) [2]
- cxx11_hdr_chrono : yes (cached) [2]
- cxx11_numeric_limits : yes (cached) [2]
- cxx11_hdr_array : yes (cached) [2]
- cxx11_hdr_atomic : yes (cached) [2]
- cxx11_allocator : yes (cached) [2]
- cxx11_explicit_conversion_operators : yes (cached) [2]
- long double support : yes (cached) [2]
warning: skipping optional Message Passing Interface (MPI) library.
note: to enable MPI support, add "using mpi ;" to user-config.jam.
note: to suppress this message, pass "--without-mpi" to bjam.
note: otherwise, you can safely ignore this message.
- cxx11_char16_t : yes (cached) [2]
- cxx11_char32_t : yes (cached) [2]
- Has Large File Support : yes (cached) [2]
- Has attribute init_priority : yes (cached) [2]
- libbacktrace builds : yes (cached) [2]
- addr2line builds : yes (cached) [2]
- WinDbg builds : no (cached) [2]
- WinDbg builds : no (cached) [3]
- WinDbgCached builds : no (cached) [2]
- WinDbgCached builds : no (cached) [3]
- BOOST_COMP_GNUC >= 4.3.0 : yes (cached) [2]
- BOOST_COMP_GNUC >= 4.3.0 : yes (cached) [4]
- cxx11_hdr_thread : yes (cached) [2]
- cxx11_hdr_regex : yes (cached) [2]
- compiler supports SSE2 : yes (cached) [4]
- compiler supports SSE4.1 : yes (cached) [4]
- has std::atomic_ref : no (cached) [4]
- has statx : yes (cached) [4]
- has init_priority attribute : yes (cached) [4]
- has stat::st_blksize : yes (cached) [4]
- has stat::st_mtim : yes (cached) [4]
- has stat::st_mtimensec : no (cached) [4]
- has stat::st_mtimespec : no (cached) [4]
- has stat::st_birthtim : no (cached) [4]
- has stat::st_birthtimensec : no (cached) [4]
- has stat::st_birthtimespec : no (cached) [4]
- has fdopendir(O_NOFOLLOW) : yes (cached) [4]
- has dirent::d_type : yes (cached) [4]
- has POSIX *at APIs : yes (cached) [4]
- cxx11_auto_declarations : yes (cached) [4]
- cxx11_constexpr : yes (cached) [4]
- cxx11_defaulted_functions : yes (cached) [4]
- cxx11_final : yes (cached) [4]
- cxx11_hdr_mutex : yes (cached) [4]
- cxx11_hdr_tuple : yes (cached) [4]
- cxx11_lambdas : yes (cached) [4]
- cxx11_noexcept : yes (cached) [4]
- cxx11_nullptr : yes (cached) [4]
- cxx11_rvalue_references : yes (cached) [4]
- cxx11_template_aliases : yes (cached) [4]
- cxx11_thread_local : yes (cached) [4]
- cxx11_variadic_templates : yes (cached) [4]
- has_icu builds : yes (cached) [4]
- zlib : yes (cached) [5]
- bzip2 : yes (cached) [5]
- lzma : yes (cached) [5]
- zstd : yes (cached) [5]
- has_lzma_cputhreads builds : yes (cached) [4]
- cxx11_decltype : yes (cached) [4]
- cxx11_basic_alignas : yes (cached) [4]
- iconv (libc) : yes (cached) [4]
- icu : yes (cached) [4]
- cxx11_defaulted_moves : yes (cached) [4]
- cxx11_hdr_functional : yes (cached) [4]
- cxx11_hdr_type_traits : yes (cached) [4]
- cxx11_override : yes (cached) [4]
- cxx11_range_based_for : yes (cached) [4]
- cxx11_scoped_enums : yes (cached) [4]
- cxx11_smart_ptr : yes (cached) [4]
- cxx11_static_assert : yes (cached) [4]
- lockfree boost::atomic_flag : yes (cached) [4]
- native atomic int32 supported : yes (cached) [4]
- native syslog supported : yes (cached) [4]
- pthread supports robust mutexes : yes (cached) [4]
- compiler supports SSSE3 : yes (cached) [4]
- compiler supports AVX2 : yes (cached) [4]
- gcc visibility : yes (cached) [4]
- sfinae_expr : yes (cached) [4]
- cxx11_unified_initialization_syntax : yes (cached) [4]
- cxx11_hdr_initializer_list : yes (cached) [4]
- cxx11_hdr_chrono : yes (cached) [4]
- cxx11_numeric_limits : yes (cached) [4]
- cxx11_hdr_array : yes (cached) [4]
- cxx11_hdr_atomic : yes (cached) [4]
- cxx11_allocator : yes (cached) [4]
- cxx11_explicit_conversion_operators : yes (cached) [4]
- long double support : yes (cached) [4]
- cxx11_char16_t : yes (cached) [4]
- cxx11_char32_t : yes (cached) [4]
- Has Large File Support : yes (cached) [4]
- Has attribute init_priority : yes (cached) [4]
- libbacktrace builds : yes (cached) [4]
- addr2line builds : yes (cached) [4]
- WinDbg builds : no (cached) [4]
- WinDbg builds : no (cached) [6]
- WinDbgCached builds : no (cached) [4]
- WinDbgCached builds : no (cached) [6]
- cxx11_hdr_thread : yes (cached) [4]
- cxx11_hdr_regex : yes (cached) [4]<br/>
[1] gcc-12
[2] gcc-12/release/python-3.11/threading-multi/visibility-hidden
[3] gcc-12/release/build-no/python-3.11/threading-multi/visibility-hidden
[4] gcc-12/release/link-static/python-3.11/threading-multi/visibility-hidden
[5] link-static
[6] gcc-12/release/build-no/link-static/python-3.11/threading-multi/visibility-hidden<br/>
Component configuration:<br/>
- atomic : building
- chrono : building
- container : building
- context : building
- contract : building
- coroutine : building
- date_time : building
- exception : building
- fiber : building
- filesystem : building
- graph : building
- graph_parallel : building
- headers : building
- iostreams : building
- json : building
- locale : building
- log : building
- math : building
- mpi : building
- nowide : building
- program_options : building
- python : building
- random : building
- regex : building
- serialization : building
- stacktrace : building
- system : building
- test : building
- thread : building
- timer : building
- type_erasure : building
- url : building
- wave : building<br/>
...patience...
...patience...
...patience...
...patience...
...patience...
...patience...
...patience...
...found 52031 targets... </span></pre>
</div>
<!-- endregion -->
<p>
The <code>b2</code> installation program stored the following include files in
<code>/usr/<wbr>local/<wbr>include/<wbr>boost/</code>
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc5f487779ddc'><button class='copyBtn' data-clipboard-target='#idc5f487779ddc' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ls -w72 /usr/local/include/boost/
<span class='unselectable'>accumulators/ make_unique.hpp
algorithm/ math/
align/ math_fwd.hpp
align.hpp mem_fn.hpp
aligned_storage.hpp memory_order.hpp
any/ metaparse/
any.hpp metaparse.hpp
archive/ move/
array.hpp mp11/
asio/ mp11.hpp
asio.hpp mpi/
assert/ mpi.hpp
assert.hpp mpl/
assign/ msm/
assign.hpp multi_array/
atomic/ multi_array.hpp
atomic.hpp multi_index/
beast/ multi_index_container.hpp
beast.hpp multi_index_container_fwd.hpp
bimap/ multiprecision/
bimap.hpp mysql/
bind/ mysql.hpp
bind.hpp next_prior.hpp
blank.hpp non_type.hpp
blank_fwd.hpp noncopyable.hpp
call_traits.hpp nondet_random.hpp
callable_traits/ none.hpp
callable_traits.hpp none_t.hpp
cast.hpp nowide/
cerrno.hpp numeric/
checked_delete.hpp operators.hpp
chrono/ operators_v1.hpp
chrono.hpp optional/
circular_buffer/ optional.hpp
circular_buffer.hpp outcome/
circular_buffer_fwd.hpp outcome.hpp
compat/ parameter/
compatibility/ parameter.hpp
compressed_pair.hpp pending/
compute/ pfr/
compute.hpp pfr.hpp
concept/ phoenix/
concept_archetype.hpp phoenix.hpp
concept_check/ pointee.hpp
concept_check.hpp pointer_cast.hpp
config/ pointer_to_other.hpp
config.hpp poly_collection/
container/ polygon/
container_hash/ polymorphic_cast.hpp
context/ polymorphic_pointer_cast.hpp
contract/ pool/
contract.hpp predef/
contract_macro.hpp predef.h
convert/ preprocessor/
convert.hpp preprocessor.hpp
core/ process/
coroutine/ process.hpp
coroutine2/ program_options/
crc.hpp program_options.hpp
cregex.hpp progress.hpp
cstdfloat.hpp property_map/
cstdint.hpp property_tree/
cstdlib.hpp proto/
current_function.hpp ptr_container/
cxx11_char_types.hpp python/
date_time/ python.hpp
date_time.hpp qvm/
describe/ qvm.hpp
describe.hpp qvm_lite.hpp
detail/ random/
dll/ random.hpp
dll.hpp range/
dynamic_bitset/ range.hpp
dynamic_bitset.hpp ratio/
dynamic_bitset_fwd.hpp ratio.hpp
enable_shared_from_this.hpp rational.hpp
endian/ ref.hpp
endian.hpp regex/
exception/ regex.h
exception_ptr.hpp regex.hpp
fiber/ regex_fwd.hpp
filesystem/ safe_numerics/
filesystem.hpp scope_exit.hpp
flyweight/ scoped_array.hpp
flyweight.hpp scoped_ptr.hpp
foreach.hpp serialization/
foreach_fwd.hpp shared_array.hpp
format/ shared_container_iterator.hpp
format.hpp shared_ptr.hpp
function/ signals2/
function.hpp signals2.hpp
function_equal.hpp smart_ptr/
function_output_iterator.hpp smart_ptr.hpp
function_types/ sort/
functional/ spirit/
functional.hpp spirit.hpp
fusion/ stacktrace/
generator_iterator.hpp stacktrace.hpp
geometry/ statechart/
geometry.hpp static_assert.hpp
get_pointer.hpp static_string/
gil/ static_string.hpp
gil.hpp stl_interfaces/
graph/ swap.hpp
hana/ system/
hana.hpp system.hpp
heap/ test/
histogram/ thread/
histogram.hpp thread.hpp
hof/ throw_exception.hpp
hof.hpp timer/
icl/ timer.hpp
implicit_cast.hpp token_functions.hpp
indirect_reference.hpp token_iterator.hpp
integer/ tokenizer.hpp
integer.hpp tti/
integer_fwd.hpp tuple/
integer_traits.hpp type.hpp
interprocess/ type_erasure/
intrusive/ type_index/
intrusive_ptr.hpp type_index.hpp
io/ type_traits/
io_fwd.hpp type_traits.hpp
iostreams/ typeof/
is_placeholder.hpp units/
iterator/ unordered/
iterator.hpp unordered_map.hpp
iterator_adaptors.hpp unordered_set.hpp
json/ url/
json.hpp url.hpp
lambda/ utility/
lambda2/ utility.hpp
lambda2.hpp uuid/
leaf/ variant/
leaf.hpp variant.hpp
lexical_cast/ variant2/
lexical_cast.hpp variant2.hpp
limits.hpp version.hpp
local_function/ visit_each.hpp
local_function.hpp vmd/
locale/ wave/
locale.hpp wave.hpp
lockfree/ weak_ptr.hpp
log/ winapi/
logic/ xpressive/
make_default.hpp yap/
make_shared.hpp </span></pre>
</div>
<!-- endregion -->
<p>
The <code>b2</code> installation program stored the following libraries in <code>/usr/<wbr>local/<wbr>lib/</code>:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3c418f212e4f'><button class='copyBtn' data-clipboard-target='#id3c418f212e4f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ls -w72 /usr/local/lib
<span class='unselectable'>cmake/
libboost_atomic.a
libboost_atomic.so@
libboost_atomic.so.1.83.0*
libboost_chrono.a
libboost_chrono.so@
libboost_chrono.so.1.83.0*
libboost_container.a
libboost_container.so@
libboost_container.so.1.83.0*
libboost_context.a
libboost_context.so@
libboost_context.so.1.83.0*
libboost_contract.a
libboost_contract.so@
libboost_contract.so.1.83.0*
libboost_coroutine.a
libboost_coroutine.so@
libboost_coroutine.so.1.83.0*
libboost_date_time.a
libboost_date_time.so@
libboost_date_time.so.1.83.0*
libboost_exception.a
libboost_fiber.a
libboost_fiber.so@
libboost_fiber.so.1.83.0*
libboost_filesystem.a
libboost_filesystem.so@
libboost_filesystem.so.1.83.0*
libboost_graph.a
libboost_graph.so@
libboost_graph.so.1.83.0*
libboost_iostreams.a
libboost_iostreams.so@
libboost_iostreams.so.1.83.0*
libboost_json.a
libboost_json.so@
libboost_json.so.1.83.0*
libboost_locale.a
libboost_locale.so@
libboost_locale.so.1.83.0*
libboost_log.a
libboost_log.so@
libboost_log.so.1.83.0*
libboost_log_setup.a
libboost_log_setup.so@
libboost_log_setup.so.1.83.0*
libboost_math_c99.a
libboost_math_c99.so@
libboost_math_c99.so.1.83.0*
libboost_math_c99f.a
libboost_math_c99f.so@
libboost_math_c99f.so.1.83.0*
libboost_math_c99l.a
libboost_math_c99l.so@
libboost_math_c99l.so.1.83.0*
libboost_math_tr1.a
libboost_math_tr1.so@
libboost_math_tr1.so.1.83.0*
libboost_math_tr1f.a
libboost_math_tr1f.so@
libboost_math_tr1f.so.1.83.0*
libboost_math_tr1l.a
libboost_math_tr1l.so@
libboost_math_tr1l.so.1.83.0*
libboost_nowide.a
libboost_nowide.so@
libboost_nowide.so.1.83.0*
libboost_prg_exec_monitor.a
libboost_prg_exec_monitor.so@
libboost_prg_exec_monitor.so.1.83.0*
libboost_program_options.a
libboost_program_options.so@
libboost_program_options.so.1.83.0*
libboost_random.a
libboost_random.so@
libboost_random.so.1.83.0*
libboost_regex.a
libboost_regex.so@
libboost_regex.so.1.83.0*
libboost_serialization.a
libboost_serialization.so@
libboost_serialization.so.1.83.0*
libboost_stacktrace_addr2line.a
libboost_stacktrace_addr2line.so@
libboost_stacktrace_addr2line.so.1.83.0*
libboost_stacktrace_backtrace.a
libboost_stacktrace_backtrace.so@
libboost_stacktrace_backtrace.so.1.83.0*
libboost_stacktrace_basic.a
libboost_stacktrace_basic.so@
libboost_stacktrace_basic.so.1.83.0*
libboost_stacktrace_noop.a
libboost_stacktrace_noop.so@
libboost_stacktrace_noop.so.1.83.0*
libboost_system.a
libboost_system.so@
libboost_system.so.1.83.0*
libboost_test_exec_monitor.a
libboost_thread.a
libboost_thread.so@
libboost_thread.so.1.83.0*
libboost_timer.a
libboost_timer.so@
libboost_timer.so.1.83.0*
libboost_type_erasure.a
libboost_type_erasure.so@
libboost_type_erasure.so.1.83.0*
libboost_unit_test_framework.a
libboost_unit_test_framework.so@
libboost_unit_test_framework.so.1.83.0*
libboost_url.a
libboost_url.so@
libboost_url.so.1.83.0*
libboost_wave.a
libboost_wave.so@
libboost_wave.so.1.83.0*
libboost_wserialization.a
libboost_wserialization.so@
libboost_wserialization.so.1.83.0*
node_modules/
python3.11/ </span></pre>
</div>
<!-- endregion -->
<h3 id="load_path">Verify Boost System Libraries</h3>
<p>
The <code>ldconfig</code> command manages Linux system libraries.
Here is the help message:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id460b3d7ee19f'><button class='copyBtn' data-clipboard-target='#id460b3d7ee19f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>man ldconfig
<span class='unselectable'>ldconfig(8) System Manager's Manual ldconfig(8)<br/>
NAME
ldconfig - configure dynamic linker run-time bindings<br/>
SYNOPSIS
/sbin/ldconfig [-nNvVX] [-C cache] [-f conf] [-r root]
directory ...<br/>
/sbin/ldconfig -l [-v] library ...<br/>
/sbin/ldconfig -p<br/>
DESCRIPTION
ldconfig creates the necessary links and cache to the most re‐
cent shared libraries found in the directories specified on the
command line, in the file /etc/ld.so.conf, and in the trusted
directories, /lib and /usr/lib. On some 64-bit architectures
such as x86-64, /lib and /usr/lib are the trusted directories
for 32-bit libraries, while /lib64 and /usr/lib64 are used for
64-bit libraries.<br/>
The cache is used by the run-time linker, ld.so or ld-linux.so.
ldconfig checks the header and filenames of the libraries it
encounters when determining which versions should have their
links updated. ldconfig should normally be run by the supe‐
ruser as it may require write permission on some root owned di‐
rectories and files.<br/>
ldconfig will look only at files that are named lib*.so* (for
regular shared objects) or ld-*.so* (for the dynamic loader it‐
self). Other files will be ignored. Also, ldconfig expects a
certain pattern to how the symbolic links are set up, like this
example, where the middle file (libfoo.so.1 here) is the SONAME
for the library:<br/>
libfoo.so -> libfoo.so.1 -> libfoo.so.1.12<br/>
Failure to follow this pattern may result in compatibility is‐
sues after an upgrade.<br/>
OPTIONS
-c fmt
--format=fmt
(Since glibc 2.2) Use cache format fmt, which is one of
old, new, or compat. Since glibc 2.32, the default is
new. Before that, it was compat.<br/>
-C cache
Use cache instead of /etc/ld.so.cache.<br/>
-f conf
Use conf instead of /etc/ld.so.conf.<br/>
-i
--ignore-aux-cache
(Since glibc 2.7) Ignore auxiliary cache file.<br/>
-l (Since glibc 2.2) Interpret each operand as a libary
name and configure its links. Intended for use only by
experts.<br/>
-n Process only the directories specified on the command
line; don't process the trusted directories, nor those
specified in /etc/ld.so.conf. Implies -N.<br/>
-N Don't rebuild the cache. Unless -X is also specified,
links are still updated.<br/>
-p
--print-cache
Print the lists of directories and candidate libraries
stored in the current cache.<br/>
-r root
Change to and use root as the root directory.<br/>
-v
--verbose
Verbose mode. Print current version number, the name of
each directory as it is scanned, and any links that are
created. Overrides quiet mode.<br/>
-V
--version
Print program version.<br/>
-X Don't update links. Unless -N is also specified, the
cache is still rebuilt.<br/>
FILES
/lib/ld.so
is the run-time linker/loader.
/etc/ld.so.conf
contains a list of directories, one per line, in which
to search for libraries.
/etc/ld.so.cache
contains an ordered list of libraries found in the di‐
rectories specified in /etc/ld.so.conf, as well as those
found in the trusted directories.<br/>
SEE ALSO
ldd(1), ld.so(8)<br/>
Linux man-pages 6.03 2023-01-07 ldconfig(8) </span></pre>
</div>
<!-- endregion -->
<p>
The Boost installation procedure automatically causes the newly built Boost libraries
to be added to the configured system libraries when
<code>bootstrap.sh</code> is not invoked with a target path,
for example by specifying the <code>--with-python-root</code> option.
</p>
<p>
To manually add the newly built Boost libraries to the system load path, use <code>ldconfig</code>.
The following adds the newly compiled and installed Boost libraries in
<code>/usr/local/lib/</code> (and any other libraries that might happen to be in that directory)
to the <code>ld.so</code> cache.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1e133a102dc0'><button class='copyBtn' data-clipboard-target='#id1e133a102dc0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo ldconfig -n /usr/local/lib</pre>
</div>
<!-- endregion -->
<p>
We can verify that the library cache now contains the newly built Boost libraries
by using <code>ldconfig</code>:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8fb437e9e0db'><button class='copyBtn' data-clipboard-target='#id8fb437e9e0db' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ldconfig -p | grep boost
<span class='unselectable'>libboost_wserialization.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_wserialization.so.1.83.0
libboost_wserialization.so (libc6,x86-64) => /usr/local/lib/libboost_wserialization.so
libboost_wave.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_wave.so.1.83.0
libboost_wave.so (libc6,x86-64) => /usr/local/lib/libboost_wave.so
libboost_url.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_url.so.1.83.0
libboost_url.so (libc6,x86-64) => /usr/local/lib/libboost_url.so
libboost_unit_test_framework.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_unit_test_framework.so.1.83.0
libboost_unit_test_framework.so (libc6,x86-64) => /usr/local/lib/libboost_unit_test_framework.so
libboost_type_erasure.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_type_erasure.so.1.83.0
libboost_type_erasure.so (libc6,x86-64) => /usr/local/lib/libboost_type_erasure.so
libboost_timer.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_timer.so.1.83.0
libboost_timer.so (libc6,x86-64) => /usr/local/lib/libboost_timer.so
libboost_thread.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_thread.so.1.83.0
libboost_thread.so (libc6,x86-64) => /usr/local/lib/libboost_thread.so
libboost_system.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_system.so.1.83.0
libboost_system.so (libc6,x86-64) => /usr/local/lib/libboost_system.so
libboost_stacktrace_noop.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_stacktrace_noop.so.1.83.0
libboost_stacktrace_noop.so (libc6,x86-64) => /usr/local/lib/libboost_stacktrace_noop.so
libboost_stacktrace_basic.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_stacktrace_basic.so.1.83.0
libboost_stacktrace_basic.so (libc6,x86-64) => /usr/local/lib/libboost_stacktrace_basic.so
libboost_stacktrace_backtrace.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_stacktrace_backtrace.so.1.83.0
libboost_stacktrace_backtrace.so (libc6,x86-64) => /usr/local/lib/libboost_stacktrace_backtrace.so
libboost_stacktrace_addr2line.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_stacktrace_addr2line.so.1.83.0
libboost_stacktrace_addr2line.so (libc6,x86-64) => /usr/local/lib/libboost_stacktrace_addr2line.so
libboost_serialization.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_serialization.so.1.83.0
libboost_serialization.so (libc6,x86-64) => /usr/local/lib/libboost_serialization.so
libboost_regex.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_regex.so.1.83.0
libboost_regex.so.1.74.0 (libc6,x86-64) => /lib/x86_64-linux-gnu/libboost_regex.so.1.74.0
libboost_regex.so (libc6,x86-64) => /usr/local/lib/libboost_regex.so
libboost_random.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_random.so.1.83.0
libboost_random.so (libc6,x86-64) => /usr/local/lib/libboost_random.so
libboost_python311.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_python311.so.1.83.0
libboost_python311.so (libc6,x86-64) => /usr/local/lib/libboost_python311.so
libboost_program_options.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_program_options.so.1.83.0
libboost_program_options.so (libc6,x86-64) => /usr/local/lib/libboost_program_options.so
libboost_prg_exec_monitor.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_prg_exec_monitor.so.1.83.0
libboost_prg_exec_monitor.so (libc6,x86-64) => /usr/local/lib/libboost_prg_exec_monitor.so
libboost_nowide.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_nowide.so.1.83.0
libboost_nowide.so (libc6,x86-64) => /usr/local/lib/libboost_nowide.so
libboost_math_tr1l.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_math_tr1l.so.1.83.0
libboost_math_tr1l.so (libc6,x86-64) => /usr/local/lib/libboost_math_tr1l.so
libboost_math_tr1f.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_math_tr1f.so.1.83.0
libboost_math_tr1f.so (libc6,x86-64) => /usr/local/lib/libboost_math_tr1f.so
libboost_math_tr1.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_math_tr1.so.1.83.0
libboost_math_tr1.so (libc6,x86-64) => /usr/local/lib/libboost_math_tr1.so
libboost_math_c99l.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_math_c99l.so.1.83.0
libboost_math_c99l.so (libc6,x86-64) => /usr/local/lib/libboost_math_c99l.so
libboost_math_c99f.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_math_c99f.so.1.83.0
libboost_math_c99f.so (libc6,x86-64) => /usr/local/lib/libboost_math_c99f.so
libboost_math_c99.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_math_c99.so.1.83.0
libboost_math_c99.so (libc6,x86-64) => /usr/local/lib/libboost_math_c99.so
libboost_log_setup.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_log_setup.so.1.83.0
libboost_log_setup.so (libc6,x86-64) => /usr/local/lib/libboost_log_setup.so
libboost_log.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_log.so.1.83.0
libboost_log.so (libc6,x86-64) => /usr/local/lib/libboost_log.so
libboost_locale.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_locale.so.1.83.0
libboost_locale.so (libc6,x86-64) => /usr/local/lib/libboost_locale.so
libboost_json.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_json.so.1.83.0
libboost_json.so (libc6,x86-64) => /usr/local/lib/libboost_json.so
libboost_iostreams.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_iostreams.so.1.83.0
libboost_iostreams.so (libc6,x86-64) => /usr/local/lib/libboost_iostreams.so
libboost_graph.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_graph.so.1.83.0
libboost_graph.so (libc6,x86-64) => /usr/local/lib/libboost_graph.so
libboost_filesystem.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_filesystem.so.1.83.0
libboost_filesystem.so (libc6,x86-64) => /usr/local/lib/libboost_filesystem.so
libboost_fiber.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_fiber.so.1.83.0
libboost_fiber.so (libc6,x86-64) => /usr/local/lib/libboost_fiber.so
libboost_date_time.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_date_time.so.1.83.0
libboost_date_time.so (libc6,x86-64) => /usr/local/lib/libboost_date_time.so
libboost_coroutine.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_coroutine.so.1.83.0
libboost_coroutine.so (libc6,x86-64) => /usr/local/lib/libboost_coroutine.so
libboost_contract.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_contract.so.1.83.0
libboost_contract.so (libc6,x86-64) => /usr/local/lib/libboost_contract.so
libboost_context.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_context.so.1.83.0
libboost_context.so (libc6,x86-64) => /usr/local/lib/libboost_context.so
libboost_container.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_container.so.1.83.0
libboost_container.so (libc6,x86-64) => /usr/local/lib/libboost_container.so
libboost_chrono.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_chrono.so.1.83.0
libboost_chrono.so (libc6,x86-64) => /usr/local/lib/libboost_chrono.so
libboost_atomic.so.1.83.0 (libc6,x86-64) => /usr/local/lib/libboost_atomic.so.1.83.0
libboost_atomic.so (libc6,x86-64) => /usr/local/lib/libboost_atomic.so </span></pre>
</div>
<!-- endregion -->
<p>
As a further check, examine the full path of an arbitrary include file (<code>src.hpp</code>) and list the Boost libraries:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbbd11fd7aa73'><button class='copyBtn' data-clipboard-target='#idbbd11fd7aa73' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>locate src.hpp
<span class='unselectable'>/usr/include/boost/asio/impl/src.hpp
/usr/include/boost/asio/ssl/impl/src.hpp
/usr/include/boost/beast/src.hpp </span>
<span class='unselectable'>$ </span>find /usr/local/lib/ -iname *boost*
<span class='unselectable'>/usr/local/lib/libboost_math_tr1f.a
/usr/local/lib/libboost_log_setup.so
/usr/local/lib/libboost_system.so.1.83.0
/usr/local/lib/libboost_system.so
/usr/local/lib/libboost_serialization.a
/usr/local/lib/libboost_context.a
/usr/local/lib/libboost_nowide.so
/usr/local/lib/libboost_contract.so.1.83.0
/usr/local/lib/libboost_prg_exec_monitor.a
/usr/local/lib/libboost_stacktrace_backtrace.so
/usr/local/lib/libboost_iostreams.so
/usr/local/lib/libboost_math_c99.so.1.83.0
/usr/local/lib/libboost_stacktrace_addr2line.so
/usr/local/lib/libboost_locale.so.1.83.0
/usr/local/lib/libboost_math_c99l.so.1.83.0
/usr/local/lib/libboost_timer.a
/usr/local/lib/libboost_atomic.so.1.83.0
/usr/local/lib/libboost_url.so.1.83.0
/usr/local/lib/libboost_math_c99.a
/usr/local/lib/libboost_date_time.so
/usr/local/lib/libboost_fiber.a
/usr/local/lib/libboost_filesystem.so.1.83.0
/usr/local/lib/libboost_date_time.a
/usr/local/lib/libboost_log_setup.a
/usr/local/lib/libboost_type_erasure.so
/usr/local/lib/libboost_json.so.1.83.0
/usr/local/lib/libboost_wave.so.1.83.0
/usr/local/lib/libboost_unit_test_framework.so
/usr/local/lib/libboost_type_erasure.so.1.83.0
/usr/local/lib/libboost_random.so.1.83.0
/usr/local/lib/libboost_filesystem.so
/usr/local/lib/libboost_stacktrace_basic.a
/usr/local/lib/libboost_math_c99.so
/usr/local/lib/libboost_regex.a
/usr/local/lib/libboost_coroutine.so.1.83.0
/usr/local/lib/libboost_filesystem.a
/usr/local/lib/libboost_date_time.so.1.83.0
/usr/local/lib/libboost_wave.a
/usr/local/lib/libboost_regex.so
/usr/local/lib/libboost_container.a
/usr/local/lib/libboost_locale.a
/usr/local/lib/libboost_graph.so
/usr/local/lib/libboost_graph.so.1.83.0
/usr/local/lib/libboost_math_tr1f.so.1.83.0
/usr/local/lib/libboost_math_tr1.so.1.83.0
/usr/local/lib/libboost_program_options.so.1.83.0
/usr/local/lib/libboost_iostreams.so.1.83.0
/usr/local/lib/libboost_math_c99f.so.1.83.0
/usr/local/lib/libboost_nowide.so.1.83.0
/usr/local/lib/libboost_thread.so.1.83.0
/usr/local/lib/libboost_unit_test_framework.a
/usr/local/lib/cmake/boost_math_c99f-1.83.0
/usr/local/lib/cmake/boost_math_c99f-1.83.0/libboost_math_c99f-variant-shared.cmake
/usr/local/lib/cmake/boost_math_c99f-1.83.0/boost_math_c99f-config.cmake
/usr/local/lib/cmake/boost_math_c99f-1.83.0/libboost_math_c99f-variant-static.cmake
/usr/local/lib/cmake/boost_math_c99f-1.83.0/boost_math_c99f-config-version.cmake
/usr/local/lib/cmake/boost_test_exec_monitor-1.83.0
/usr/local/lib/cmake/boost_test_exec_monitor-1.83.0/libboost_test_exec_monitor-variant-static.cmake
/usr/local/lib/cmake/boost_test_exec_monitor-1.83.0/libboost_test_exec_monitor-variant-shared.cmake
/usr/local/lib/cmake/boost_test_exec_monitor-1.83.0/boost_test_exec_monitor-config.cmake
/usr/local/lib/cmake/boost_test_exec_monitor-1.83.0/boost_test_exec_monitor-config-version.cmake
/usr/local/lib/cmake/boost_stacktrace_backtrace-1.83.0
/usr/local/lib/cmake/boost_stacktrace_backtrace-1.83.0/libboost_stacktrace_backtrace-variant-shared.cmake
/usr/local/lib/cmake/boost_stacktrace_backtrace-1.83.0/boost_stacktrace_backtrace-config-version.cmake
/usr/local/lib/cmake/boost_stacktrace_backtrace-1.83.0/libboost_stacktrace_backtrace-variant-static.cmake
/usr/local/lib/cmake/boost_stacktrace_backtrace-1.83.0/boost_stacktrace_backtrace-config.cmake
/usr/local/lib/cmake/boost_wave-1.83.0
/usr/local/lib/cmake/boost_wave-1.83.0/libboost_wave-variant-shared.cmake
/usr/local/lib/cmake/boost_wave-1.83.0/boost_wave-config-version.cmake
/usr/local/lib/cmake/boost_wave-1.83.0/libboost_wave-variant-static.cmake
/usr/local/lib/cmake/boost_wave-1.83.0/boost_wave-config.cmake
/usr/local/lib/cmake/boost_log-1.83.0
/usr/local/lib/cmake/boost_log-1.83.0/libboost_log-variant-shared.cmake
/usr/local/lib/cmake/boost_log-1.83.0/boost_log-config.cmake
/usr/local/lib/cmake/boost_log-1.83.0/libboost_log-variant-static.cmake
/usr/local/lib/cmake/boost_log-1.83.0/boost_log-config-version.cmake
/usr/local/lib/cmake/boost_stacktrace_basic-1.83.0
/usr/local/lib/cmake/boost_stacktrace_basic-1.83.0/boost_stacktrace_basic-config.cmake
/usr/local/lib/cmake/boost_stacktrace_basic-1.83.0/boost_stacktrace_basic-config-version.cmake
/usr/local/lib/cmake/boost_stacktrace_basic-1.83.0/libboost_stacktrace_basic-variant-static.cmake
/usr/local/lib/cmake/boost_stacktrace_basic-1.83.0/libboost_stacktrace_basic-variant-shared.cmake
/usr/local/lib/cmake/boost_coroutine-1.83.0
/usr/local/lib/cmake/boost_coroutine-1.83.0/boost_coroutine-config-version.cmake
/usr/local/lib/cmake/boost_coroutine-1.83.0/boost_coroutine-config.cmake
/usr/local/lib/cmake/boost_coroutine-1.83.0/libboost_coroutine-variant-shared.cmake
/usr/local/lib/cmake/boost_coroutine-1.83.0/libboost_coroutine-variant-static.cmake
/usr/local/lib/cmake/boost_timer-1.83.0
/usr/local/lib/cmake/boost_timer-1.83.0/boost_timer-config-version.cmake
/usr/local/lib/cmake/boost_timer-1.83.0/boost_timer-config.cmake
/usr/local/lib/cmake/boost_timer-1.83.0/libboost_timer-variant-shared.cmake
/usr/local/lib/cmake/boost_timer-1.83.0/libboost_timer-variant-static.cmake
/usr/local/lib/cmake/boost_program_options-1.83.0
/usr/local/lib/cmake/boost_program_options-1.83.0/boost_program_options-config.cmake
/usr/local/lib/cmake/boost_program_options-1.83.0/libboost_program_options-variant-static.cmake
/usr/local/lib/cmake/boost_program_options-1.83.0/libboost_program_options-variant-shared.cmake
/usr/local/lib/cmake/boost_program_options-1.83.0/boost_program_options-config-version.cmake
/usr/local/lib/cmake/boost_math_tr1f-1.83.0
/usr/local/lib/cmake/boost_math_tr1f-1.83.0/libboost_math_tr1f-variant-static.cmake
/usr/local/lib/cmake/boost_math_tr1f-1.83.0/boost_math_tr1f-config.cmake
/usr/local/lib/cmake/boost_math_tr1f-1.83.0/boost_math_tr1f-config-version.cmake
/usr/local/lib/cmake/boost_math_tr1f-1.83.0/libboost_math_tr1f-variant-shared.cmake
/usr/local/lib/cmake/boost_date_time-1.83.0
/usr/local/lib/cmake/boost_date_time-1.83.0/libboost_date_time-variant-static.cmake
/usr/local/lib/cmake/boost_date_time-1.83.0/libboost_date_time-variant-shared.cmake
/usr/local/lib/cmake/boost_date_time-1.83.0/boost_date_time-config.cmake
/usr/local/lib/cmake/boost_date_time-1.83.0/boost_date_time-config-version.cmake
/usr/local/lib/cmake/boost_math_c99l-1.83.0
/usr/local/lib/cmake/boost_math_c99l-1.83.0/libboost_math_c99l-variant-static.cmake
/usr/local/lib/cmake/boost_math_c99l-1.83.0/libboost_math_c99l-variant-shared.cmake
/usr/local/lib/cmake/boost_math_c99l-1.83.0/boost_math_c99l-config.cmake
/usr/local/lib/cmake/boost_math_c99l-1.83.0/boost_math_c99l-config-version.cmake
/usr/local/lib/cmake/boost_prg_exec_monitor-1.83.0
/usr/local/lib/cmake/boost_prg_exec_monitor-1.83.0/libboost_prg_exec_monitor-variant-shared.cmake
/usr/local/lib/cmake/boost_prg_exec_monitor-1.83.0/libboost_prg_exec_monitor-variant-static.cmake
/usr/local/lib/cmake/boost_prg_exec_monitor-1.83.0/boost_prg_exec_monitor-config.cmake
/usr/local/lib/cmake/boost_prg_exec_monitor-1.83.0/boost_prg_exec_monitor-config-version.cmake
/usr/local/lib/cmake/boost_wserialization-1.83.0
/usr/local/lib/cmake/boost_wserialization-1.83.0/libboost_wserialization-variant-shared.cmake
/usr/local/lib/cmake/boost_wserialization-1.83.0/boost_wserialization-config-version.cmake
/usr/local/lib/cmake/boost_wserialization-1.83.0/libboost_wserialization-variant-static.cmake
/usr/local/lib/cmake/boost_wserialization-1.83.0/boost_wserialization-config.cmake
/usr/local/lib/cmake/boost_serialization-1.83.0
/usr/local/lib/cmake/boost_serialization-1.83.0/boost_serialization-config.cmake
/usr/local/lib/cmake/boost_serialization-1.83.0/libboost_serialization-variant-static.cmake
/usr/local/lib/cmake/boost_serialization-1.83.0/boost_serialization-config-version.cmake
/usr/local/lib/cmake/boost_serialization-1.83.0/libboost_serialization-variant-shared.cmake
/usr/local/lib/cmake/boost_random-1.83.0
/usr/local/lib/cmake/boost_random-1.83.0/boost_random-config.cmake
/usr/local/lib/cmake/boost_random-1.83.0/libboost_random-variant-static.cmake
/usr/local/lib/cmake/boost_random-1.83.0/libboost_random-variant-shared.cmake
/usr/local/lib/cmake/boost_random-1.83.0/boost_random-config-version.cmake
/usr/local/lib/cmake/boost_log_setup-1.83.0
/usr/local/lib/cmake/boost_log_setup-1.83.0/boost_log_setup-config-version.cmake
/usr/local/lib/cmake/boost_log_setup-1.83.0/libboost_log_setup-variant-shared.cmake
/usr/local/lib/cmake/boost_log_setup-1.83.0/libboost_log_setup-variant-static.cmake
/usr/local/lib/cmake/boost_log_setup-1.83.0/boost_log_setup-config.cmake
/usr/local/lib/cmake/boost_exception-1.83.0
/usr/local/lib/cmake/boost_exception-1.83.0/boost_exception-config.cmake
/usr/local/lib/cmake/boost_exception-1.83.0/boost_exception-config-version.cmake
/usr/local/lib/cmake/boost_system-1.83.0
/usr/local/lib/cmake/boost_system-1.83.0/libboost_system-variant-shared.cmake
/usr/local/lib/cmake/boost_system-1.83.0/boost_system-config-version.cmake
/usr/local/lib/cmake/boost_system-1.83.0/libboost_system-variant-static.cmake
/usr/local/lib/cmake/boost_system-1.83.0/boost_system-config.cmake
/usr/local/lib/cmake/boost_python-1.83.0
/usr/local/lib/cmake/boost_python-1.83.0/boost_python-config.cmake
/usr/local/lib/cmake/boost_python-1.83.0/boost_python-config-version.cmake
/usr/local/lib/cmake/boost_thread-1.83.0
/usr/local/lib/cmake/boost_thread-1.83.0/libboost_thread-variant-shared.cmake
/usr/local/lib/cmake/boost_thread-1.83.0/boost_thread-config.cmake
/usr/local/lib/cmake/boost_thread-1.83.0/boost_thread-config-version.cmake
/usr/local/lib/cmake/boost_thread-1.83.0/libboost_thread-variant-static.cmake
/usr/local/lib/cmake/boost_json-1.83.0
/usr/local/lib/cmake/boost_json-1.83.0/libboost_json-variant-shared.cmake
/usr/local/lib/cmake/boost_json-1.83.0/libboost_json-variant-static.cmake
/usr/local/lib/cmake/boost_json-1.83.0/boost_json-config.cmake
/usr/local/lib/cmake/boost_json-1.83.0/boost_json-config-version.cmake
/usr/local/lib/cmake/boost_iostreams-1.83.0
/usr/local/lib/cmake/boost_iostreams-1.83.0/libboost_iostreams-variant-shared.cmake
/usr/local/lib/cmake/boost_iostreams-1.83.0/boost_iostreams-config.cmake
/usr/local/lib/cmake/boost_iostreams-1.83.0/libboost_iostreams-variant-static.cmake
/usr/local/lib/cmake/boost_iostreams-1.83.0/boost_iostreams-config-version.cmake
/usr/local/lib/cmake/boost_math_c99-1.83.0
/usr/local/lib/cmake/boost_math_c99-1.83.0/libboost_math_c99-variant-static.cmake
/usr/local/lib/cmake/boost_math_c99-1.83.0/boost_math_c99-config-version.cmake
/usr/local/lib/cmake/boost_math_c99-1.83.0/libboost_math_c99-variant-shared.cmake
/usr/local/lib/cmake/boost_math_c99-1.83.0/boost_math_c99-config.cmake
/usr/local/lib/cmake/boost_unit_test_framework-1.83.0
/usr/local/lib/cmake/boost_unit_test_framework-1.83.0/libboost_unit_test_framework-variant-static.cmake
/usr/local/lib/cmake/boost_unit_test_framework-1.83.0/boost_unit_test_framework-config-version.cmake
/usr/local/lib/cmake/boost_unit_test_framework-1.83.0/libboost_unit_test_framework-variant-shared.cmake
/usr/local/lib/cmake/boost_unit_test_framework-1.83.0/boost_unit_test_framework-config.cmake
/usr/local/lib/cmake/boost_filesystem-1.83.0
/usr/local/lib/cmake/boost_filesystem-1.83.0/libboost_filesystem-variant-static.cmake
/usr/local/lib/cmake/boost_filesystem-1.83.0/boost_filesystem-config.cmake
/usr/local/lib/cmake/boost_filesystem-1.83.0/boost_filesystem-config-version.cmake
/usr/local/lib/cmake/boost_filesystem-1.83.0/libboost_filesystem-variant-shared.cmake
/usr/local/lib/cmake/Boost-1.83.0
/usr/local/lib/cmake/Boost-1.83.0/BoostConfigVersion.cmake
/usr/local/lib/cmake/Boost-1.83.0/BoostConfig.cmake
/usr/local/lib/cmake/boost_headers-1.83.0
/usr/local/lib/cmake/boost_headers-1.83.0/boost_headers-config.cmake
/usr/local/lib/cmake/boost_headers-1.83.0/boost_headers-config-version.cmake
/usr/local/lib/cmake/boost_nowide-1.83.0
/usr/local/lib/cmake/boost_nowide-1.83.0/libboost_nowide-variant-static.cmake
/usr/local/lib/cmake/boost_nowide-1.83.0/libboost_nowide-variant-shared.cmake
/usr/local/lib/cmake/boost_nowide-1.83.0/boost_nowide-config.cmake
/usr/local/lib/cmake/boost_nowide-1.83.0/boost_nowide-config-version.cmake
/usr/local/lib/cmake/boost_atomic-1.83.0
/usr/local/lib/cmake/boost_atomic-1.83.0/boost_atomic-config-version.cmake
/usr/local/lib/cmake/boost_atomic-1.83.0/boost_atomic-config.cmake
/usr/local/lib/cmake/boost_atomic-1.83.0/libboost_atomic-variant-shared.cmake
/usr/local/lib/cmake/boost_atomic-1.83.0/libboost_atomic-variant-static.cmake
/usr/local/lib/cmake/boost_graph-1.83.0
/usr/local/lib/cmake/boost_graph-1.83.0/boost_graph-config-version.cmake
/usr/local/lib/cmake/boost_graph-1.83.0/libboost_graph-variant-static.cmake
/usr/local/lib/cmake/boost_graph-1.83.0/libboost_graph-variant-shared.cmake
/usr/local/lib/cmake/boost_graph-1.83.0/boost_graph-config.cmake
/usr/local/lib/cmake/boost_math_tr1-1.83.0
/usr/local/lib/cmake/boost_math_tr1-1.83.0/boost_math_tr1-config-version.cmake
/usr/local/lib/cmake/boost_math_tr1-1.83.0/libboost_math_tr1-variant-static.cmake
/usr/local/lib/cmake/boost_math_tr1-1.83.0/libboost_math_tr1-variant-shared.cmake
/usr/local/lib/cmake/boost_math_tr1-1.83.0/boost_math_tr1-config.cmake
/usr/local/lib/cmake/boost_graph_parallel-1.83.0
/usr/local/lib/cmake/boost_graph_parallel-1.83.0/boost_graph_parallel-config-version.cmake
/usr/local/lib/cmake/boost_graph_parallel-1.83.0/boost_graph_parallel-config.cmake
/usr/local/lib/cmake/boost_stacktrace_noop-1.83.0
/usr/local/lib/cmake/boost_stacktrace_noop-1.83.0/libboost_stacktrace_noop-variant-static.cmake
/usr/local/lib/cmake/boost_stacktrace_noop-1.83.0/boost_stacktrace_noop-config-version.cmake
/usr/local/lib/cmake/boost_stacktrace_noop-1.83.0/boost_stacktrace_noop-config.cmake
/usr/local/lib/cmake/boost_stacktrace_noop-1.83.0/libboost_stacktrace_noop-variant-shared.cmake
/usr/local/lib/cmake/boost_mpi-1.83.0
/usr/local/lib/cmake/boost_mpi-1.83.0/boost_mpi-config-version.cmake
/usr/local/lib/cmake/boost_mpi-1.83.0/boost_mpi-config.cmake
/usr/local/lib/cmake/boost_type_erasure-1.83.0
/usr/local/lib/cmake/boost_type_erasure-1.83.0/libboost_type_erasure-variant-shared.cmake
/usr/local/lib/cmake/boost_type_erasure-1.83.0/libboost_type_erasure-variant-static.cmake
/usr/local/lib/cmake/boost_type_erasure-1.83.0/boost_type_erasure-config-version.cmake
/usr/local/lib/cmake/boost_type_erasure-1.83.0/boost_type_erasure-config.cmake
/usr/local/lib/cmake/boost_chrono-1.83.0
/usr/local/lib/cmake/boost_chrono-1.83.0/boost_chrono-config-version.cmake
/usr/local/lib/cmake/boost_chrono-1.83.0/libboost_chrono-variant-static.cmake
/usr/local/lib/cmake/boost_chrono-1.83.0/libboost_chrono-variant-shared.cmake
/usr/local/lib/cmake/boost_chrono-1.83.0/boost_chrono-config.cmake
/usr/local/lib/cmake/boost_math_tr1l-1.83.0
/usr/local/lib/cmake/boost_math_tr1l-1.83.0/libboost_math_tr1l-variant-shared.cmake
/usr/local/lib/cmake/boost_math_tr1l-1.83.0/boost_math_tr1l-config.cmake
/usr/local/lib/cmake/boost_math_tr1l-1.83.0/boost_math_tr1l-config-version.cmake
/usr/local/lib/cmake/boost_math_tr1l-1.83.0/libboost_math_tr1l-variant-static.cmake
/usr/local/lib/cmake/boost_contract-1.83.0
/usr/local/lib/cmake/boost_contract-1.83.0/libboost_contract-variant-shared.cmake
/usr/local/lib/cmake/boost_contract-1.83.0/boost_contract-config-version.cmake
/usr/local/lib/cmake/boost_contract-1.83.0/boost_contract-config.cmake
/usr/local/lib/cmake/boost_contract-1.83.0/libboost_contract-variant-static.cmake
/usr/local/lib/cmake/boost_fiber-1.83.0
/usr/local/lib/cmake/boost_fiber-1.83.0/libboost_fiber-variant-static.cmake
/usr/local/lib/cmake/boost_fiber-1.83.0/boost_fiber-config-version.cmake
/usr/local/lib/cmake/boost_fiber-1.83.0/libboost_fiber-variant-shared.cmake
/usr/local/lib/cmake/boost_fiber-1.83.0/boost_fiber-config.cmake
/usr/local/lib/cmake/BoostDetectToolset-1.83.0.cmake
/usr/local/lib/cmake/boost_context-1.83.0
/usr/local/lib/cmake/boost_context-1.83.0/boost_context-config.cmake
/usr/local/lib/cmake/boost_context-1.83.0/libboost_context-variant-shared.cmake
/usr/local/lib/cmake/boost_context-1.83.0/libboost_context-variant-static.cmake
/usr/local/lib/cmake/boost_context-1.83.0/boost_context-config-version.cmake
/usr/local/lib/cmake/boost_container-1.83.0
/usr/local/lib/cmake/boost_container-1.83.0/libboost_container-variant-shared.cmake
/usr/local/lib/cmake/boost_container-1.83.0/boost_container-config-version.cmake
/usr/local/lib/cmake/boost_container-1.83.0/boost_container-config.cmake
/usr/local/lib/cmake/boost_container-1.83.0/libboost_container-variant-static.cmake
/usr/local/lib/cmake/boost_regex-1.83.0
/usr/local/lib/cmake/boost_regex-1.83.0/libboost_regex-variant-shared.cmake
/usr/local/lib/cmake/boost_regex-1.83.0/boost_regex-config.cmake
/usr/local/lib/cmake/boost_regex-1.83.0/libboost_regex-variant-static.cmake
/usr/local/lib/cmake/boost_regex-1.83.0/boost_regex-config-version.cmake
/usr/local/lib/cmake/boost_url-1.83.0
/usr/local/lib/cmake/boost_url-1.83.0/boost_url-config-version.cmake
/usr/local/lib/cmake/boost_url-1.83.0/libboost_url-variant-static.cmake
/usr/local/lib/cmake/boost_url-1.83.0/libboost_url-variant-shared.cmake
/usr/local/lib/cmake/boost_url-1.83.0/boost_url-config.cmake
/usr/local/lib/cmake/boost_locale-1.83.0
/usr/local/lib/cmake/boost_locale-1.83.0/libboost_locale-variant-static.cmake
/usr/local/lib/cmake/boost_locale-1.83.0/boost_locale-config.cmake
/usr/local/lib/cmake/boost_locale-1.83.0/libboost_locale-variant-shared.cmake
/usr/local/lib/cmake/boost_locale-1.83.0/boost_locale-config-version.cmake
/usr/local/lib/cmake/boost_stacktrace_addr2line-1.83.0
/usr/local/lib/cmake/boost_stacktrace_addr2line-1.83.0/libboost_stacktrace_addr2line-variant-static.cmake
/usr/local/lib/cmake/boost_stacktrace_addr2line-1.83.0/libboost_stacktrace_addr2line-variant-shared.cmake
/usr/local/lib/cmake/boost_stacktrace_addr2line-1.83.0/boost_stacktrace_addr2line-config-version.cmake
/usr/local/lib/cmake/boost_stacktrace_addr2line-1.83.0/boost_stacktrace_addr2line-config.cmake
/usr/local/lib/libboost_fiber.so.1.83.0
/usr/local/lib/libboost_chrono.a
/usr/local/lib/libboost_serialization.so
/usr/local/lib/libboost_fiber.so
/usr/local/lib/libboost_wserialization.a
/usr/local/lib/libboost_unit_test_framework.so.1.83.0
/usr/local/lib/libboost_stacktrace_noop.a
/usr/local/lib/libboost_iostreams.a
/usr/local/lib/libboost_math_c99f.a
/usr/local/lib/libboost_exception.a
/usr/local/lib/libboost_type_erasure.a
/usr/local/lib/libboost_thread.a
/usr/local/lib/libboost_chrono.so.1.83.0
/usr/local/lib/libboost_math_c99l.so
/usr/local/lib/libboost_log_setup.so.1.83.0
/usr/local/lib/libboost_stacktrace_addr2line.a
/usr/local/lib/libboost_container.so.1.83.0
/usr/local/lib/libboost_context.so
/usr/local/lib/libboost_program_options.so
/usr/local/lib/libboost_stacktrace_basic.so
/usr/local/lib/libboost_graph.a
/usr/local/lib/libboost_prg_exec_monitor.so.1.83.0
/usr/local/lib/libboost_atomic.a
/usr/local/lib/libboost_math_tr1l.a
/usr/local/lib/libboost_log.so.1.83.0
/usr/local/lib/libboost_random.a
/usr/local/lib/libboost_math_tr1f.so
/usr/local/lib/libboost_contract.a
/usr/local/lib/libboost_atomic.so
/usr/local/lib/libboost_serialization.so.1.83.0
/usr/local/lib/libboost_thread.so
/usr/local/lib/libboost_wave.so
/usr/local/lib/libboost_program_options.a
/usr/local/lib/libboost_context.so.1.83.0
/usr/local/lib/libboost_coroutine.a
/usr/local/lib/libboost_json.a
/usr/local/lib/libboost_stacktrace_noop.so.1.83.0
/usr/local/lib/libboost_wserialization.so.1.83.0
/usr/local/lib/libboost_math_tr1.a
/usr/local/lib/libboost_chrono.so
/usr/local/lib/libboost_stacktrace_backtrace.a
/usr/local/lib/libboost_stacktrace_addr2line.so.1.83.0
/usr/local/lib/libboost_stacktrace_basic.so.1.83.0
/usr/local/lib/libboost_math_c99l.a
/usr/local/lib/libboost_log.so
/usr/local/lib/libboost_log.a
/usr/local/lib/libboost_math_tr1l.so
/usr/local/lib/libboost_container.so
/usr/local/lib/libboost_coroutine.so
/usr/local/lib/libboost_prg_exec_monitor.so
/usr/local/lib/libboost_nowide.a
/usr/local/lib/libboost_url.so
/usr/local/lib/libboost_url.a
/usr/local/lib/libboost_math_c99f.so
/usr/local/lib/libboost_stacktrace_backtrace.so.1.83.0
/usr/local/lib/libboost_test_exec_monitor.a
/usr/local/lib/libboost_json.so
/usr/local/lib/libboost_wserialization.so
/usr/local/lib/libboost_system.a
/usr/local/lib/libboost_regex.so.1.83.0
/usr/local/lib/libboost_contract.so
/usr/local/lib/libboost_stacktrace_noop.so
/usr/local/lib/libboost_locale.so
/usr/local/lib/libboost_math_tr1.so
/usr/local/lib/libboost_timer.so.1.83.0
/usr/local/lib/libboost_random.so
/usr/local/lib/libboost_math_tr1l.so.1.83.0
/usr/local/lib/libboost_timer.so </span></pre>
</div>
<!-- endregion -->
<!-- #region clean up -->
<h3 id="clean">Clean Up</h3>
<p>
Many Ubuntu/Debian packages are left behind by the Boost installation program.
You can view them as follows:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id939f18cccb39'><button class='copyBtn' data-clipboard-target='#id939f18cccb39' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>apt --dry-run autoremove | grep -Po '^Remv \K[^ ]+'
<span class='unselectable'>libboost-mpi-python-dev
libboost-mpi-python1.74-dev
libboost-mpi-python1.74.0
mpi-default-bin
libboost-mpi-dev
libboost-mpi1.74-dev
mpi-default-dev
libopenmpi-dev
openmpi-bin
libcoarrays-openmpi-dev
libopenmpi3
libucx0
libfabric1
ibverbs-providers
libboost-atomic-dev
libboost-coroutine-dev
libboost-coroutine1.74-dev
libboost-fiber-dev
libboost-fiber1.74-dev
libboost-context1.74-dev
libboost-type-erasure-dev
libboost-type-erasure1.74-dev
libboost-thread1.74-dev
libboost-log-dev
libboost-log1.74-dev
libboost-atomic1.74-dev
libboost-atomic1.74.0
libboost-chrono-dev
libboost-timer-dev
libboost-timer1.74-dev
libboost-chrono1.74-dev
libboost-timer1.74.0
libboost-chrono1.74.0
libboost-container-dev
libboost-container1.74-dev
libboost-container1.74.0
libboost-context-dev
libboost-fiber1.74.0
libboost-coroutine1.74.0
libboost-context1.74.0
libboost-date-time-dev
libboost-date-time1.74-dev
libboost-date-time1.74.0
libboost-dev
libboost-exception-dev
libboost-exception1.74-dev
libboost-filesystem-dev
libboost-wave-dev
libboost-wave1.74-dev
libboost-filesystem1.74-dev
libboost-graph-dev
libboost-graph-parallel-dev
libboost-graph-parallel1.74-dev
libboost-graph-parallel1.74.0
libboost-graph1.74-dev
libboost-graph1.74.0
libboost-iostreams-dev
libboost-iostreams1.74-dev
libboost-locale-dev
libboost-locale1.74-dev
libboost-log1.74.0
libboost-math-dev
libboost-math1.74-dev
libboost-math1.74.0
libboost-mpi1.74.0
libboost-nowide-dev
libboost-nowide1.74-dev
libboost-nowide1.74.0
libboost-numpy-dev
libboost-numpy1.74-dev
libboost-numpy1.74.0
libboost-program-options-dev
libboost-program-options1.74-dev
libboost-program-options1.74.0
libboost-python-dev
libboost-python1.74-dev
libboost-python1.74.0
libboost-random-dev
libboost-random1.74-dev
libboost-random1.74.0
libboost-regex-dev
libboost-regex1.74-dev
libboost-serialization-dev
libboost-serialization1.74-dev
libboost-serialization1.74.0
libboost-stacktrace-dev
libboost-stacktrace1.74-dev
libboost-stacktrace1.74.0
libboost-system-dev
libboost-system1.74-dev
libboost-system1.74.0
libboost-test-dev
libboost-test1.74-dev
libboost-test1.74.0
libboost-thread-dev
libboost-tools-dev
libboost-type-erasure1.74.0
libboost-wave1.74.0
libboost1.74-dev
libboost1.74-tools-dev
libcaf-openmpi-3
libpmix-dev
libevent-dev
libevent-extra-2.1-7
libevent-openssl-2.1-7
libpmix2
libevent-pthreads-2.1-7
libhwloc-dev
libhwloc-plugins
libhwloc15
libibverbs-dev
librdmacm1
libibverbs1
libjs-jquery-ui
libmunge2
libnl-route-3-dev
libnl-3-dev
libnuma-dev
libpsm-infinipath1
libpsm2-2
openmpi-common </span></pre>
</div>
<!-- endregion -->
<p>
Remove the packages as follows:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb6ee09f3d44d'><button class='copyBtn' data-clipboard-target='#idb6ee09f3d44d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt autoremove
<span class='unselectable'>Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following packages will be REMOVED:
ibverbs-providers libboost-atomic-dev libboost-atomic1.74-dev libboost-atomic1.74.0 libboost-chrono-dev libboost-chrono1.74-dev libboost-chrono1.74.0
libboost-container-dev libboost-container1.74-dev libboost-container1.74.0 libboost-context-dev libboost-context1.74-dev libboost-context1.74.0
libboost-coroutine-dev libboost-coroutine1.74-dev libboost-coroutine1.74.0 libboost-date-time-dev libboost-date-time1.74-dev libboost-date-time1.74.0
libboost-dev libboost-exception-dev libboost-exception1.74-dev libboost-fiber-dev libboost-fiber1.74-dev libboost-fiber1.74.0 libboost-filesystem-dev
libboost-filesystem1.74-dev libboost-graph-dev libboost-graph-parallel-dev libboost-graph-parallel1.74-dev libboost-graph-parallel1.74.0
libboost-graph1.74-dev libboost-graph1.74.0 libboost-iostreams-dev libboost-iostreams1.74-dev libboost-locale-dev libboost-locale1.74-dev
libboost-log-dev libboost-log1.74-dev libboost-log1.74.0 libboost-math-dev libboost-math1.74-dev libboost-math1.74.0 libboost-mpi-dev
libboost-mpi-python-dev libboost-mpi-python1.74-dev libboost-mpi-python1.74.0 libboost-mpi1.74-dev libboost-mpi1.74.0 libboost-nowide-dev
libboost-nowide1.74-dev libboost-nowide1.74.0 libboost-numpy-dev libboost-numpy1.74-dev libboost-numpy1.74.0 libboost-program-options-dev
libboost-program-options1.74-dev libboost-program-options1.74.0 libboost-python-dev libboost-python1.74-dev libboost-python1.74.0 libboost-random-dev
libboost-random1.74-dev libboost-random1.74.0 libboost-regex-dev libboost-regex1.74-dev libboost-serialization-dev libboost-serialization1.74-dev
libboost-serialization1.74.0 libboost-stacktrace-dev libboost-stacktrace1.74-dev libboost-stacktrace1.74.0 libboost-system-dev libboost-system1.74-dev
libboost-system1.74.0 libboost-test-dev libboost-test1.74-dev libboost-test1.74.0 libboost-thread-dev libboost-thread1.74-dev libboost-timer-dev
libboost-timer1.74-dev libboost-timer1.74.0 libboost-tools-dev libboost-type-erasure-dev libboost-type-erasure1.74-dev libboost-type-erasure1.74.0
libboost-wave-dev libboost-wave1.74-dev libboost-wave1.74.0 libboost1.74-dev libboost1.74-tools-dev libcaf-openmpi-3 libcoarrays-openmpi-dev libevent-dev
libevent-extra-2.1-7 libevent-openssl-2.1-7 libevent-pthreads-2.1-7 libfabric1 libhwloc-dev libhwloc-plugins libhwloc15 libibverbs-dev libibverbs1
libjs-jquery-ui libmunge2 libnl-3-dev libnl-route-3-dev libnuma-dev libopenmpi-dev libopenmpi3 libpmix-dev libpmix2 libpsm-infinipath1 libpsm2-2
librdmacm1 libucx0 mpi-default-bin mpi-default-dev openmpi-bin openmpi-common
0 upgraded, 0 newly installed, 121 to remove and 4 not upgraded.
After this operation, 413 MB disk space will be freed.
(Reading database ... 399799 files and directories currently installed.)
Removing libboost-mpi-python-dev (1.74.0.3ubuntu7) ...
Removing libboost-mpi-python1.74-dev (1.74.0-18.1ubuntu3) ...
Removing libboost-mpi-python1.74.0 (1.74.0-18.1ubuntu3) ...
Removing mpi-default-bin (1.14) ...
Removing libboost-mpi-dev (1.74.0.3ubuntu7) ...
Removing libboost-mpi1.74-dev (1.74.0-18.1ubuntu3) ...
Removing mpi-default-dev (1.14) ...
Removing libopenmpi-dev:amd64 (4.1.4-3ubuntu2) ...
Removing libcoarrays-openmpi-dev:amd64 (2.10.1-1) ...
update-alternatives: warning: alternative /usr/bin/caf.openmpi (part of link group caf) doesn't exist; removing from list of alternatives
update-alternatives: warning: /etc/alternatives/caf is dangling; it will be updated with best choice
Removing libboost-atomic-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-coroutine-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-coroutine1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-fiber-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-fiber1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-type-erasure-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-type-erasure1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-log-dev (1.74.0.3ubuntu7) ...
Removing libboost-log1.74-dev (1.74.0-18.1ubuntu3) ...
Removing libboost-chrono-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-timer-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-timer1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-timer1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-container-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-container1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-container1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-context-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-fiber1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-coroutine1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-date-time-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-exception-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-exception1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-filesystem-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-wave-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-wave1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-filesystem1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-graph-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-graph-parallel-dev (1.74.0.3ubuntu7) ...
Removing libboost-graph-parallel1.74-dev (1.74.0-18.1ubuntu3) ...
Removing libboost-graph-parallel1.74.0 (1.74.0-18.1ubuntu3) ...
Removing libboost-graph1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-graph1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-iostreams-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-iostreams1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-locale-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-locale1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-log1.74.0 (1.74.0-18.1ubuntu3) ...
Removing libboost-math-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-math1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-math1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-mpi1.74.0 (1.74.0-18.1ubuntu3) ...
Removing libboost-nowide-dev (1.74.0.3ubuntu7) ...
Removing libboost-nowide1.74-dev (1.74.0-18.1ubuntu3) ...
Removing libboost-nowide1.74.0 (1.74.0-18.1ubuntu3) ...
Removing libboost-numpy-dev (1.74.0.3ubuntu7) ...
Removing libboost-numpy1.74-dev (1.74.0-18.1ubuntu3) ...
Removing libboost-numpy1.74.0 (1.74.0-18.1ubuntu3) ...
Removing libboost-program-options-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-program-options1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-program-options1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-python-dev (1.74.0.3ubuntu7) ...
Removing libboost-python1.74-dev (1.74.0-18.1ubuntu3) ...
Removing libboost-python1.74.0 (1.74.0-18.1ubuntu3) ...
Removing libboost-random-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-random1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-random1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-regex-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-regex1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-serialization-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-stacktrace-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-stacktrace1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-stacktrace1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-system-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-test-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-test1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-test1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-thread-dev:amd64 (1.74.0.3ubuntu7) ...
Removing libboost-tools-dev (1.74.0.3ubuntu7) ...
Removing libboost-type-erasure1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-wave1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost1.74-tools-dev (1.74.0-18.1ubuntu3) ...
Removing libcaf-openmpi-3:amd64 (2.10.1-1) ...
Removing libpmix-dev:amd64 (4.2.2-1) ...
Removing libevent-dev (2.1.12-stable-8ubuntu3) ...
Removing libevent-extra-2.1-7:amd64 (2.1.12-stable-8ubuntu3) ...
Removing libevent-openssl-2.1-7:amd64 (2.1.12-stable-8ubuntu3) ...
Removing libhwloc-dev:amd64 (2.9.0-1) ...
Removing libibverbs-dev:amd64 (44.0-2) ...
Removing libjs-jquery-ui (1.13.2+dfsg-1) ...
Removing libnl-route-3-dev:amd64 (3.7.0-0.2) ...
Removing libnl-3-dev:amd64 (3.7.0-0.2) ...
Removing libnuma-dev:amd64 (2.0.16-1) ...
Removing openmpi-bin (4.1.4-3ubuntu2) ...
Removing libopenmpi3:amd64 (4.1.4-3ubuntu2) ...
Removing libucx0:amd64 (1.13.1-1) ...
Removing libfabric1:amd64 (1.17.0-3) ...
Removing ibverbs-providers:amd64 (44.0-2) ...
Removing libboost-context1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-thread1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-atomic1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-atomic1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-chrono1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-chrono1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-context1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-date-time1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-date-time1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-serialization1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-serialization1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-system1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost-system1.74.0:amd64 (1.74.0-18.1ubuntu3) ...
Removing libboost1.74-dev:amd64 (1.74.0-18.1ubuntu3) ...
Removing libpmix2:amd64 (4.2.2-1) ...
Removing libevent-pthreads-2.1-7:amd64 (2.1.12-stable-8ubuntu3) ...
Removing libhwloc-plugins:amd64 (2.9.0-1) ...
Removing libhwloc15:amd64 (2.9.0-1) ...
Removing librdmacm1:amd64 (44.0-2) ...
Removing libibverbs1:amd64 (44.0-2) ...
Removing libmunge2 (0.5.15-2) ...
Removing libpsm-infinipath1 (3.3+20.604758e7-6.2) ...
update-alternatives: warning: alternative /usr/lib/libpsm1/libpsm_infinipath.so.1.16 (part of link group libpsm_infinipath.so.1) doesn't exist; removing from list of alternatives
update-alternatives: warning: /etc/alternatives/libpsm_infinipath.so.1 is dangling; it will be updated with best choice
Removing libpsm2-2 (11.2.185-2) ...
Removing openmpi-common (4.1.4-3ubuntu2) ...
Processing triggers for man-db (2.11.2-1) ...
Processing triggers for libc-bin (2.37-0ubuntu2) ...
/sbin/ldconfig.real: /usr/lib/wsl/lib/libcuda.so.1 is not a symbolic link </span></pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region conclusion -->
<h2 id="go">Go Forth and Get Boosted</h2>
<span style='font-size: 3em; float: right; margin-left: 5px;'>😁</span>
<p>
Boost usage as a C++ general-pupose library is
widespread throughout a diverse range of industries and requirements.
</p>
<!-- endregion -->
PDF Manipulation2023-09-08T00:00:00-04:00https://mslinn.github.io/blog/2023/09/08/pdftk<!-- #region intro -->
<h2 id="install">Installation</h2>
<p>
The <a href='https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/' target='_blank' rel='nofollow'><code>pdftk</code> website</a>
discusses <code>pdktk</code> (a F/OSS command-line program), a Windows GUI, and mentions
the book <a href='https://github.com/InspectorDidi/Hacking-Books/blob/master/PDF%20Hacks.pdf' target='_blank' rel='nofollow'><b>PDF Hacks</b></a>.
</p>
<p>
An alternative free GUI is <a href='https://portableapps.com/apps/office/pdftk_builder_portable' target='_blank' rel='nofollow'>PDFTK Builder</a>.
</p>
<p>
Install the command-line <code>pdftk</code> program on Ubuntu:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd9e9e201d9f5'><button class='copyBtn' data-clipboard-target='#idd9e9e201d9f5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo apt install pdftk</pre>
</div>
<!-- endregion -->
<p>
This is the <code>pdftk</code> help text:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idab818190aadf'><button class='copyBtn' data-clipboard-target='#idab818190aadf' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>man pdftk
<span class='unselectable'>pdftk.pdftk-java(1) General Commands Manual pdftk.pdftk-java(1)<br/>
NAME
pdftk - A handy tool for manipulating PDF<br/>
SYNOPSIS
pdftk <input PDF files | - | PROMPT>
[ input_pw <input PDF owner passwords | PROMPT> ]
[ <operation> <operation arguments> ]
[ output <output filename | - | PROMPT> ]
[ encrypt_40bit | encrypt_128bit | encrypt_aes128 ]
[ allow <permissions> ]
[ owner_pw <owner password | PROMPT> ]
[ user_pw <user password | PROMPT> ]
[ flatten ] [ need_appearances ]
[ compress | uncompress ]
[ keep_first_id | keep_final_id ] [ drop_xfa ] [ drop_xmp
]
[ replacement_font <font name> ]
[ verbose ] [ dont_ask | do_ask ]
Where:
<operation> may be empty, or:
[ cat | shuffle | burst | rotate |
generate_fdf | fill_form |
background | multibackground |
stamp | multistamp |
dump_data | dump_data_utf8 |
dump_data_fields | dump_data_fields_utf8 |
dump_data_annots |
update_info | update_info_utf8 |
attach_files | unpack_files ]<br/>
For Complete Help: pdftk --help<br/>
DESCRIPTION
If PDF is electronic paper, then pdftk is an electronic staple-
remover, hole-punch, binder, secret-decoder-ring, and X-Ray-
glasses. Pdftk is a simple tool for doing everyday things with
PDF documents. Use it to:<br/>
* Merge PDF Documents or Collate PDF Page Scans
* Split PDF Pages into a New Document
* Rotate PDF Documents or Pages
* Decrypt Input as Necessary (Password Required)
* Encrypt Output as Desired
* Fill PDF Forms with X/FDF Data and/or Flatten Forms
* Generate FDF Data Stencils from PDF Forms
* Apply a Background Watermark or a Foreground Stamp
* Report PDF Metrics, Bookmarks and Metadata
* Add/Update PDF Metrics, Bookmarks or Metadata
* Attach Files to PDF Pages or the PDF Document
* Unpack PDF Attachments
* Burst a PDF Document into Single Pages
* Uncompress and Re-Compress Page Streams
* Repair Corrupted PDF (Where Possible)<br/>
OPTIONS
A summary of options is included below.<br/>
--help, -h
Show this summary of options.<br/>
<input PDF files | - | PROMPT>
A list of the input PDF files. If you plan to combine
these PDFs (without using handles) then list files in
the order you want them combined. Use - to pass a sin‐
gle PDF into pdftk via stdin. Input files can be asso‐
ciated with handles, where a handle is one or more up‐
per-case letters:<br/>
<input PDF handle>=<input PDF filename><br/>
Handles are often omitted. They are useful when speci‐
fying PDF passwords or page ranges, later.<br/>
For example: A=input1.pdf QT=input2.pdf M=input3.pdf<br/>
[input_pw <input PDF owner passwords | PROMPT>]
Input PDF owner passwords, if necessary, are associated
with files by using their handles:<br/>
<input PDF handle>=<input PDF file owner password><br/>
If handles are not given, then passwords are associated
with input files by order.<br/>
Most pdftk features require that encrypted input PDF are
accompanied by the ~owner~ password. If the input PDF
has no owner password, then the user password must be
given, instead. If the input PDF has no passwords, then
no password should be given.<br/>
When running in do_ask mode, pdftk will prompt you for a
password if the supplied password is incorrect or none
was given.<br/>
[<operation> <operation arguments>]
Available operations are: cat, shuffle, burst, rotate,
generate_fdf, fill_form, background, multibackground,
stamp, multistamp, dump_data, dump_data_utf8,
dump_data_fields, dump_data_fields_utf8, dump_data_an‐
nots, update_info, update_info_utf8, attach_files, un‐
pack_files. Some operations takes additional arguments,
described below.<br/>
If this optional argument is omitted, then pdftk runs in
'filter' mode. Filter mode takes only one PDF input and
creates a new PDF after applying all of the output op‐
tions, like encryption and compression.<br/>
cat [<page ranges>]
Assembles (catenates) pages from input PDFs to create
a new PDF. Use cat to merge PDF pages or to split PDF
pages from documents. You can also use it to rotate
PDF pages. Page order in the new PDF is specified by
the order of the given page ranges. Page ranges are
described like this:<br/>
<input PDF handle>[<begin page number>[-<end page
number>[<qualifier>]]][<page rotation>]<br/>
Where the handle identifies one of the input PDF
files, and the beginning and ending page numbers are
one-based references to pages in the PDF file. The
qualifier can be even, odd, or ~, and the page rota‐
tion can be north, south, east, west, left, right, or
down.<br/>
If a PDF handle is given but no pages are specified,
then the entire PDF is used. If no pages are speci‐
fied for any of the input PDFs, then the input PDFs'
bookmarks are also merged and included in the output.<br/>
If the handle is omitted from the page range, then
the pages are taken from the first input PDF.<br/>
The even qualifier causes pdftk to use only the even-
numbered PDF pages, so 1-6even yields pages 2, 4 and
6 in that order. 6-1even yields pages 6, 4 and 2 in
that order.<br/>
The odd qualifier works similarly to the even.<br/>
Pages can be subtracted from a page range using the ~
qualifier followed by a page range. For instance,
1-20~5-6 and 1-20~5~6 are equivalent to 1-4 7-20, and
~5 yields all pages except page 5. Depending on your
shell, you may need to quote this argument because of
the ~ at the beginning.<br/>
The page rotation setting can cause pdftk to rotate
pages and documents. Each option sets the page rota‐
tion as follows (in degrees): north: 0, east: 90,
south: 180, west: 270, left: -90, right: +90, down:
+180. left, right, and down make relative adjustments
to a page's rotation.<br/>
If no arguments are passed to cat, then pdftk com‐
bines all input PDFs in the order they were given to
create the output.<br/>
NOTES:
* <end page number> may be less than <begin page num‐
ber>.
* The keyword end may be used to reference the final
page of a document instead of a page number.
* Reference a single page by omitting the ending page
number.
* The handle may be used alone to represent the en‐
tire PDF document, e.g., B1-end is the same as B.
* You can reference page numbers in reverse order by
prefixing them with the letter r. For example, page
r1 is the last page of the document, r2 is the next-
to-last page of the document, and rend is the first
page of the document. You can use this prefix in
ranges, too, for example r3-r1 is the last three
pages of a PDF.<br/>
Page Range Examples without Handles:
1\-endeast – rotate entire document 90 degrees
5 11 20 – take single pages from input PDF
5-25oddwest – take odd pages in range, rotate 90 de‐
grees
6-1 – reverse pages in range from input PDF<br/>
Page Range Examples Using Handles:
Say A=in1.pdf B=in2.pdf, then:
A1-21 – take range from in1.pdf
Bend-1odd – take all odd pages from in2.pdf in re‐
verse order
A72 – take a single page from in1.pdf
A1-21 Beven A72 – assemble pages from both in1.pdf
and in2.pdf
Awest – rotate entire in1.pdf document 90 degrees
B – use all of in2.pdf
A2-30evenleft – take the even pages from the range,
remove 90 degrees from each page's rotation
A A – catenate in1.pdf with in1.pdf
Aevenwest Aoddeast – apply rotations to even pages,
odd pages from in1.pdf
Awest Bwest Bdown – catenate rotated documents<br/>
shuffle [<page ranges>]
Collates pages from input PDFs to create a new PDF.
Works like the cat operation except that it takes one
page at a time from each page range to assemble the
output PDF. If one range runs out of pages, it con‐
tinues with the remaining ranges. Ranges can use all
of the features described above for cat, like reverse
page ranges, multiple ranges from a single PDF, and
page rotation. This feature was designed to help
collate PDF pages after scanning paper documents.<br/>
burst Splits a single input PDF document into individual
pages. Also creates a report named doc_data.txt which
is the same as the output from dump_data. The output
section can contain a printf-styled format string to
name these pages. For example, if you want pages
named page_01.pdf, page_02.pdf, etc., pass output
page_%02d.pdf to pdftk. If the pattern is omitted,
then a default pattern g_%04d.pdf is appended and
produces pages named pg_0001.pdf, pg_0002.pdf, etc.
Encryption can be applied to the output by appending
output options such as owner_pw, e.g.:<br/>
pdftk in.pdf burst owner_pw foopass<br/>
rotate [<page ranges>]
Takes a single input PDF and rotates just the speci‐
fied pages. All other pages remain unchanged. The
page order remains unchanged. Specify the pages to
rotate using the same notation as you would with cat,
except you omit the pages that you aren't rotating:<br/>
[<begin page number>[-<end page number>[<quali‐
fier>]]][<page rotation>]<br/>
The qualifier can be even or odd, and the page rota‐
tion can be north, south, east, west, left, right, or
down.<br/>
Each option sets the page rotation as follows (in de‐
grees): north: 0, east: 90, south: 180, west: 270,
left: -90, right: +90, down: +180. left, right, and
down make relative adjustments to a page's rotation.<br/>
The given order of the pages doesn't change the page
order in the output.<br/>
generate_fdf
Reads a single input PDF file and generates an FDF
file suitable for fill_form out of it to the given
output filename or (if no output is given) to stdout.
Does not create a new PDF.<br/>
fill_form <FDF data filename | XFDF data filename | - |
PROMPT>
Fills the single input PDF's form fields with the
data from an FDF file, XFDF file or stdin. Enter the
data filename after fill_form, or use - to pass the
data via stdin, like so:<br/>
pdftk form.pdf fill_form data.fdf output
form.filled.pdf<br/>
If the input FDF file includes Rich Text formatted
data in addition to plain text, then the Rich Text
data is packed into the form fields as well as the
plain text. Pdftk also sets a flag that cues
Reader/Acrobat to generate new field appearances
based on the Rich Text data. So when the user opens
the PDF, the viewer will create the Rich Text appear‐
ance on the spot. If the user's PDF viewer does not
support Rich Text, then the user will see the plain
text data instead. If you flatten this form before
Acrobat has a chance to create (and save) new field
appearances, then the plain text field data is what
you'll see.<br/>
Also see the flatten, need_appearances, and replace‐
ment_font options.<br/>
background <background PDF filename | - | PROMPT>
Applies a PDF watermark to the background of a single
input PDF. Pass the background PDF's filename after
background like so:<br/>
pdftk in.pdf background back.pdf output out.pdf<br/>
Pdftk uses only the first page from the background
PDF and applies it to every page of the input PDF.
This page is scaled and rotated as needed to fit the
input page. You can use - to pass a background PDF
into pdftk via stdin.<br/>
If the input PDF does not have a transparent back‐
ground (such as a PDF created from page scans) then
the resulting background won't be visible – use the
stamp operation instead.<br/>
multibackground <background PDF filename | - | PROMPT>
Same as the background operation, but applies each
page of the background PDF to the corresponding page
of the input PDF. If the input PDF has more pages
than the stamp PDF, then the final stamp page is re‐
peated across these remaining pages in the input PDF.<br/>
stamp <stamp PDF filename | - | PROMPT>
This behaves just like the background operation ex‐
cept it overlays the stamp PDF page on top of the in‐
put PDF document's pages. This works best if the
stamp PDF page has a transparent background.<br/>
multistamp <stamp PDF filename | - | PROMPT>
Same as the stamp operation, but applies each page of
the background PDF to the corresponding page of the
input PDF. If the input PDF has more pages than the
stamp PDF, then the final stamp page is repeated
across these remaining pages in the input PDF.<br/>
dump_data
Reads a single input PDF file and reports its meta‐
data, bookmarks (a/k/a outlines), page metrics (me‐
dia, rotation and labels), data embedded by STAMPtk
(see STAMPtk's embed option) and other data to the
given output filename or (if no output is given) to
stdout. Non-ASCII characters are encoded as XML nu‐
merical entities. Does not create a new PDF.<br/>
dump_data_utf8
Same as dump_data except that the output is encoded
as UTF-8.<br/>
dump_data_fields
Reads a single input PDF file and reports form field
statistics to the given output filename or (if no
output is given) to stdout. Non-ASCII characters are
encoded as XML numerical entities. Does not create a
new PDF.<br/>
dump_data_fields_utf8
Same as dump_data_fields except that the output is
encoded as UTF-8.<br/>
dump_data_annots
This operation currently reports only link annota‐
tions. Reads a single input PDF file and reports an‐
notation information to the given output filename or
(if no output is given) to stdout. Non-ASCII charac‐
ters are encoded as XML numerical entities. Does not
create a new PDF.<br/>
update_info <info data filename | - | PROMPT>
Changes the bookmarks, page labels, page sizes, page
rotations, and metadata in a single PDF's Info dic‐
tionary to match the input data file. The input data
file uses the same syntax as the output from
dump_data. Non-ASCII characters should be encoded as
XML numerical entities.<br/>
This operation does not change the metadata stored in
the PDF's XMP stream, if it has one. (For this reason
you should include a ModDate entry in your updated
info with a current date/timestamp, format: D:YYYYM‐
MDDHHmmSS, e.g. D:201307241346 – omitted data after
YYYY revert to default values.)<br/>
For example:<br/>
pdftk in.pdf update_info in.info output out.pdf<br/>
update_info_utf8 <info data filename | - | PROMPT>
Same as update_info except that the input is encoded
as UTF-8.<br/>
attach_files <attachment filenames | PROMPT> [to_page <page
number | PROMPT> | relation <relationship>]
Packs arbitrary files into a PDF using PDF's file at‐
tachment features. More than one attachment may be
listed after attach_files. Attachments are added at
the document level unless the optional to_page option
is given, in which case the files are attached to the
given page number (the first page is 1, the final
page is end). Attachments at the document level may
be tagged with a relationship among Source, Data, Al‐
ternative, Supplement, and Unspecified (default).<br/>
For example:<br/>
pdftk in.pdf attach_files table1.html table2.html
to_page 6 output out.pdf<br/>
pdftk in.pdf attach_files in.tex relation Source out‐
put out.pdf<br/>
unpack_files
Copies all of the attachments from the input PDF into
the current folder or to an output directory given
after output. For example:<br/>
pdftk report.pdf unpack_files output ~/atts/<br/>
or, interactively:<br/>
pdftk report.pdf unpack_files output PROMPT<br/>
[output <output filename | - | PROMPT>]
The output PDF filename may not be set to the name of an
input filename. Use - to output to stdout. When using
the dump_data operation, use output to set the name of
the output data file. When using the unpack_files opera‐
tion, use output to set the name of an output directory.
When using the burst operation, you can use output to
control the resulting PDF page filenames (described
above).<br/>
[encrypt_40bit | encrypt_128bit | encrypt_aes128]
If an output PDF user or owner password is given, the
output PDF encryption algorithm defaults to AES-128. The
weaker RC4 40-bit and RC4 128-bit algorithms can be cho‐
sen by specifying encrypt_40bit or encrypt_128bit (dis‐
couraged).<br/>
[allow <permissions>]
Permissions are applied to the output PDF only if an en‐
cryption strength is specified or an owner or user pass‐
word is given. If permissions are not specified, they
default to 'none,' which means all of the following fea‐
tures are disabled.<br/>
The permissions section may include one or more of the
following features:<br/>
Printing
Top Quality Printing<br/>
DegradedPrinting
Lower Quality Printing<br/>
ModifyContents
Also allows Assembly<br/>
Assembly<br/>
CopyContents
Also allows ScreenReaders<br/>
ScreenReaders<br/>
ModifyAnnotations
Also allows FillIn<br/>
FillIn<br/>
AllFeatures
Allows the user to perform all of the above, and
top quality printing.<br/>
[owner_pw <owner password | PROMPT>]<br/>
[user_pw <user password | PROMPT>]
If an encryption strength is given but no passwords are
supplied, then the owner and user passwords remain
empty, which means that the resulting PDF may be opened
and its security parameters altered by anybody.<br/>
[compress | uncompress]
These are only useful when you want to edit PDF code in
a text editor like vim or emacs. Remove PDF page stream
compression by applying the uncompress filter. Use the
compress filter to restore compression.<br/>
[flatten]
Use this option to merge an input PDF's interactive form
fields (and their data) with the PDF's pages. Only one
input PDF may be given. Sometimes used with the
fill_form operation.<br/>
[need_appearances]
Sets a flag that cues Reader/Acrobat to generate new
field appearances based on the form field values. Use
this when filling a form with non-ASCII text to ensure
the best presentation in Adobe Reader or Acrobat. It
won't work when combined with the flatten option.<br/>
[replacement_font <font name>]
Use the specified font to display text in form fields.
This option is useful when filling a form with non-ASCII
text that is not supported by the fonts included in the
input PDF. font name may be either the file name or the
family name of a font, but using a file name is more re‐
liable. Currently only TrueType fonts with Unicode text
are supported.<br/>
[keep_first_id | keep_final_id]
When combining pages from multiple PDFs, use one of
these options to copy the document ID from either the
first or final input document into the new output PDF.
Otherwise pdftk creates a new document ID for the output
PDF. When no operation is given, pdftk always uses the
ID from the (single) input PDF.<br/>
[drop_xfa]
If your input PDF is a form created using Acrobat 7 or
Adobe Designer, then it probably has XFA data. Filling
such a form using pdftk yields a PDF with data that
fails to display in Acrobat 7 (and 6?). The workaround
solution is to remove the form's XFA data, either before
you fill the form using pdftk or at the time you fill
the form. Using this option causes pdftk to omit the XFA
data from the output PDF form.<br/>
This option is only useful when running pdftk on a sin‐
gle input PDF. When assembling a PDF from multiple in‐
puts using pdftk, any XFA data in the input is automati‐
cally omitted.<br/>
[drop_xmp]
Many PDFs store document metadata using both an Info
dictionary (old school) and an XMP stream (new school).
Pdftk's update_info operation can update the Info dic‐
tionary, but not the XMP stream. The proper remedy for
this is to include a ModDate entry in your updated info
with a current date/timestamp. The date/timestamp format
is: D:YYYYMMDDHHmmSS, e.g. D:201307241346 – omitted data
after YYYY revert to default values. This newer ModDate
should cue PDF viewers that the Info metadata is more
current than the XMP data.<br/>
Alternatively, you might prefer to remove the XMP stream
from the PDF altogether – that's what this option does.
Note that objects inside the PDF might have their own,
separate XMP metadata streams, and that drop_xmp does
not remove those. It only removes the PDF's document-
level XMP stream.<br/>
[verbose]
By default, pdftk runs quietly. Append verbose to the
end and it will speak up.<br/>
[dont_ask | do_ask]
Depending on the compile-time settings (see
ASK_ABOUT_WARNINGS), pdftk might prompt you for further
input when it encounters a problem, such as a bad pass‐
word. Override this default behavior by adding dont_ask
(so pdftk won't ask you what to do) or do_ask (so pdftk
will ask you what to do).<br/>
When running in dont_ask mode, pdftk will over-write
files with its output without notice.<br/>
EXAMPLES
Collate scanned pages
pdftk A=even.pdf B=odd.pdf shuffle A B output collated.pdf
or if odd.pdf is in reverse order:
pdftk A=even.pdf B=odd.pdf shuffle A Bend-1 output col‐
lated.pdf<br/>
The following examples use actual passwords as command line pa‐
rameters, which is discouraged (see the SECURITY CONSIDERATIONS
section).<br/>
Decrypt a PDF
pdftk secured.pdf input_pw foopass output unsecured.pdf<br/>
Encrypt a PDF using AES-128 (the default), withhold all permis‐
sions (the default)
pdftk 1.pdf output 1.128.pdf owner_pw foopass<br/>
Same as above, except password 'baz' must also be used to open
output PDF
pdftk 1.pdf output 1.128.pdf owner_pw foo user_pw baz<br/>
Same as above, except printing is allowed (once the PDF is
open)
pdftk 1.pdf output 1.128.pdf owner_pw foo user_pw baz allow
printing<br/>
Apply RCA 40-bit encryption to output, revoking all permissions
(the default). Set the owner PW to 'foopass'.
pdftk 1.pdf 2.pdf cat output 3.pdf encrypt_40bit owner_pw
foopass<br/>
Join two files, one of which requires the password 'foopass'.
The output is not encrypted.
pdftk A=secured.pdf 2.pdf input_pw A=foopass cat output 3.pdf<br/>
Join in1.pdf and in2.pdf into a new PDF, out1.pdf
pdftk in1.pdf in2.pdf cat output out1.pdf
or (using handles):
pdftk A=in1.pdf B=in2.pdf cat A B output out1.pdf
or (using wildcards):
pdftk *.pdf cat output combined.pdf<br/>
Remove page 13 from in1.pdf to create out1.pdf
pdftk in.pdf cat 1-12 14-end output out1.pdf
or:
pdftk A=in1.pdf cat A1-12 A14-end output out1.pdf<br/>
Uncompress PDF page streams for editing the PDF in a text edi‐
tor (e.g., vim, emacs)
pdftk doc.pdf output doc.unc.pdf uncompress<br/>
Repair a PDF's corrupted XREF table and stream lengths, if pos‐
sible
pdftk broken.pdf output fixed.pdf<br/>
Burst a single PDF document into pages and dump its data to
doc_data.txt
pdftk in.pdf burst<br/>
Burst a single PDF document into encrypted pages. Allow low-
quality printing
pdftk in.pdf burst owner_pw foopass allow DegradedPrinting<br/>
Write a report on PDF document metadata and bookmarks to re‐
port.txt
pdftk in.pdf dump_data output report.txt<br/>
Rotate the first PDF page to 90 degrees clockwise
pdftk in.pdf cat 1east 2-end output out.pdf<br/>
Rotate an entire PDF document to 180 degrees
pdftk in.pdf cat 1-endsouth output out.pdf<br/>
NOTES
This is a port of pdftk to java. See
https://gitlab.com/pdftk-java/pdftk
The original program can be found at www.pdftk.com<br/>
AUTHOR
Original author of pdftk is Sid Steward (sid.steward at pdflabs
dot com).<br/>
SECURITY CONSIDERATIONS
Passing a password as a command line parameter is insecure be‐
cause it can get saved into the shell's history and be accessi‐
ble by other users via /proc. Use the keyword PROMPT and input
any passwords via standard input instead.<br/>
December 7, 2020 pdftk.pdftk-java(1) </span></pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region OCR -->
<h2 id="ocr">OCR</h2>
<p>
<a href='https://tools.pdf24.org/en/ocr-pdf' target='_blank' rel='nofollow'><code>https://tools.pdf24.org/en/ocr-pdf</code></a> provides fast, free OCR, without ads.
</p>
<p>
This website works well, and offers a total of 30 PDF operations.
The other operations of <a href='https://tools.pdf24.org/' target='_blank' rel='nofollow'><code>https://tools.pdf24.org/</code></a> include merging, splitting,
compressing, editing, signing, redacting, watermarking, locking, unlocking, etc.
</p>
<!-- endregion -->
<!-- #region ops -->
<h2 id="ops">PdfTk Operations</h2>
<!-- #region rotate one page -->
<h3 id="2south">Rotate page 2 only</h3>
<p>
Given <code>my_file.pdf</code>, a 2-page document:
</p>
<ol>
<li>Do not transform page 1 (keep it as-is)</li>
<li>Rotate page 2 only</li>
<li>Save as <code>my_file_fixed.pdf</code></li>
</ol>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9d3e593410e7'><button class='copyBtn' data-clipboard-target='#id9d3e593410e7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk my_file.pdf cat 1 2south output my_file_fixed.pdf</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region merge -->
<h3 id="merge">Merge PDFs</h3>
<p>
Concatenate <code>file_a.pdf</code>, <code>file_b.pdf</code> and <code>file_c.pdf</code>, then save as <code>big_file.pdf</code>:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id48170f6db20f'><button class='copyBtn' data-clipboard-target='#id48170f6db20f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk file_a.pdf file_b.pdf file_c.pdf cat output big_file.pdf</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region assemble -->
<h3 id="assemble">Assemble Book Excerpt</h3>
<p>
If you need to create multiple excerpts of a book, one way to proceed is to:
</p>
<ol>
<li>
Create a PDF containing 3 pages:
the front cover, the page containing the <a href='https://en.wikipedia.org/wiki/Edition_notice' target='_blank' rel='nofollow'>edition notice</a>,
and the back cover.
Let's call this PDF file <code>book_wrapper.pdf</code>.
</li>
<li>
Scan each of the excerpts into separate PDFs.
Let's call these PDF files <code>book_excerpt_raw_1.pdf</code>, <code>book_excerpt_raw_2.pdf</code>, etc.
</li>
<li>
Wrap the each excerpt within the front cover and edition notice, and the back cover.
To do that, concatenate the first 2 pages of <code>book_wrapper.pdf</code>, <code>book_excerpt_raw_N.pdf</code>
and the last page of <code>book_wrapper.pdf</code>, then save as <code>book_excerpt_N.pdf</code>.
Following is an example of how to do that for 3 excerpts:
</li>
</ol>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7debafcd58fa'><button class='copyBtn' data-clipboard-target='#id7debafcd58fa' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk book_wrapper.pdf 1-2 \
book_excerpt_raw_1.pdf \
book_wrapper.pdf 3 \
cat output book_excerpt_1.pdf
<span class='unselectable'>$ </span>pdftk book_wrapper.pdf 1-2 \
book_excerpt_raw_2.pdf \
book_wrapper.pdf 3 \
cat output book_excerpt_2.pdf
<span class='unselectable'>$ </span>pdftk book_wrapper.pdf 1-2 \
book_excerpt_raw_3.pdf \
book_wrapper.pdf 3 \
cat output book_excerpt_3.pdf</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region cut -->
<h3 id="cut">Cut a large PDF into 2 Parts</h3>
<p>
Cut <code>bigfile.pdf</code> into two parts:
</p>
<ol>
<li><code>bigfile_part1.pdf</code> (containing pages 1-150)</li>
<li><code>bigfile_part2.pdf</code> (containing pages 151-350)</li>
</ol>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id528820770a3c'><button class='copyBtn' data-clipboard-target='#id528820770a3c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk A=bigfile.pdf cat 1-150 output bigfile_part1.pdf
<span class='unselectable'>$ </span>pdftk A=bigfile.pdf cat 151-end output bigfile_part2.pdf</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region delete 1 -->
<h3 id="cut">Delete a Page</h3>
<p>
Delete page 69 from <code>bigfile.pdf</code> and save as <code>smaller.pdf</code>:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id23ee81eb6864'><button class='copyBtn' data-clipboard-target='#id23ee81eb6864' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk A=bigfile.pdf cat ~69 output smaller.pdf</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region delete several -->
<h3 id="cut">Delete Several Pages</h3>
<p>
Delete pages 69-96 from <code>bigfile.pdf</code> and save as <code>smaller.pdf</code>:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0705f64cfc24'><button class='copyBtn' data-clipboard-target='#id0705f64cfc24' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk A=bigfile.pdf cat ~69-96 output smaller.pdf</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region remove password -->
<h2 id="pwd">Remove Password</h2>
<p>
Assuming <code>protected.pdf</code> is encrypted with password <code>myPwd</code>, make an unencrypted copy and save as <code>output.pdf</code>:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3dfaa9d94474'><button class='copyBtn' data-clipboard-target='#id3dfaa9d94474' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pdftk protected.pdf input_pw myPwd output output.pdf</pre>
</div>
<!-- endregion -->
<p>
You might need to enclose the password within single quotes.
</p>
<!-- endregion -->
<!-- endregion -->
Pytest and Visual Studio Code2023-08-20T00:00:00-04:00https://mslinn.github.io/blog/2023/08/20/pytest<!-- #region intro -->
<p>
Python has several well-known testing frameworks.
Popular choices for functional and unit testing include
<a href='https://docs.pytest.org' target='_blank' rel='nofollow'><code>pytest</code></a>,
the <a href='https://robotframework.org/' target='_blank' rel='nofollow'>Robot framework</a>,
and <a href='https://docs.python.org/3/library/unittest.html' target='_blank' rel='nofollow'><code>unittest</code></a>.
<a href='https://github.com/gabrielfalcao/lettuce' target='_blank' rel='nofollow'>Lettuce</a>
and <a href='https://behave.readthedocs.io/en/latest/' target='_blank' rel='nofollow'>Behave</a>
are popular for behavior-driven testing.
</p>
<p>
I decided to use <code>pytest</code> for a recent Python project.
</p>
<p>
I found that <code>pytest</code>’s test discovery mechanism was finicky,
and demanded that Python packages and modules be set up before tests could be run.
This is in sharp contrast to writing unit tests for Ruby, for example, using
<a href='https://rspec.info/' target='_blank' rel='nofollow'><code>rspec</code></a>,
because Ruby modules are so much more flexible, forgiving and malleable.
</p>
<p>
However, getting <code>pytest</code> to work in Visual Studio Code was nearly impossible.
Microsoft's documentation leaves a lot unsaid.
I share what I learned, and how I made things work in this article.
</p>
<!-- endregion -->
<!-- #region pytest -->
<h2 id="pytest">Pytest</h2>
<p>
You can run <code>pytest</code> from the command line 3 ways.
</p>
<!-- #region pytest command -->
<h3 id="cmd"><span class="code">Pytest</span> Command</h3>
<p>
The most common way to run <code>pytest</code> is to type the <code>pytest</code> command,
like the following, which runs all tests:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idea1ddc40c22c'><button class='copyBtn' data-clipboard-target='#idea1ddc40c22c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pytest</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region module -->
<h3 id="module"><span class="code">Pytest</span> Module</h3>
<p>
The second way to run <code>pytest</code> is to invoke the <code>pytest</code> Python module, as follows:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id733677487d2e'><button class='copyBtn' data-clipboard-target='#id733677487d2e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>python -m pytest</pre>
</div>
<!-- endregion -->
<p>
Running <code>pytest</code> as a Python module is nearly the same as running the <code>pytest</code> command,
except that running it as a module adds the current directory to <code>sys.path</code>,
which is standard python behavior, and can be helpful.
</p>
<p>
I had best results running <code>pytest</code> tests from Visual Studio Code when <code>pytest</code> was invoked as a module.
</p>
<!-- endregion -->
<!-- #region calling from python -->
<h3 id="pgm">Calling <span class="code">Pytest</span> from Python</h3>
<p>
Your code can
<a href='https://docs.pytest.org/en/7.1.x/how-to/usage.html#calling-pytest-from-python-code' target='_blank' rel='nofollow'>call <code>pytest</code></a>.
Lots of interesting and advanced development setups could be crafted using this approach.
Baby steps.
</p>
<!-- endregion -->
<!-- #region help -->
<h3 id="help">Help Message</h3>
<p>
The help message for <code>pytest</code> is:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5e5309d9a7ed'><button class='copyBtn' data-clipboard-target='#id5e5309d9a7ed' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pytest -h
<span class='unselectable'>usage: pytest [options] [file_or_dir] [file_or_dir] [...]<br/>
positional arguments:
file_or_dir<br/>
general:
-k EXPRESSION Only run tests which match the given substring
expression. An expression is a Python evaluatable
expression where all names are substring-matched against
test names and their parent classes. Example: -k
'test_method or test_other' matches all test functions
and classes whose name contains 'test_method' or
'test_other', while -k 'not test_method' matches those
that don't contain 'test_method' in their names. -k 'not
test_method and not test_other' will eliminate the
matches. Additionally keywords are matched to classes
and functions containing extra names in their
'extra_keyword_matches' set, as well as functions which
have names assigned directly to them. The matching is
case-insensitive.
-m MARKEXPR Only run tests matching given mark expression. For
example: -m 'mark1 and not mark2'.
--markers show markers (builtin, plugin and per-project ones).
-x, --exitfirst Exit instantly on first error or failed test
--fixtures, --funcargs
Show available fixtures, sorted by plugin appearance
(fixtures with leading '_' are only shown with '-v')
--fixtures-per-test Show fixtures per test
--pdb Start the interactive Python debugger on errors or
KeyboardInterrupt
--pdbcls=modulename:classname
Specify a custom interactive Python debugger for use
with --pdb.For example:
--pdbcls=IPython.terminal.debugger:TerminalPdb
--trace Immediately break when running each test
--capture=method Per-test capturing method: one of fd|sys|no|tee-sys
-s Shortcut for --capture=no
--runxfail Report the results of xfail tests as if they were not
marked
--lf, --last-failed Rerun only the tests that failed at the last run (or all
if none failed)
--ff, --failed-first Run all tests, but run the last failures first. This may
re-order tests and thus lead to repeated fixture
setup/teardown.
--nf, --new-first Run tests from new files first, then the rest of the
tests sorted by file mtime
--cache-show=[CACHESHOW]
Show cache contents, don't perform collection or tests.
Optional argument: glob (default: '*').
--cache-clear Remove all cache contents at start of test run
--lfnf={all,none}, --last-failed-no-failures={all,none}
Which tests to run with no previously (known) failures
--sw, --stepwise Exit on test failure and continue from last failing test
next time
--sw-skip, --stepwise-skip
Ignore the first failing test but stop on the next
failing test. Implicitly enables --stepwise.<br/>
Reporting:
--durations=N Show N slowest setup/test durations (N=0 for all)
--durations-min=N Minimal duration in seconds for inclusion in slowest
list. Default: 0.005.
-v, --verbose Increase verbosity
--no-header Disable header
--no-summary Disable summary
-q, --quiet Decrease verbosity
--verbosity=VERBOSE Set verbosity. Default: 0.
-r chars Show extra test summary info as specified by chars:
(f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed,
(p)assed, (P)assed with output, (a)ll except passed
(p/P), or (A)ll. (w)arnings are enabled by default (see
--disable-warnings), 'N' can be used to reset the list.
(default: 'fE').
--disable-warnings, --disable-pytest-warnings
Disable warnings summary
-l, --showlocals Show locals in tracebacks (disabled by default)
--no-showlocals Hide locals in tracebacks (negate --showlocals passed
through addopts)
--tb=style Traceback print mode (auto/long/short/line/native/no)
--show-capture={no,stdout,stderr,log,all}
Controls how captured stdout/stderr/log is shown on
failed tests. Default: all.
--full-trace Don't cut any tracebacks (default is to cut)
--color=color Color terminal output (yes/no/auto)
--code-highlight={yes,no}
Whether code should be highlighted (only if --color is
also enabled). Default: yes.
--pastebin=mode Send failed|all info to bpaste.net pastebin service
--junit-xml=path Create junit-xml style report file at given path
--junit-prefix=str Prepend prefix to classnames in junit-xml output<br/>
pytest-warnings:
-W PYTHONWARNINGS, --pythonwarnings=PYTHONWARNINGS
Set which warnings to report, see -W option of Python
itself
--maxfail=num Exit after first num failures or errors
--strict-config Any warnings encountered while parsing the `pytest`
section of the configuration file raise errors
--strict-markers Markers not registered in the `markers` section of the
configuration file raise errors
--strict (Deprecated) alias to --strict-markers
-c FILE, --config-file=FILE
Load configuration from `FILE` instead of trying to
locate one of the implicit configuration files.
--continue-on-collection-errors
Force test execution even if collection errors occur
--rootdir=ROOTDIR Define root directory for tests. Can be relative path:
'root_dir', './root_dir', 'root_dir/another_dir/';
absolute path: '/home/user/root_dir'; path with
variables: '$HOME/root_dir'.<br/>
collection:
--collect-only, --co Only collect tests, don't execute them
--pyargs Try to interpret all arguments as Python packages
--ignore=path Ignore path during collection (multi-allowed)
--ignore-glob=path Ignore path pattern during collection (multi-allowed)
--deselect=nodeid_prefix
Deselect item (via node id prefix) during collection
(multi-allowed)
--confcutdir=dir Only load conftest.py's relative to specified dir
--noconftest Don't load any conftest.py files
--keep-duplicates Keep duplicate tests
--collect-in-virtualenv
Don't ignore tests in a local virtualenv directory
--import-mode={prepend,append,importlib}
Prepend/append to sys.path when importing test modules
and conftest files. Default: prepend.
--doctest-modules Run doctests in all .py modules
--doctest-report={none,cdiff,ndiff,udiff,only_first_failure}
Choose another output format for diffs on doctest
failure
--doctest-glob=pat Doctests file matching pattern, default: test*.txt
--doctest-ignore-import-errors
Ignore doctest ImportErrors
--doctest-continue-on-failure
For a given doctest, continue to run after the first
failure<br/>
test session debugging and configuration:
--basetemp=dir Base temporary directory for this test run. (Warning:
this directory is removed if it exists.)
-V, --version Display pytest version and information about plugins.
When given twice, also display information about
plugins.
-h, --help Show help message and configuration info
-p name Early-load given plugin module name or entry point
(multi-allowed). To avoid loading of plugins, use the
`no:` prefix, e.g. `no:doctest`.
--trace-config Trace considerations of conftest.py files
--debug=[DEBUG_FILE_NAME]
Store internal tracing debug information in this log
file. This file is opened with 'w' and truncated as a
result, care advised. Default: pytestdebug.log.
-o OVERRIDE_INI, --override-ini=OVERRIDE_INI
Override ini option with "option=value" style, e.g. `-o
xfail_strict=True -o cache_dir=cache`.
--assert=MODE Control assertion debugging tools.
'plain' performs no assertion debugging.
'rewrite' (the default) rewrites assert statements in
test modules on import to provide assert expression
information.
--setup-only Only setup fixtures, do not execute tests
--setup-show Show setup of fixtures while executing tests
--setup-plan Show what fixtures and tests would be executed but don't
execute anything<br/>
logging:
--log-level=LEVEL Level of messages to catch/display. Not set by default,
so it depends on the root/parent log handler's effective
level, where it is "WARNING" by default.
--log-format=LOG_FORMAT
Log format used by the logging module
--log-date-format=LOG_DATE_FORMAT
Log date format used by the logging module
--log-cli-level=LOG_CLI_LEVEL
CLI logging level
--log-cli-format=LOG_CLI_FORMAT
Log format used by the logging module
--log-cli-date-format=LOG_CLI_DATE_FORMAT
Log date format used by the logging module
--log-file=LOG_FILE Path to a file when logging will be written to
--log-file-level=LOG_FILE_LEVEL
Log file logging level
--log-file-format=LOG_FILE_FORMAT
Log format used by the logging module
--log-file-date-format=LOG_FILE_DATE_FORMAT
Log date format used by the logging module
--log-auto-indent=LOG_AUTO_INDENT
Auto-indent multiline messages passed to the logging
module. Accepts true|on, false|off or an integer.
--log-disable=LOGGER_DISABLE
Disable a logger by name. Can be passed multiple times.<br/>
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:<br/>
markers (linelist): Markers for test functions
empty_parameter_set_mark (string):
Default marker for empty parametersets
norecursedirs (args): Directory patterns to avoid for recursion
testpaths (args): Directories to search for tests when no files or
directories are given on the command line
filterwarnings (linelist):
Each line specifies a pattern for
warnings.filterwarnings. Processed after
-W/--pythonwarnings.
usefixtures (args): List of default fixtures to be used with this project
python_files (args): Glob-style file patterns for Python test module
discovery
python_classes (args):
Prefixes or glob names for Python test class discovery
python_functions (args):
Prefixes or glob names for Python test function and
method discovery
disable_test_id_escaping_and_forfeit_all_rights_to_community_support (bool):
Disable string escape non-ASCII characters, might cause
unwanted side effects(use at your own risk)
console_output_style (string):
Console output: "classic", or with additional progress
information ("progress" (percentage) | "count" |
"progress-even-when-capture-no" (forces progress even
when capture=no)
xfail_strict (bool): Default for the strict parameter of xfail markers when
not given explicitly (default: False)
tmp_path_retention_count (string):
How many sessions should we keep the `tmp_path`
directories, according to `tmp_path_retention_policy`.
tmp_path_retention_policy (string):
Controls which directories created by the `tmp_path`
fixture are kept around, based on test outcome.
(all/failed/none)
enable_assertion_pass_hook (bool):
Enables the pytest_assertion_pass hook. Make sure to
delete any previously generated pyc cache files.
junit_suite_name (string):
Test suite name for JUnit report
junit_logging (string):
Write captured log messages to JUnit report: one of
no|log|system-out|system-err|out-err|all
junit_log_passing_tests (bool):
Capture log information for passing tests to JUnit
report:
junit_duration_report (string):
Duration time to report: one of total|call
junit_family (string):
Emit XML for schema: one of legacy|xunit1|xunit2
doctest_optionflags (args):
Option flags for doctests
doctest_encoding (string):
Encoding used for doctest files
cache_dir (string): Cache directory path
log_level (string): Default value for --log-level
log_format (string): Default value for --log-format
log_date_format (string):
Default value for --log-date-format
log_cli (bool): Enable log display during test run (also known as "live
logging")
log_cli_level (string):
Default value for --log-cli-level
log_cli_format (string):
Default value for --log-cli-format
log_cli_date_format (string):
Default value for --log-cli-date-format
log_file (string): Default value for --log-file
log_file_level (string):
Default value for --log-file-level
log_file_format (string):
Default value for --log-file-format
log_file_date_format (string):
Default value for --log-file-date-format
log_auto_indent (string):
Default value for --log-auto-indent
pythonpath (paths): Add paths to sys.path
faulthandler_timeout (string):
Dump the traceback of all threads if a test takes more
than TIMEOUT seconds to finish
addopts (args): Extra command line options
minversion (string): Minimally required pytest version
required_plugins (args):
Plugins that must be present for pytest to run<br/>
Environment variables:
PYTEST_ADDOPTS Extra command line options
PYTEST_PLUGINS Comma-separated plugins to load during startup
PYTEST_DISABLE_PLUGIN_AUTOLOAD Set to disable plugin auto-loading
PYTEST_DEBUG Set to enable debug tracing of pytest's internals<br/><br/>
to see available markers type: pytest --markers
to see available fixtures type: pytest --fixtures
(shown according to specified file_or_dir or current dir if not specified; fixtures with leading '_' are only shown with the '-v' option </span></pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- endregion -->
<!-- #region helpful options -->
<h3 id="options">Helpful Options</h3>
<p>
It is handy to run new and failing tests, but to skip previously passing tests.
The <code>‑‑nf</code> and <code>‑‑lf</code> options,
respectively, invoke that mode of behavior:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbb4a11cfcd38'><button class='copyBtn' data-clipboard-target='#idbb4a11cfcd38' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pytest --nf --lf</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region setup -->
<h2 id="setup">Setup</h2>
<!-- #region config -->
<h3 id="config">Configuration</h3>
<p>
<code>Pytest</code> needed to know that
the source code for my Python project is stored in the <code>src</code> directory.
I created <code>pyproject.toml</code> as follows, which configured <code>pytest</code>:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>pyproject.toml</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id87a77cf442ff'><button class='copyBtn' data-clipboard-target='#id87a77cf442ff' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>[tool.pytest.ini_options]
addopts = [
"--import-mode=importlib",
]
pythonpath = "src"</pre>
</div>
<!-- endregion -->
<p>
The way in which <code>pytest</code> discovers tests is
<a href='https://docs.pytest.org/en/7.1.x/explanation/pythonpath.htm' target='_blank' rel='nofollow'>complex</a>.
<a href='https://www.merriam-webster.com/dictionary/TL%3BDR' target='_blank' rel='nofollow'>TL;DR</a>:
specify the newest and best mechanism for test discovery by using the
<code>‑‑import-mode=importlib</code> option as shown above.
</p>
<p>
To always run <code>pytest</code> with the <code>‑‑nf</code> and <code>‑‑lf</code> options,
add those options to <code>pyproject.toml</code>:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>pyproject.toml</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc76012aad970'><button class='copyBtn' data-clipboard-target='#idc76012aad970' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>[tool.pytest.ini_options]
addopts = [
"--import-mode=importlib",
<span class='bg_yellow'>"--lf",</span>
<span class='bg_yellow'>"--nf",</span>
]
pythonpath = "src"</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region Test Directories -->
<h3 id="dirs">Test Directories</h3>
<p>
Flexibility generally adds complexity.
<code>Pytest</code> allows for a lot of flexibility regarding the location of where tests can reside.
Unfortunately, Python’s package and module mechanism in combination with the <code>pytest</code> discovery
mechanism for tests can lead to a frustrating experience when setting up a new project.
</p>
<p>
You can read about
<a href='https://pytest.org/en/7.4.x/explanation/goodpractices.html' target='_blank' rel='nofollow'>good practices for <code>pytest</code></a>.
Those recommendations are intimidating for first-time <code>pytest</code> programmers.
I did not follow all of them.
</p>
<p>
I found the simplest approach was to create a subdirectory for the bulk of the Python project code.
If a file called <code>__init__.py</code> is created in that directory
(use <a href='https://man7.org/linux/man-pages/man1/touch.1.html' target='_blank' rel='nofollow'><code>touch</code></a> for this)
then the subdirectory becomes a submodule of your Python program.
For this type of <code>pytest</code> setup,
each submodule needs a <code>tests</code> directory.
</p>
<p>
Here is how you could use <code>touch</code> to define an imaginary subdirectory/submodule
called <code>my_submodule</code>:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id667e744b4f0b'><button class='copyBtn' data-clipboard-target='#id667e744b4f0b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>mkdir -p src/my_submodule/<br>
<span class='unselectable'>$ </span>touch src/my_submodule/__init__.py</pre>
</div>
<!-- endregion -->
<p>
My project structure was as follows.
<a href='https://linux.die.net/man/1/tree' target='_blank' rel='nofollow'><code>Tree</code> docs are here.</a>
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>pytest project structure</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd1289be3c678'><button class='copyBtn' data-clipboard-target='#idd1289be3c678' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>tree -I .git -I __pycache__
<span class='unselectable'>.
├── README.md
├── dl
├── dl.config
├── pyproject.toml
├── requirements.txt
└── src
├── __main__.py
└── dl
├── __init__.py
├── argument_parse.py
├── dl_config.py
├── media_file.py
├── remote.py
├── tests
│ ├── __init__.py
│ ├── test_dl_config.py
│ └── test_uti.py
└── util.py
3 directories, 15 files </span></pre>
</div>
<!-- endregion -->
<p>
<code>src/dl/tests/__init__.py</code> was an empty marker file,
but the <code>___init__.py</code> file in the
<a href='https://docs.python.org/3/tutorial/modules.html' target='_blank' rel='nofollow'>parent module</a>
defined <code>import</code>s for the submodule:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>src/dl/__init__.py</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4e08a0eb5d40'><button class='copyBtn' data-clipboard-target='#id4e08a0eb5d40' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>from dl.argument_parse import ArgParse
from dl.dl_config import DLConfig
from dl.media_file import MediaFile
from dl.remote import Remote
from dl.util import *</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- endregion -->
<!-- #region sample test -->
<h2 id="test">Sample Test</h2>
<p>
Here is a sample <code>pytest</code> file, containing 2 unit tests.
It is simple and clear.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>src/dl/tests/test_util.py</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3f2801312ada'><button class='copyBtn' data-clipboard-target='#id3f2801312ada' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>import pytest
from pathlib import Path
from dl import samba_mount, samba_parse
class TestUtil:
def test_samba_parse(self):
remote_drive, local_path = samba_parse('e:/media/renders')
assert remote_drive=='e'
assert local_path=='/media/renders'
def test_samba_mount(self):
samba_root = samba_mount('bear', 'e', False)
assert samba_root==Path('/mnt/bear/e')
assert samba_root.is_mount()</pre>
</div>
<!-- endregion -->
<h2 id="rap">Secret Sauce</h2>
<p>
If you have been paying attention,
you should realize that one option for running new and failing tests is to
type the following from the command line.
We will use this method of invocation in the next section to integrate with Visual Studio Code.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id35996674bd6b'><button class='copyBtn' data-clipboard-target='#id35996674bd6b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>python -m pytest --nf --lf</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region vscode -->
<h2 id="vscode">Visual Studio Code Integration</h2>
<p>
Visual Studio Code has a Testing explorer and a Run and Debug explorer.
I find the existance of two very similar explorers annoying,
because you often want to debug a test.
Yes, you can do that by pushing the right button in the Testing Explorer ... if it works.
This did not work for me.
</p>
<p>
<a href='https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-extensions-pack' target='_blank' rel='nofollow'>Shopify Ruby</a>,
the latest and greatest Ruby extension for Visual Studio Code,
uses the Run and Debug explorer to run and debug tests as well as entire programs.
This makes sense to me.
</p>
<p>
<a href='https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance' target='_blank' rel='nofollow'>Microsoft Pylance</a>
(for Python) follows a different convention,
and uses the Run and Debug explorer to run and debug Python programs.
You must use the Testing explorer for running or debugging Python unit tests.
Annoying.
</p>
<p>
In the Testing explorer, I was able to see my <code>dl</code> module,
but no tests were discovered.
This was really annoying, and this is where more documentation from Microsoft would have been helpful.
After wasting lots of time trying to solve this problem, I discovered a solution.
</p>
<h3 id="">Invoke <span class="code">Pytest</span> As a Module from VSCode</h3>
<p>
Run and Debug configurations that invoke the <code>pytest</code> module work perfectly.
Here is an example <code>.vscode/launch.json</code> that demonstrates this:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>.vscode/launch.json</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd2c2f308c183'><button class='copyBtn' data-clipboard-target='#idd2c2f308c183' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>{
"version": "0.2.0",
"configurations": [
{
"name": "PyTest All",
"type": "python",
"request": "launch",
"module": "pytest",
"justMyCode": true
},
{
"args": ["--nf", "--lf"],
"name": "PyTest New and Failing",
"type": "python",
"request": "launch",
"module": "pytest",
"justMyCode": true
},
]
}</pre>
</div>
<!-- endregion -->
<span style='font-size: 3em; float: right; margin-left: 5px;'>😁</span>
<p>
You can set breakpoints in your code and debug your tests this way.
</p>
<!-- endregion -->
C and C++ Online and On Ubuntu2023-05-12T00:00:00-04:00https://mslinn.github.io/blog/2023/05/12/c<!-- #region intro -->
<div class="one_column right quartersize">
<div class='imgWrapper imgFlex inline' style=' '>
<figure>
<a href='/blog/c/Lattice_C_8086_1982.pdf' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/c/lattice_c.webp" type="image/webp">
<source srcset="/blog/c/lattice_c.png" type="image/png">
<img alt='Lattice C'
class="imgImg rounded shadow"
src="/blog/c/lattice_c.png"
style='width: 100%; '
title='Lattice C'
/>
</picture>
</a>
<figcaption class='imgFigCaption '>
<a href="/blog/c/Lattice_C_8086_1982.pdf" target='_blank' >
Lattice C
</a>
</figcaption>
</figure>
</div>
<div class='imgWrapper imgFlex inline' style=' '>
<figure>
<picture class='imgPicture'>
<source srcset="/blog/c/k&r.webp" type="image/webp">
<source srcset="/blog/c/k&r.png" type="image/png">
<img alt='K&R Book'
class="imgImg rounded shadow"
src="/blog/c/k&r.png"
style='width: 100%; '
title='K&R Book'
/>
</picture>
<figcaption class='imgFigCaption '>
K&R Book
</figcaption>
</figure>
</div>
<div class='imgWrapper imgFlex inline' style=' '>
<figure>
<a href='https://developerinsider.co/download-turbo-c-for-windows-7-8-8-1-and-windows-10-32-64-bit-full-screen/' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/c/turbo_c_2.webp" type="image/webp">
<source srcset="/blog/c/turbo_c_2.png" type="image/png">
<img alt='Borland Turbo C'
class="imgImg rounded shadow"
src="/blog/c/turbo_c_2.png"
style='width: 100%; '
title='Borland Turbo C'
/>
</picture>
</a>
<figcaption class='imgFigCaption '>
<a href="https://developerinsider.co/download-turbo-c-for-windows-7-8-8-1-and-windows-10-32-64-bit-full-screen/" target='_blank' >
Borland Turbo C
</a>
</figcaption>
</figure>
</div>
<div class='imgWrapper imgFlex inline' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/zamples.webp" type="image/webp">
<source srcset="/blog/images/zamples.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/zamples.png"
style='width: 100%; '
/>
</picture>
</div>
</div>
<p>
Recently I decided to brush up on my C programming.
</p>
<p>
I first discovered C during a visit in 1978 to the
<a href='https://cs.berkeley.edu/' target='_blank' rel='nofollow'>Computer Science department of University of California, Berkeley</a>,
where I also encounted UNIX for the first time.
</p>
<p>
From about 1983 to 1985 I was the unofficial Canadian distributor of the
<a href='/blog/c/Lattice_C_8086_1982.pdf'>Lattice C compiler</a>.
I used that compiler for most of the programming work I was doing on PCs at the time.
FedEx Express Canada did not come into being until 1987,
UPS was even worse with the Canadian border than it is now,
and Canada Post took forever to bring packages across the border.
I bought in bulk at a discount, and sold at list price.
Distribution is all about time and place.
</p>
<p>
I also sold the first edition of the K&R book (“The C Programming Language”) via mail order,
and I provided technical support,
which brought in interesting consulting work.
</p>
<p>
The Borland Turbo C compiler, released in 1990, was the original IDE.
It was eventually replaced by Borland C++ compiler, which also compiled C.
In 2005 I became the product marketing manager for most of Borland’s compilers.
One of my responsibilities was to give free copies to book authors and members of the trade press.
Borland C++ shipped in a massive box that was responsible for a lot of dead trees.
</p>
<p>
The C language has evolved since K&R days.
The current and previous iterations are well summarized on
<a href='https://en.cppreference.com/w/c/language' target='_blank' rel='nofollow'><code>cppreference.com</code></a>,
which also provides an online C/C++ IDE for sample code.
</p>
<!-- endregion -->
<!-- #region Revisions -->
<h2 id="revisions">Revision History</h2>
<p>
1978:
<b>K&R book</b> first published.
</p>
<p>
1989:
<b>C89</b>, also known as ANSI X3. 159-1989, or ANSI C.
</p>
<p>1995:
<b>NA1</b> (aka C94 and C95) added support for wide-characters, wide-strings, and international keyboard layouts.
</p>
<p>1999:
<b>C99</b> added restricted pointers, variable-length arrays, flexible array members,
complex numbers support, type-generic math, <code>long long int</code>, extended identifiers,
hexadecimal floating-point constants, compound literals, designated initializers,
single line comments, extended integer type, the ability to mix declarations and code,
variadic macros, new math functions, inline functions, boolean types,
<code>_Pragma</code> and standard pragmas, and <code>VA_COPY</code>.
C99 removed implicit function declaration.
</p>
<p>2011:
<b>C11</b> added concurrency support,
type-generic expressions, alignment-related facilities,
static assertion, Unicode support, floating-point characteristic macros,
no-return functions, anonymous structures and unions,
bound-checking, and reentrancy functions.
</p>
<p>2018:
<b>C18</b> addressed defects in C11 without introducing new language features.
</p>
<p>2023:
The most recent publicly available draft of the next release,
<a href='https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf' target='_blank' rel='nofollow'><b>C23</b></a>,
was released on April 1, 2023.
Unfortunately, that document was written for insiders, and is very difficult to read.
Happily, JeanHeyd Meneide, the Project Editor for ISO/IEC JTC1 SC22 WG14 - Programming Languages, C, wrote an
<a href='https://thephd.dev/c23-is-coming-here-is-what-is-on-the-menu' target='_blank' rel='nofollow'>easy-to-read <wbr>summary of C23</a>.
</p>
<h2 id="linux">Linux Is Written in C</h2>
<p>
It has been <a href='https://www.zdnet.com/article/linus-torvalds-prepares-to-move-the-linux-kernel-to-modern-c/' target='_blank' rel='nofollow'>reported</a>
that Linux was originally written in C89, but Linux moved to C11 in 2022.
I am curious as to why C18 is not used, since C18 just contains bug fixes for C11.
Most of the Linux kernel code is written using the
<a href='https://gcc.gnu.org/onlinedocs/gcc-10.2.0/gcc/C-Extensions.html#C-Extensions' target='_blank' rel='nofollow'>GNU extensions of GCC</a>.
</p>
<!-- endregion -->
<!-- #region online -->
<h2 id="online">Online IDEs</h2>
<p>
I pioneered online programming with
SMX Explorer in 1994,
<a href='/resume/history/jspexplorer/developerWorks/'>JSP Explorer</a> in 2000,
and supported most available languages in 2002 with
<a href='/blog/2004/12/09/live-code-examples-for-j2se-6-mustang.html'>Zamples.com</a>.
</p>
<p>
Now it seems that every day another online programming option becomes available.
Many are free:
</p>
<ul>
<li>
<a href='https://coliru.stacked-crooked.com/' target='_blank' rel='nofollow'>Coliru</a>
is used by <code>cppreference.com</code>;
<a href='https://github.com/StackedCrooked/coliru' target='_blank' rel='nofollow'>GitHub project</a>
</li>
<li>
<a href='https://github.com/features/codespaces' target='_blank' rel='nofollow'>GitHub Codespaces</a>
is tightly integrated with Microsoft Azure.
</li>
<li><a href='https://www.jetbrains.com/space/' target='_blank' rel='nofollow'>JetBrains Space</a></li>
<li>
<a href='https://www.onlinegdb.com/online_c_compiler' target='_blank' rel='nofollow'>OnlineGDB</a>
claims to be to first Online IDE, but the
<a href='https://web.archive.org/web/20160815000000*/OnlineGDB.com' target='_blank' rel='nofollow'>WayBack Machine</a>
shows that this site was created in 2016.
</li>
<li><a href='https://www.programiz.com/c-programming/online-compiler/' target='_blank' rel='nofollow'>Programiz</a></li>
<li><a href='https://www.tutorialspoint.com/compile_c_online.php' target='_blank' rel='nofollow'>TutorialsPoint CodingGround</a></li>
<li><a href='https://replit.com/languages/c' target='_blank' rel='nofollow'>Replit</a></li>
<li><a href='https://onecompiler.com/c' target='_blank' rel='nofollow'>OneCompiler</a></li>
<li>... and <a href='https://www.google.com/search?q=online+c+programming' target='_blank' rel='nofollow'>many more</a> ...
<a href='https://arne-mertz.de/2017/05/online-compilers/' target='_blank' rel='nofollow'>even more!</a></li>
</ul>
<!-- endregion -->
<!-- #region install -->
<h2 id="install">Installing C/C++ on WSL/Ubuntu</h2>
<p>
Online programming is fine for learning,
but I prefer to be independent and self-reliant whenever practical for doing actual work.
</p>
<h3 id="cli">Command-Line Toolchain</h3>
<p>
To program in C or C++ on Ubuntu using the traditional Linux command-line tool­chain,
install the <a href='https://packages.debian.org/sid/build-essential' target='_blank' rel='nofollow'><code>build-essential</code></a>
Debian meta-package on Ubuntu.
It includes the <a href='https://gcc.gnu.org/' target='_blank' rel='nofollow'>GNU Compiler Collection (GCC)</a>,
which is a collection of compilers and libraries for
C, C++, Objective-C, Fortran, Ada, Go, and D programming languages.
It also includes <a href='https://linux.die.net/man/1/make' target='_blank' rel='nofollow'><code>make</code></a> and
<a href='https://packages.debian.org/sid/libc6.1-dev' target='_blank' rel='nofollow'><code>libc6.1-dev</code></a>,
the GNU C development libraries and header files.
</p>
<p>
You probably should also install
<a href='https://www.sourceware.org/gdb/' target='_blank' rel='nofollow'><code>gdb</code></a>, the GNU Project Debugger,
and the <a href='https://packages.debian.org/unstable/manpages-dev' target='_blank' rel='nofollow'>manual pages</a>
about using GNU/Linux for development.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddbadfd25c859'><button class='copyBtn' data-clipboard-target='#iddbadfd25c859' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo apt install build-essential gdb manpages-dev</pre>
</div>
<!-- endregion -->
<p>
Now you are able to compile and run C programs at the command line:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id542528725e91'><button class='copyBtn' data-clipboard-target='#id542528725e91' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>mkdir test && cd test
<span class='unselectable'>$ </span>cat >test.c <<EOF
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
}
EOF
<span class='unselectable'>$ </span>gcc test.c -o test && ./test
<span class='unselectable'>Hello, World! </span></pre>
</div>
<!-- endregion -->
<h3 id="vscode">Visual Studio Code</h3>
<p>
Visual Studio Code is a high-quality (and free) IDE.
To work with C and C++ in Visual Studio Code, install the
<a href='https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools' target='_blank' rel='nofollow'>Microsoft C/C++</a>
extension.
</p>
<!-- endregion -->
Don't Poke the Bear2023-03-03T00:00:00-05:00https://mslinn.github.io/blog/2023/03/03/lego-tm<!-- #region -->
<h2 style="margin-bottom: 2em; margin-top: 2em;">
To the developers of the excellent ‘<a href='https://github.com/go-acme/lego/' class='code>lego</span>' target='_blank' rel='nofollow'><span</a>’ GitHub project
</h2>
<div class='imgWrapper imgFlex inline' style=' '>
<a href='https://github.com/go-acme/lego' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/ddns/lego-logo.webp" type="image/webp">
<source srcset="/blog/images/ddns/lego-logo.png" type="image/png">
<img
class="imgImg "
src="/blog/images/ddns/lego-logo.png"
style='width: 100%; '
/>
</picture>
</a>
</div>
<p>
The name of this project is problematic, and you
<a href='https://github.com/go-acme/lego/issues/470' target='_blank' rel='nofollow'>publicly dismissed</a>
the issue when it was brought to your attention.
While ignorance of the law is no defense;
demonstrably flaunting another party’s rights never has positive consequences.
</p>
<p>
<a href='https://www.lego.com/en-sg/service/help/fun-for-fans/behind-the-scenes/our-company/commercial-use-of-lego-products-bltb4ad5090ec5e4b08' target='_blank' rel='nofollow'>The LEGO Group</a>
has not taken legal action yet,
but the laws of most countries (including the US) are that
<a href='https://www.forbes.com/sites/oliverherzfeld/2013/02/28/failure-to-enforce-trademarks-if-you-snooze-do-you-lose/?sh=1d67ec326c22' target='_blank' rel='nofollow'>if a trademark is not defended, then the trademark is at risk of being lost</a>.
</p>
<p>
You should expect to receive a legal notice at some point.
Not because the trademark owners are evil, but because
they have a duty to their company,
and the well-being of their employees, and a duty to their shareholders.
</p>
<p>
Your disregard for the LEGO Group's rights constituted a real and present danger to them,
their employees and shareholders.
They are compelled to address threats against their well-being.
</p>
<div class='imgWrapper imgBlock right fullsize' style=' '>
<figure>
<a href='https://www.flickr.com/photos/angietigger/6894972004/' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/bearpokelego.webp" type="image/webp">
<source srcset="/blog/images/bearpokelego.png" type="image/png">
<img alt='‘lego bear’ by angie <br> CC BY-NC-SA 2.0'
class="imgImg rounded shadow"
src="/blog/images/bearpokelego.png"
style='width: 100%; '
title='‘lego bear’ by angie <br> CC BY-NC-SA 2.0'
/>
</picture>
</a>
<figcaption class='imgFigCaption fullsize'>
<a href="https://www.flickr.com/photos/angietigger/6894972004/" target='_blank' >
‘lego bear’ by angie <br> CC BY-NC-SA 2.0
</a>
</figcaption>
</figure>
</div>
<h2 id="think">Think</h2>
<p>
Not only must a trademark holder defend their trademark, they must be seen to defend it.
That means taking action that is publicly visible.
For example, they might include GitHub and maybe even Microsoft in the legal action against you.
</p>
<div class="pullQuote">
No-one would have reason to defend you
</div>
<p>
The LEGO Group would likely include GitHub/Microsoft in the case, even though they know that
<a href='https://www.eff.org/issues/cda230' target='_blank' rel='nofollow'>Section 230</a> would probably be found to apply (in due course),
and GitHub/Microsoft would not be found to be liable –
after they spent several million dollars in a successful legal defense.
</p>
<p>
It is easy to understand the motivation for the LEGO Group deliberately ‘losing’
at the attempt to ensnare GitHub/Microsoft in this court case.
You would have caused GitHub/Microsoft an unnecessary and pointless waste of time and resources,
and it would be shown that you knew better.
Remember, you have already been publicly been warned that there would likely be consequences –
yet you chose to ignore the warning.
</p>
<p>
This would likely motivate GitHub/Microsoft to protect themselves against similar misguided behavior of other GitHub users.
Restrictive policies and procedures would likely get implemented,
and the entire world would know that the resulting degraded GitHub experience was your fault.
You would be vilified by every other GitHub user.
Not a great thing for your resume.
</p>
<p>
... And GitHub/Microsoft might ask you to pay their legal expenses.
</p>
<h2 id="family">Do You Have Families?</h2>
<p>
Please understand that because you are acting as an informal collection of individuals,
everyone who ever contributed to this project is exposed to signficant financial risk.
The LEGO Group would probably not attempt to narrow down the guilty individuals –
identifying the guilty and assigning punishment would be the outcomes of the court case.
Instead, they would likely include every contributor in the litigation; that means every committer.
</p>
<p>
You are risking the financial well-being of your families ...
just because you like a cute play on words?
</p>
<p class="pullQuoteFull">
Rational people would not expose themselves to significant risk without any prospect of signficant benefit,
and when not faced with dire necessity.
</p>
<p>
If you presently balk at paying a lawyer to advise you on this matter,
that cost will be as nothing compared to the world of hurt you are likely
to find yourselves in if you do not change the name of your project.
</p>
<p>
From a technical aspect, you have done excellent work on your project,
and you are freely providing the world a valuable benefit.
Unfortunately, you trampled the rights of another in doing so...
and for no reason.
Please act on this before the inevitable negative consequences arise.
Do not poke the bear because you are curious to see what it will do.
</p>
<div class='imgWrapper imgFlex right' style='width: 45%; '>
<picture class='imgPicture'>
<source srcset="/blog/images/bearpoke.webp" type="image/webp">
<source srcset="/blog/images/bearpoke.png" type="image/png">
<img
class="imgImg "
src="/blog/images/bearpoke.png"
style='width: 100%; '
/>
</picture>
</div>
<p class="left alert rounded shadow" style="width: 50%;">
The wheels of corporate law grind slowly, and huge amounts of money are spent.
According to the path you are now on,
the committers of this project may be subjected to immense financial and mental burdens.
Broken families and suicides may result.
<br><br>
Please heed this warning.
If nothing bad has ever happened to you yet in life,
rest assured that
<a href='https://brewminate.com/clotho-lachesis-and-atropos-the-three-sisters-of-fate-in-ancient-greek-mythology/' target='_blank' rel='nofollow'>Lachesis, Atropos and Clotho</a>
will inevitably make themselves known.
Do not let arrogance and pride ruin you.
</p>
<div class="clear pullQuoteFull" style="margin-top: 2.5em;">
I am trying to help you people.<br>
Because you have done good work,<br>
and it would be a shame to see you suffer.
</div>
<h2 id="disclaim" class="clear">Disclaimer</h2>
<p>
I have no relationship with the LEGO Group, or any individuals associated with them.
Please consider this as a notice freely offered for public service.
</p>
<p>
Although <a href='/softwareexpert/'>I have experience working as an expert witness in the US Federal legal system as a software expert</a>,
I am not a lawyer and I have no legal training.
However, I have seen corporate legal machinery at work many times, up close.
I have spent thousands of hours working with corporate attorneys in the US and Europe.
My clients have included Adobe, Amazon/AWS, Apple, and SAP.
</p>
<!-- endregion -->
Letsencrypt/ACME Wildcard SSL Certificates by Lego2023-03-02T00:00:00-05:00https://mslinn.github.io/blog/2023/03/02/lego<style>
.redButton {
color:white;
background-color:#e04433;
padding: 2px;
font-family: Helvetica, Arial, Sans-Serif;
font-weight: bold;
}
</style>
<!-- #region intro -->
<div class='imgWrapper imgFlex right' style='width: 40%; '>
<a href='https://letsencrypt.org/' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/ddns/letsencrypt-logo.webp" type="image/webp">
<source srcset="/blog/images/ddns/letsencrypt-logo.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/ddns/letsencrypt-logo.png"
style='width: 100%; '
/>
</picture>
</a>
</div>
<p>
Letsencrypt certificates are only valid for
<a href='https://letsencrypt.org/docs/faq/#what-is-the-lifetime-for-let-s-encrypt-certificates-for-how-long-are-they-valid' target='_blank' rel='nofollow'>90 days</a>.
It is common practice to renew them every 60 days,
a task that one should automate when the website is first published.
</p>
<p>
I wrote about
<a href='/blog/2022/06/15/certbot.html'>setting up wildcard SSL certificates with Nginx</a> 9 months ago.
That post included a demonstration of how to create an SSL certificate manually,
but did not discuss how to maintain it.
</p>
<p>
This blog post discusses how to automatically generate and maintain the SSL certificate using
<a href='https://github.com/go-acme/lego' target='_blank' rel='nofollow'><code>lego</code></a>,
a Letsencrypt/ACME client and library written in Go
that has become popular since I last wrote about this topic.
</p>
<p>
<code>Lego</code> handles many moving parts transparently.
It is <i>much</i> simpler than using DNS delegates with
<a href='https://github.com/joohoi/acme-dns' target='_blank' rel='nofollow'><code>acme-dns</code></a>.
</p>
<p>
We’ll start by briefly discussing some background information.
</p>
<!-- endregion -->
<!-- #region Letsencrypt certbot and wildcard ssl certs -->
<h2 id="certbot" class="clear">Letsencrypt’s <span class="code">Certbot</span> and Wildcard SSL Certificates</h2>
<p>
You must prove to Letsencrypt that you control the DNS for a domain before
it issues a wildcard SSL certificate for that domain.
<a href='https://certbot.eff.org/' target='_blank' rel='nofollow'>Letsencrypt’s <code>certbot</code></a> currently uses the
<a href='https://cert-manager.io/docs/configuration/acme/dns01/' target='_blank' rel='nofollow'><code>DNS-01</code> challenge</a>
for this purpose.
</p>
<h3 id="DNS-01">DNS-01 Challenge</h3>
<p>
The DNS-01 challenge requires that DNS <code>TXT</code> records for the domain be
created with specific values as part of the authentication mechanism.
Several of these <code>TXT</code> records can co-exist among the DNS records for a domain.
The name of the <code>TXT</code> records is always <code>_acme-challenge</code>.
</p>
<p>
When <code>certbot</code> requests that a new wildcard SSL certificate be created by Letsencrypt,
it sends DNS-01 <code>TXT</code> queries to the primary DNS.
The Letsencrypt service verifies that the required <code>_acme-challenge</code> <code>TXT</code>
records are available with the correct values.
</p>
<p>
<a href='https://www.eff.org/deeplinks/2018/02/technical-deep-dive-securing-automation-acme-dns-challenge-validation' target='_blank' rel='nofollow'>A Technical Deep Dive: Securing the Automation of ACME DNS Challenge Validation</a>,
published by the Electronic Frontier Foundation, explains how this works in detail.
</p>
<p>
<code>Certbot</code> plugins have been created for various DNS providers to make this process easier.
<code>Lego</code> is a higher-level program that makes the entire process even easier.
</p>
<!-- endregion -->
<!-- #region agenda -->
<h2 id="agenda">Agenda</h2>
<p>
The high-level outline of the remainder of this blog is:
</p>
<ol>
<li><a href='#go'>Install <code>go</code> language support.</a></li>
<li><a href='#install'>Install <code>lego</code>.</a></li>
<li><a href='#gen'>Generate the wildcard SSL certificate.</a></li>
<li><a href='#provide'>Provide the certificate</a> to your production web server.</li>
<li><a href='#restart'>Restart the webserver</a> and verify the new certificate is served.</li>
<li><a href='#crontab'>Add a new entry to <code>crontab</code></a> to automate the SSL certificate generation.</li>
</ol>
<!-- endregion -->
<!-- #region Install Go Language Support -->
<h2 id="go">Install Go Language Support</h2>
<p>
<code>Lego</code> is written in Go.
Ensure that <a href='https://go.dev/doc/install' target='_blank' rel='nofollow'>Go language support</a> is installed.
For Ubuntu Linux (and the default WSL distro), type:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfe28cda50649'><button class='copyBtn' data-clipboard-target='#idfe28cda50649' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install golang-go
<span class='unselectable'>The following NEW packages will be installed:
golang-1.18-go golang-1.18-src golang-go golang-src
0 upgraded, 4 newly installed, 0 to remove and 0 not upgraded.
Need to get 82.2 MB of archives.
After this operation, 436 MB of additional disk space will be used.
Do you want to continue? [Y/n]
Get:1 http://archive.ubuntu.com/ubuntu jammy/main amd64 golang-1.18-src all 1.18.1-1ubuntu1 [16.2 MB]
Get:2 http://archive.ubuntu.com/ubuntu jammy/main amd64 golang-1.18-go amd64 1.18.1-1ubuntu1 [66.0 MB]
Get:3 http://archive.ubuntu.com/ubuntu jammy/main amd64 golang-src all 2:1.18~0ubuntu2 [4438 B]
Get:4 http://archive.ubuntu.com/ubuntu jammy/main amd64 golang-go amd64 2:1.18~0ubuntu2 [41.8 kB]
Fetched 82.2 MB in 2s (33.6 MB/s)
Selecting previously unselected package golang-1.18-src.
(Reading database ... 173763 files and directories currently installed.)
Preparing to unpack .../golang-1.18-src_1.18.1-1ubuntu1_all.deb ...
Unpacking golang-1.18-src (1.18.1-1ubuntu1) ...
Selecting previously unselected package golang-1.18-go.
Preparing to unpack .../golang-1.18-go_1.18.1-1ubuntu1_amd64.deb ...
Unpacking golang-1.18-go (1.18.1-1ubuntu1) ...
Selecting previously unselected package golang-src.
Preparing to unpack .../golang-src_2%3a1.18~0ubuntu2_all.deb ...
Unpacking golang-src (2:1.18~0ubuntu2) ...
Selecting previously unselected package golang-go:amd64.
Preparing to unpack .../golang-go_2%3a1.18~0ubuntu2_amd64.deb ...
Unpacking golang-go:amd64 (2:1.18~0ubuntu2) ...
Setting up golang-1.18-src (1.18.1-1ubuntu1) ...
Setting up golang-src (2:1.18~0ubuntu2) ...
Setting up golang-1.18-go (1.18.1-1ubuntu1) ...
Setting up golang-go:amd64 (2:1.18~0ubuntu2) ...
Processing triggers for man-db (2.10.2-1) ...
Scanning processes...
Scanning processor microcode...
Scanning linux images...
Failed to retrieve available kernel versions.
Failed to check for processor microcode upgrades.
No services need to be restarted.
No containers need to be restarted.
No user sessions are running outdated binaries.
No VM guests are running outdated hypervisor (qemu) binaries on this host. </span></pre>
</div>
<p>
View the version of <code>go</code> that was installed:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfb61cc5427d9'><button class='copyBtn' data-clipboard-target='#idfb61cc5427d9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>go version
<span class='unselectable'>go version go1.19.2 linux/amd64 </span></pre>
</div>
<!-- endregion -->
<!-- #region Installing Lego -->
<h2 id="install">Installing Lego</h2>
<div class='imgWrapper imgFlex inline' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/ddns/lego-logo.webp" type="image/webp">
<source srcset="/blog/images/ddns/lego-logo.png" type="image/png">
<img
class="imgImg "
src="/blog/images/ddns/lego-logo.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
The official <code>lego</code> installation instructions are
<a href='https://go-acme.github.io/lego/installation/' target='_blank' rel='nofollow'>here</a>.
They do not mention Ubuntu instructions, but the
<a href='https://ubuntu.pkgs.org/22.04/ubuntu-universe-amd64/lego_4.1.3-3ubuntu1_amd64.deb.html' target='_blank' rel='nofollow'><code>lego</code> package for Ubuntu</a>
can be installed in the usual way:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb273f394ea31'><button class='copyBtn' data-clipboard-target='#idb273f394ea31' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install lego
<span class='unselectable'>Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following NEW packages will be installed:
lego
0 upgraded, 1 newly installed, 0 to remove and 7 not upgraded.
Need to get 5322 kB of archives.
After this operation, 19.7 MB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu jammy-updates/universe amd64 lego amd64 4.1.3-3ubuntu1.22.04.1 [5322 kB]
Fetched 5322 kB in 1s (10.2 MB/s)
Selecting previously unselected package lego.
(Reading database ... 86044 files and directories currently installed.)
Preparing to unpack .../lego_4.1.3-3ubuntu1.22.04.1_amd64.deb ...
Unpacking lego (4.1.3-3ubuntu1.22.04.1) ...
Setting up lego (4.1.3-3ubuntu1.22.04.1) ... </span></pre>
</div>
<p>
Here is the <code>lego</code> help message:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5cd0b7c9e0db'><button class='copyBtn' data-clipboard-target='#id5cd0b7c9e0db' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>lego -h
<span class='unselectable'>NAME:
lego - Let's Encrypt client written in Go<br/>
USAGE:
lego [global options] command [command options] [arguments...]<br/>
VERSION:
dev<br/>
COMMANDS:
run Register an account, then create and install a certificate
revoke Revoke a certificate
renew Renew a certificate
dnshelp Shows additional help for the '--dns' global option
list Display certificates and accounts information.
help, h Shows a list of commands or help for one command<br/>
GLOBAL OPTIONS:
--domains value, -d value Add a domain to the process. Can be specified multiple times.
--server value, -s value CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. (default: "https://acme-v02.api.letsencrypt.org/directory")
--accept-tos, -a By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.
--email value, -m value Email used for registration and recovery contact.
--csr value, -c value Certificate signing request filename, if an external CSR is to be used.
--eab Use External Account Binding for account registration. Requires --kid and --hmac.
--kid value Key identifier from External CA. Used for External Account Binding.
--hmac value MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.
--key-type value, -k value Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384. (default: "ec256")
--filename value (deprecated) Filename of the generated certificate.
--path value Directory to use for storing the data. (default: "/mnt/_/work/lego/.lego") [$LEGO_PATH]
--http Use the HTTP challenge to solve challenges. Can be mixed with other types of challenges.
--http.port value Set the port and interface to use for HTTP based challenges to listen on.Supported: interface:port or :port. (default: ":80")
--http.proxy-header value Validate against this HTTP header when solving HTTP based challenges behind a reverse proxy. (default: "Host")
--http.webroot value Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge
--http.memcached-host value Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.
--tls Use the TLS challenge to solve challenges. Can be mixed with other types of challenges.
--tls.port value Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port. (default: ":443")
--dns value Solve a DNS challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.
--dns.disable-cp By setting this flag to true, disables the need to wait the propagation of the TXT record to all authoritative name servers.
--dns.resolvers value Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.
--http-timeout value Set the HTTP timeout value to a specific value in seconds. (default: 0)
--dns-timeout value Set the DNS timeout value to a specific value in seconds. Used only when performing authoritative name servers queries. (default: 10)
--pem Generate a .pem file by concatenating the .key and .crt files together.
--cert.timeout value Set the certificate timeout value to a specific value in seconds. Only used when obtaining certificates. (default: 30)
--help, -h show help
--version, -v print the version </span></pre>
</div>
<!-- endregion -->
<!-- #region Generating the Wildcard SSL Certificate -->
<h2 id="gen">Generating the Wildcard SSL Certificate</h2>
<!-- #region /etc/environment -->
<h3 id="env"><span class="code">/etc/environment</span></h3>
<p>
My projects are defined in a directory tree,
which is pointed to by an environment variable called <code>work</code>.
This is helpful when working across many machines,
each of which has a different directory layout.
</p>
<p>
I define the <code>work</code> environment variable in the
<a href='https://askubuntu.com/a/866240/58760' target='_blank' rel='nofollow'>system-wide <code>/etc/environment</code></a> file,
where it affects all users, all scripts, and crontab entries:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>/etc/environment</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide3249f8fcb50'><button class='copyBtn' data-clipboard-target='#ide3249f8fcb50' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
work=/mnt/_/work
sites=/mnt/_/www</pre>
</div>
<p>
<code>$work</code> is used in the script shown <a href='#certGenScalaCourses'>below</a>,
and that script will be launched via <code>crontab</code>,
which is why <code>/etc/environment</code> is used instead of <code>$HOME/.bashrc</code>.
</p>
<p class="alert rounded shadow">
WSL does not run <code>systemd</code> services, and WSL2 only runs them if they are enabled.
You can easily convert WSL instances into WSL2 instances.
<br><br>
Unless <code>systemd</code> starts automatically at boot time,
<code>/etc/environment</code> will not be processed.
<br><br>
Read Microsoft’s announcement to learn more:
<a href='https://devblogs.microsoft.com/commandline/systemd-support-is-now-available-in-wsl/' target='_blank' rel='nofollow'><code>Systemd</code> support is now available in WSL!</a>
</p>
<!-- endregion -->
<!-- #region generation -->
<h3 id="dir">Generation</h3>
<p>
When working with the <code>lego</code> CLI with DNS authentication,
a directory is needed to hold all the files that are required.
I created a directory for this purpose at <code>$work/lego</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6142bc6e8add'><button class='copyBtn' data-clipboard-target='#id6142bc6e8add' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>mkdir $work/lego<br>
<span class='unselectable'>$ </span>cd $work/lego</pre>
</div>
<p>
The <code>lego --dns namecheap</code> option connects to Namecheap, which is my DNS provider,
and uses the Namecheap DNS for the <code>DNS-01</code> challenge.
Lego supports <a href='https://go-acme.github.io/lego/dns/#dns-providers' target='_blank' rel='nofollow'>many other DNS providers</a>.
</p>
<!-- #region Namecheap Users -->
<div class="alert rounded shadow">
<h2>Namecheap Users</h2>
<p>
The Namecheap API documentation is <a href='https://www.namecheap.com/support/api/intro/' target='_blank' rel='nofollow'>here</a>.
For some reason, that page reloads to a different page, which is annoying.
Press the <kbd>Esc</kbd> key right after the page opens, so you can read it.
</p>
<p>
Please also read the
<a href='https://www.namecheap.com/support/knowledgebase/article.aspx/9739/63/api-faq/#t' target='_blank' rel='nofollow'>FAQ</a>.<br>
Enable the Namecheap API
<a href='https://ap.www.namecheap.com/settings/tools/apiaccess/' target='_blank' rel='nofollow'>here</a>.<br>
You could enable the Namecheap sandbox API
<a href='https://ap.www.sandbox.namecheap.com/settings/tools/apiaccess/' target='_blank' rel='nofollow'>here</a>.
</p>
<p>
Here is a short script that displays your current public IP address:
</p>
<div class="codeLabel"><a href='data:text/plain;charset=UTF-8,whatismyip' download='whatismyip'
title='Click on the file name to download the file'>whatismyip</a>
</div>
<pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id55ba1f1e4ca8"><button class='copyBtn' data-clipboard-target='#id55ba1f1e4ca8'title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash
dig +short myip.opendns.com @resolver1.opendns.com
</pre>
<p style="margin-top: 1em;">
If your modem goes offline,
Namecheap’s DNS will assign a new IP address the next time the modem reconnects.
That will cause the scripts shown below to stop working until you manually provide the new address to the Namecheap API.
Annoying!
Be sure to plug your modem into a UPS to maintain the IP address even during short outages, power brownouts and voltage dropouts.
A sine-wave UPS is strongly preferred, since modems are sensitive to low-quality power.
</p>
<p>
The Namecheap API needs 2 environment variables for authentication
(<code>NAMECHEAP_API_USER</code> and <code>NAMECHEAP_API_KEY</code>).
Here is how I defined those environment variables so they were available for the
<code>lego</code> process launched by <code>sudo</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id09654a60aec7'><button class='copyBtn' data-clipboard-target='#id09654a60aec7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>export NAMECHEAP_API_USER=asdf<br>
<span class='unselectable'>$ </span>export NAMECHEAP_API_KEY=asdfasdf</pre>
</div>
</div>
<!-- endregion -->
<p>
To generate a wildcard certificate, you must specify the <code>--domains</code> option twice:
once with the domain name, and once for all subdomains, like this:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2e7f7d9ab5ee'><button class='copyBtn' data-clipboard-target='#id2e7f7d9ab5ee' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>lego \
--accept-tos \
--dns namecheap \
--domains="scalacourses.com" \
--domains="*.scalacourses.com" \
--email="mslinn@scalacourses.com" \
<span class="bg_yellow">run</span>
<span class='unselectable'>2023/03/01 13:40:03 [INFO] [*.scalacourses.com] acme: Obtaining bundled SAN certificate
2023/03/01 13:40:03 [INFO] [*.scalacourses.com] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/207409481036
2023/03/01 13:40:03 [INFO] [*.scalacourses.com] acme: use dns-01 solver
2023/03/01 13:40:03 [INFO] [*.scalacourses.com] acme: Preparing to solve DNS-01
2023/03/01 13:40:04 [INFO] [*.scalacourses.com] acme: Trying to solve DNS-01
2023/03/01 13:40:04 [INFO] [*.scalacourses.com] acme: Checking DNS record propagation using [172.19.176.1:53]
2023/03/01 13:40:19 [INFO] Wait for propagation [timeout: 1h0m0s, interval: 15s]
2023/03/01 13:40:20 [INFO] [*.scalacourses.com] acme: Waiting for DNS record propagation.
2023/03/01 13:40:35 [INFO] [*.scalacourses.com] acme: Waiting for DNS record propagation.
2023/03/01 13:40:56 [INFO] [*.scalacourses.com] The server validated our request
2023/03/01 13:40:56 [INFO] [*.scalacourses.com] acme: Cleaning DNS-01 challenge
2023/03/01 13:40:56 [INFO] [*.scalacourses.com] acme: Validations succeeded; requesting certificates
2023/03/01 13:40:57 [INFO] [*.scalacourses.com] Server responded with a certificate. </span></pre>
</div>
<!-- endregion -->
<p>
The wildcard SSL certificate and associated files are generated into the <code>.lego/certificates</code> directory.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6455b9f8f8cc'><button class='copyBtn' data-clipboard-target='#id6455b9f8f8cc' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ls -alF .lego/certificates
<span class='unselectable'>total 12
drwx------ 1 mslinn mslinn 4096 Mar 1 13:40 ./
drwx------ 1 mslinn mslinn 4096 Mar 1 13:15 ../
-rw------- 1 mslinn mslinn 5325 Mar 1 13:40 scalacourses.com.crt
-rw------- 1 mslinn mslinn 3751 Mar 1 13:40 scalacourses.com.issuer.crt
-rw------- 1 mslinn mslinn 239 Mar 1 13:40 scalacourses.com.json
-rw------- 1 mslinn mslinn 227 Mar 1 13:40 scalacourses.com.key </span></pre>
</div>
<!-- endregion -->
<ul>
<li>
<code>scalacourses.com.crt</code> is the server certificate that nginx needs,
already combined with the CA certificate.
</li>
<li><code>scalacourses.com.key</code> is the private key for the server certificate.</li>
<li><code>scalacourses.com.issuer.crt</code> is the issuing Certificate Authority's certificate.</li>
<li>
<code>scalacourses.com.json</code> contains JSON encoded meta information.
It looked like this:
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>$work/lego/.lego/certificates/scalacourses.com.json</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc597e1851f5d'><button class='copyBtn' data-clipboard-target='#idc597e1851f5d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>{
"domain": "*.scalacourses.com",
"certUrl": "https://acme-v02.api.letsencrypt.org/acme/cert/3939493849340275024024",
"certStableUrl": "https://acme-v02.api.letsencrypt.org/acme/cert/738378373923492384932874932"
}</pre>
</div>
</li>
</ul>
<p>
Using the value for <code>certStableUrl</code> in the above JSON file,
we can view the generated (and combined) certificate that was saved on <code>letsencrypt.org</code>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ideca6d90443d7'><button class='copyBtn' data-clipboard-target='#ideca6d90443d7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>wget -O aw.crt \
https://acme-v02.api.letsencrypt.org/acme/cert/03b099f52b4841db17e035c5c2b390f0219b</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region certGenScalaCourses script -->
<h2 id="certGenScalaCourses"><span class="code">certGenScalaCourses</span> Script</h2>
<p>
60 days from now, when the certificate should be renewed,
the last parameter passed to <code>lego</code> in the above command line should be changed from
<code class="bg_yellow">run</code> to <code>renew</code>.
Here is a bash script that you can edit for this purpose;
at the end of this article this script is incorporated into a new <code>crontab</code> entry:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>$work/lego/certGenScalaCourses</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc16df4d395b7'><button class='copyBtn' data-clipboard-target='#idc16df4d395b7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash
export NAMECHEAP_API_USER=asdf
export NAMECHEAP_API_KEY=asdfasdf
ACTION=run
if [ -f .lego/certificates/scalacourses.com.crt ]; then
ACTION=renew
fi
cd $work/lego
lego \
--accept-tos \
--dns namecheap \
--domains="scalacourses.com" \
--domains="*.scalacourses.com" \
--email="mslinn@scalacourses.com" \
"$ACTION"
sudo systemctl reload nginx</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region Viewing a Certificate -->
<h2 id="view">Viewing a Certificate</h2>
<p>
For Ubuntu 22.10, the filetype associations are defined in a hierarchy of files called <code>mimeapps.list</code>:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc3433bff95c0'><button class='copyBtn' data-clipboard-target='#idc3433bff95c0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>locate mimeapps.list
<span class='unselectable'>~/.config/mimeapps.list
~/.local/share/applications/mimeapps.list
/snap/core/14447/usr/share/applications/mimeapps.list
/snap/core/14784/usr/share/applications/mimeapps.list
/snap/core18/2679/usr/share/applications/mimeapps.list
/snap/core18/2697/usr/share/applications/mimeapps.list
/snap/core20/1778/usr/share/applications/mimeapps.list
/snap/core20/1822/usr/share/applications/mimeapps.list
/snap/core22/509/usr/share/applications/mimeapps.list
/snap/core22/522/usr/share/applications/mimeapps.list
/usr/share/gdm/greeter/applications/mimeapps.list </span></pre>
</div>
<!-- endregion -->
<p>
<code>/snap/<wbr>core22/<wbr>509/<wbr>usr/<wbr>share/<wbr>applications/<wbr>mimeapps.list</code>
contains the default association for <code>*.crt</code> files:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>/snap/core22/509/usr/share/applications/mimeapps.list</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id286396cba5c8'>[Default Applications]
x-scheme-handler/http=xdg-open.desktop
x-scheme-handler/https=xdg-open.desktop
x-scheme-handler/mailto=xdg-open.desktop
x-scheme-handler/help=xdg-open.desktop</pre>
</div>
<!-- endregion -->
<p>
The above associates a program called
<a href='https://linux.die.net/man/1/xdg-open' target='_blank' rel='nofollow'><code>xdg-open</code></a>
with CRT files.
</p>
<p>
Double-clicking on a <code>crt</code> file displayed by an Ubuntu file manager such as Nautilus causes
<code>xdg-open</code> to open the file.
You can also use the command line to open the file in <code>xdg-open</code>.
For example, typing the following causes <code>aw.crt</code> to open:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9427c425d97b'><button class='copyBtn' data-clipboard-target='#id9427c425d97b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>xdg-open aw.crt</pre>
</div>
<p>
This is what <code>xdg-open</code> displays:
</p>
<div class='imgWrapper imgFlex center' style='width: 478px; '>
<picture class='imgPicture'>
<source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.webp" type="image/webp">
<source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_2.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/ddns/ubuntuViewFileOfSslCert_2.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
As you can see, this is a combined certificate, containing not just one certificate, but an entire certificate chain.
Clicking on any of the red
<kbd class="redButton">> Details</kbd>
buttons causes more information to be displayed about the corresponding certificate in the chain.
</p>
<!-- endregion -->
<!-- #region warning -->
<div class="alert rounded shadow">
<h2>Warning</h2>
<p>
If you forget to include the domain, and just specify the wildcard for subdomains,
the certificate will not be valid for the domain.
For example, if above I had only specified one <code>--domains</code> option, like this:
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id35f3d2b5f6ae'>--domains="scalacourses.com"</pre>
</div>
... instead of
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id748676283543'>--domains="scalacourses.com" \<br>--domains="*.scalacourses.com"</pre>
</div>
... then that would be an error.
You can notice your error three ways:
</p>
<ol>
<li>
The name of the generated certificate will start with an underscore (<code>_</code>),
for example <code>_.scalacourses.<wbr>com.<wbr>crt</code>,
instead of being named <code>scalacourses.<wbr>com.<wbr>crt</code>.
</li>
<li>
When you examine the certificate,
the domain listed will show the subdomain wildcard, as shown below,
with a leading underscore asterisk (<code>*</code>).<br>
<div class='imgWrapper imgFlex center' style='width: 478px; '>
<picture class='imgPicture'>
<source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.webp" type="image/webp">
<source srcset="/blog/images/ddns/ubuntuViewFileOfSslCert_1.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/ddns/ubuntuViewFileOfSslCert_1.png"
style='width: 100%; margin-top: 2em;'
/>
</picture>
</div>
</li>
<li style="margin-bottom: 0; padding-bottom: 0;">
When you open the certificate, the section entitled <b>Subject Alternative Names</b>
will just contain the subdomain wildcard, like this:<br><br>
<b>Subject Alternative Names</b><br>
<code>DNS: *.scalacourses.com</code>
<br><br>Or just the domain, without a wildcard:<br><br>
<b>Subject Alternative Names</b><br>
<code>DNS: scalacourses.com</code><br><br>
Instead of both being present, like this:<br><br>
<b>Subject Alternative Names</b><br>
<code>DNS: *.scalacourses.com<br>
DNS: scalacourses.com</code>
</li>
</ol>
</div>
<!-- endregion -->
<!-- #region Provide the certificate to Nginx -->
<h2 id="provide">Provide the certificate to Nginx</h2>
<p>
<a href='/blog/2022/07/08/reverse-proxy.html#vs'>Previously</a>,
before working with <code>lego</code>, I used raw <code>certbot</code> to generate the SSL wildcard certificates.
That process generated files in <code>~/.certbot/<wbr>scalacourses.com/<wbr>config/<wbr>live/<wbr>scalacourses.com</code>:
</p>
<ul>
<li>
<code>fullchain.pem</code> (the
<a href='http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate' target='_blank' rel='nofollow'><code>ssl_certificate</code></a>)
</li>
<li>
<code>privkey.pem</code> (the
<a href='https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate_key' target='_blank' rel='nofollow'><code>ssl_certificate_key</code></a>)
</li>
</ul>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>From /etc/nginx/sites-enabled/scalacourses.com</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id81cefa40c18e'><button class='copyBtn' data-clipboard-target='#id81cefa40c18e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>ssl_certificate /home/mslinn/.certbot/scalacourses.com/config/live/scalacourses.com/fullchain.pem;
ssl_certificate_key /home/mslinn/.certbot/scalacourses.com/config/live/scalacourses.com/privkey.pem;</pre>
</div>
<p>
Now I need to modify <code>/etc/<wbr>nginx/<wbr>sites-enabled/<wbr>scalacourses.com</code>
to reference the files generated by <code>lego</code>:
</p>
<ul>
<li>
<code>scalacourses.com.crt</code> (the
<a href='http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate' target='_blank' rel='nofollow'><code>ssl_certificate</code></a>)
</li>
<li>
<code>scalacourses.com.key</code> (the
<a href='https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate_key' target='_blank' rel='nofollow'><code>ssl_certificate_key</code></a>)
</li>
</ul>
<p>
If the webserver had access to the directory containing the new wildcard certificate,
it would be simplest to provide the full path to the certificate and its key, like this:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Modified portion of /etc/nginx/sites-enabled/scalacourses.com</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id34cd7372b424'><button class='copyBtn' data-clipboard-target='#id34cd7372b424' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>ssl_certificate /mnt/_/work/lego/.lego/certificates/scalacourses.com.crt;
ssl_certificate_key /mnt/_/work/lego/.lego/certificates/scalacourses.com.key;</pre>
</div>
<!-- endregion -->
<!-- #region Restart Nginx -->
<h2 id="restart">Restart Nginx</h2>
<p>
I tested the <code>nginx</code> configuration:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf9c8c07913f3'><button class='copyBtn' data-clipboard-target='#idf9c8c07913f3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo nginx -t
<span class='unselectable'>nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful </span></pre>
</div>
<p>
Then I reloaded nginx:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idce79a0a6e0f2'><button class='copyBtn' data-clipboard-target='#idce79a0a6e0f2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo systemctl reload nginx</pre>
</div>
<p>
The new SSL certificate needs to be checked to make sure it was installed properly.
I just visually examine the start and expire dates:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idca92318a0b84'><button class='copyBtn' data-clipboard-target='#idca92318a0b84' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl -Lvs https://www.scalacourses.com \
2>&1 1>/dev/null | \
grep '\(start\|expire\) date:'
<span class='unselectable'>* <span style="color:red">start date</span>: Mar 1 17:40:56 2023 GMT
* <span style="color:red">expire date</span>: May 30 17:40:55 2023 GMT </span></pre>
</div>
<!-- endregion -->
<!-- #region Add a New Entry to crontab -->
<h2 id="crontab">Add a New Entry to <span class="code">Crontab</span></h2>
<p>
Automating the regeneration of the SSL wildcard certificate is easy.
Edit your <code>crontab</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id92dfa07557b3'><button class='copyBtn' data-clipboard-target='#id92dfa07557b3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>crontab -e</pre>
</div>
<p>
Add the following reference to the
<a href='#certGenScalaCourses'><code>certGenScalaCourses</code> script</a>
that I gave you earlier to <code>crontab</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>crontab Entry</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idea8ae2979bf1'><button class='copyBtn' data-clipboard-target='#idea8ae2979bf1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button># Runs every 2 months / 60 days (more or less)
30 0 1 */2 * $work/lego/certGenScalaCourses</pre>
</div>
<p>
The above only works because
<a href='#env'>we defined the <code>work</code> environment variable</a>
in <code>/etc/<wbr>environment</code>.
</p>
<!-- endregion -->
HTML Hyphens2023-01-23T00:00:00-05:00https://mslinn.github.io/blog/2023/01/23/html-hyphen<p>
Web browsers now have built-in automatic hyphenation support,
but it is turned off by default for compatibity with older browsers.
You can enable hyphenation with two words of CSS, like this:
</p>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9b737489a804'><button class='copyBtn' data-clipboard-target='#id9b737489a804' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>body {
<span class='bg_yellow'>hyphens: auto;</span>
}</pre>
</div>
<h2 id="w3c">W3 CSS3 Hyphenation Standard</h2>
<div class='quote'>
<div class='quoteText clearfix'>
Hyphenation occurs when the line breaks at a valid hyphenation opportunity,
which is a type of soft wrap opportunity that exists within a word where hyphenation is allowed.
<br><br>
In CSS, hyphenation opportunities are controlled with the <code>hyphens</code> property.
CSS Text Level 3 does not define the exact rules for hyphenation;
however, UAs are strongly encouraged to optimize their choice of break points and to chose
language-appropriate hyphenation points.
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd5284e29f8aa'>Possible hyphens values: none | manual | auto
Initial value: manual</pre>
</div>
</div><div class='quoteAttribution'> – <a href='https://www.w3.org/TR/css-text-3/#hyphenation' rel='nofollow' target='_blank'>W3 CSS3 Standard</a></div>
</div>
<p>
However, the W3 CSS3 hyphenation standard also says:
</p>
<div class='quote'>
<div class='quoteText clearfix'>
Correct automatic hyphenation requires a hyphenation resource appropriate to the language
of the text being broken.
The UA must therefore only automatically hyphenate text for which the content language is
known and for which it has an appropriate hyphenation resource.
<br><br>
Authors should correctly tag their content’s language
(e.g. using the HTML <code>lang</code> attribute or XML <code>xml:lang</code> attribute)
in order to obtain correct automatic hyphenation.
</div><div class='quoteAttribution'> – <a href='https://www.w3.org/TR/css-text-3/#hyphenation' rel='nofollow' target='_blank'>W3 CSS3 Standard</a></div>
</div>
<p>
Let’s put this into practice now.
</p>
<h2 id="css">Your Website Stylesheet</h2>
<p>
You may not want certain passages to be hyphenated.
Defining a CSS style that disables automatic hyphenation for portions of a document is easy;
you can still use <code>&shy;</code> in those portions of the document
to manually define optional hyphenation points.
</p>
<p>
Here is a suggestion for your stylesheet,
so it has the hyphenation support you need.
Note that hypenation for <kbd>kbd</kbd> tags is completely disabled.
</p>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id998bee4fdc49'><button class='copyBtn' data-clipboard-target='#id998bee4fdc49' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>body, .hyphen {
hyphens: auto;
}
.nohyphen {
hyphens: manual;
}
kbd {
hyphens: none;
}</pre>
</div>
<h2 id="example">Complete Example</h2>
<p>
The following example enables automatic hyphenation of the entire HTML <code>body</code>,
according to US English rules.
The HTML document contains nested tags,
some of which disable or enable automatic hyphenation.
One line demonstrates manual hyphenation, through the use of
<a href='https://developer.mozilla.org/en-US/docs/Web/CSS/hyphens' target='_blank' rel='nofollow'><code>&shy;</code> entities</a>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>index.html</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcf80b9b3936b'><button class='copyBtn' data-clipboard-target='#idcf80b9b3936b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><html <span class="bg_yellow">lang="en-US"</span>>
<head>
<style>
body, .hyphen {
hyphens: auto;
}
.nohyphen {
hyphens: manual;
}
</style>
</head>
<body>
<p>This element is automatically hyphenated.</p>
<p <span class="bg_yellow">class="nohyphen"</span>>This element is not hyphenated.</p>
<ol <span class="bg_yellow">class="nohyphen"</span>>
<li>This elem<span class="bg_yellow">&shy;</span>ent is manual<span class="bg_yellow">&shy;</span>ly hyphen<span class="bg_yellow">&shy;</span>ated.</li>
<li <span class="bg_yellow">class="hyphen"</span>>This element is automatically hyphenated.</li>
</ol>
</body>
</html></pre>
</div>
<div class="right" style="font-size: 3em;">😁</div>
<p>Easy!</p>
<h2 id="alt" class="clear">Alternative Manual Hyphenation</h2>
<p>
Invisible hypenation is sometimes desirable.
For example, if I have a long directory path, like
<code>$HOME/.rbenv/versions/$RUBY_VERSION/lib/ruby/gems/$RUBY_VERSION/$GEM_NAME-$GEM_VERSION</code>
I might want to tell the web browser where the string could be broken.
</p>
<p>
You have two options — they are actually different syntaxes for the same
<a href='https://en.wikipedia.org/wiki/Zero-width_space' target='_blank' rel='nofollow'><i>zero-width space</i></a>.
</p>
<h3 id="alt">Zero-Width Space HTML Entity</h3>
<p>
<code>&#8203;</code> is the HTML entity for a unicode character called the zero-width space
(<a href='https://unicode-table.com/en/200B/' target='_blank' rel='nofollow'><code>ZWSP</code></a>).
</p>
<p>
For example,
<code>$HOME/.rbenv/<span class="bg_yellow">&#8203;</span>versions/<span class="bg_yellow">&#8203;</span>$RUBY_VERSION/<span class="bg_yellow">&#8203;</span>lib/<span class="bg_yellow">&#8203;</span>ruby/<span class="bg_yellow">&#8203;</span>gems/<span class="bg_yellow">&#8203;</span>$RUBY_VERSION/<span class="bg_yellow">&#8203;</span>$GEM_NAME-<span class="bg_yellow">&#8203;</span>$GEM_VERSION</code>
<br><br>renders as:<br><br>
<code>$HOME/.rbenv/versions/$RUBY_VERSION/​lib/​ruby/​gems/​$RUBY_VERSION/​$GEM_NAME-​$GEM_VERSION</code>
</p>
<h3 id="alt">Line Break Opportunity Tag</h3>
<p>
You could also use the
<a href='https://developer.mozilla.org/en-US/docs/Web/HTML/Element/wbr' target='_blank' rel='nofollow'>line break opportunity tag</a>,
<code><wbr></code> .
</p>
<p>
For example,
<code>$HOME/.rbenv/<span class="bg_yellow"><wbr></span>versions/<span class="bg_yellow"><wbr></span>$RUBY_VERSION/<span class="bg_yellow"><wbr></span>lib/<span class="bg_yellow"><wbr></span>ruby/<span class="bg_yellow"><wbr></span>gems/<span class="bg_yellow"><wbr></span>$RUBY_VERSION/<span class="bg_yellow"><wbr></span>$GEM_NAME-<span class="bg_yellow"><wbr></span>$GEM_VERSION</code>
<br><br>renders as:<br><br>
<code>$HOME/.rbenv/versions/$RUBY_VERSION/<wbr>lib/<wbr>ruby/<wbr>gems/<wbr>$RUBY_VERSION/<wbr>$GEM_NAME-<wbr>$GEM_VERSION</code>
</p>
<h3 id="same">Both Options Yield the Same Rendered Text</h3>
<p>
I prefer to use <wbr>. <a href='https://dictionary.cambridge.org/dictionary/english/ymmv' target='_blank' rel='nofollow'>YMMV</a>.
</p>
<h2 id="nbh">Non-Breaking Hyphen</h2>
<p>
The non-breaking hyphen HTML entity (<code>&#8209;</code>) displays a hyphen character that does not break.
</p>
<p>
For CSS, use <code>content: "\2011"</code> to produce a non-breaking hyphen.
</p>
A Curmudgeon’s Social Networking2023-01-07T00:00:00-05:00https://mslinn.github.io/blog/2023/01/07/curmudgeon<p>
I am an unapologetic curmudgeon.
I am not here to please anyone else, I am only here to earnestly be myself to the best of my ability.
</p>
<p>
If we do not continuously demonstrate that we endeavor to treat each other right, then we should be dismissed and forgotten.
</p>
<p>
Bullshit walks; money drives away, unsatisfied.
</p>
<p>
Fairness and equity are essential in all things without reservation.
Otherwise, go away, life is short and you are just noise.
</p>
<h2 id="social">Facebook and LinkedIn</h2>
<p>
Twitter is now a circus.
I don't need or want another circus in my communication.
Do not look for me on Twitter any longer.
</p>
<p>
If someone connects with me on Facebook or LinkedIn,
I expect them to interact with me in person, on the phone, and to want to engage with me.
Failing that, I disconnect from them without hesitation.
</p>
<p>
Yes, please connect with me.
Tell me your truth.
Listen to me.
I will listen to you.
</p>
<h2 id="action">Action Over Empty Words</h2>
<p>
Even better, we might endeavor to do things together.
Imagining, conceptualizing, and building something real with others gives me joy.
I want to share that joy.
</p>
<p>
People who talk about what ‘others’ should do, but are unwilling to act themselves, are a waste of time.
They irritate me.
Only approach me if you are an action-oriented individual.
</p>
<div class='quote'>
The most difficult thing is the decision to act, the rest is merely tenacity.
<span class='quoteAttribution'> – Amelia Earhart
</span>
</div>
<p>
Peace and love. <span style="font-size: 36pt;"><a href='https://en.wiktionary.org/wiki/Sigma' target='_blank' rel='nofollow'>σ</a></span>
</p>
Working With Volumes and Directories Under Ubuntu2022-12-01T00:00:00-05:00https://mslinn.github.io/blog/2022/12/01/ubuntu-files<p>
I've been reorganizing directories across several volumes on an Ubuntu server.
This blog post documents how I used the commands I found to be most useful:
<a href='https://linux.die.net/man/1/rsync' target='_blank' rel='nofollow'><code>rsync</code></a>,
<a href='https://linux.die.net/man/1/ncdu' target='_blank' rel='nofollow'><code>ncdu</code></a>,
and <a href='https://linux.die.net/man/1/fdupes' target='_blank' rel='nofollow'><code>fdupes</code></a>.
The latter two programs are not installed by default.
You can install them this way:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8266f895a6ac'><button class='copyBtn' data-clipboard-target='#id8266f895a6ac' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install fdupes ncdu</pre>
</div>
<h2 id="ncdu">Display Directory Sizes</h2>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbedc07e9736a'><button class='copyBtn' data-clipboard-target='#idbedc07e9736a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ncdu /directory</pre>
</div>
<h2 id="fdupes">Find Duplicate Files</h2>
<p>
This displays duplicate files and interactively asks which duplicate files to delete.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd76eda287118'><button class='copyBtn' data-clipboard-target='#idd76eda287118' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>fdupes -r1Sd /directory</pre>
</div>
<h2 id="comp">Compare Two Folders For Missing Files</h2>
<p>
This tip was inspired by an answer on
<a href='https://unix.stackexchange.com/questions/524074/compare-two-folders-for-missing-files' target='_blank' rel='nofollow'><code>unix.stackexchange.com</code></a>.
</p>
<h3 id="dry">Dry Run</h3>
<p>
The <code>-n</code> option displays the names of missing files in the destination directory,
but makes no changes to the destination file system.
If that option is not specified, then the missing files are copied.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id36fbd07084bf'><button class='copyBtn' data-clipboard-target='#id36fbd07084bf' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sync -ri --ignore-existing -n /srcDir<span class="bg_yellow">/</span> /destDir</pre>
</div>
<p>
There are a few things to note:
</p>
<ul>
<li>The first directory is the source, and it must end with a slash (<kbd class="bg_yellow">/</kbd>).</li>
<li>The second directory is the target, and it must NOT end with a slash.</li>
</ul>
<h3 id="doit">Copy Missing Files</h3>
<p>
Simply do not provide the <code>-n</code> (dry run) option:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id31760e239dd5'><button class='copyBtn' data-clipboard-target='#id31760e239dd5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>rsync -ri --ignore-existing /srcDir<span class="bg_yellow">/</span> /destDir</pre>
</div>
JiraCLI, a Feature-rich Interactive Jira Command Line2022-08-12T00:00:00-04:00https://mslinn.github.io/blog/2022/08/12/jiracli<p>
When first released in 2002, <a href='https://www.atlassian.com/software/jira' target='_blank' rel='nofollow'>Jira</a> was merely a bug tracker.
Since then, it has gained features and is now used as a project management tool.
Jira only provides a web user interface.
</p>
<p>
<a href='https://github.com/ankitpokhrel/jira-cli' target='_blank' rel='nofollow'>JiraCLI</a>,
an independent F/OSS project,
provides command-line explorers for Jira issues, epics, and sprints.
</p>
<h2 id="install">Installation</h2>
<p>
JiraCLI is a Go program, so it is quick and easy to install on Ubuntu once Go is installed.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddab005573636'><button class='copyBtn' data-clipboard-target='#iddab005573636' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install golang-go
<span class='unselectable'>$ </span>go install github.com/ankitpokhrel/jira-cli/cmd/jira@latest
<span class='unselectable'>go: downloading github.com/ankitpokhrel/jira-cli v1.0.0
go: downloading github.com/kr/text v0.2.0
go: downloading github.com/spf13/cobra v1.5.0
go: downloading github.com/spf13/viper v1.12.0
go: downloading github.com/zalando/go-keyring v0.2.1
go: downloading github.com/briandowns/spinner v1.18.1
go: downloading github.com/fatih/color v1.13.0
go: downloading github.com/mitchellh/go-homedir v1.1.0
go: downloading github.com/AlecAivazis/survey/v2 v2.3.5
go: downloading github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
go: downloading github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
go: downloading github.com/cpuguy83/go-md2man/v2 v2.0.2
go: downloading github.com/spf13/pflag v1.0.5
go: downloading gopkg.in/yaml.v2 v2.4.0
go: downloading github.com/atotto/clipboard v0.1.4
go: downloading github.com/charmbracelet/glamour v0.5.0
go: downloading github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
go: downloading github.com/cli/safeexec v1.0.0
go: downloading github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
go: downloading github.com/kentaro-m/blackfriday-confluence v0.0.0-20220126124413-8e85477b49b3
go: downloading github.com/russross/blackfriday/v2 v2.1.0
go: downloading github.com/mattn/go-colorable v0.1.12
go: downloading github.com/mattn/go-isatty v0.0.14
go: downloading github.com/gdamore/tcell/v2 v2.5.1
go: downloading github.com/rivo/tview v0.0.0-20220610163003-691f46d6f500
go: downloading github.com/fsnotify/fsnotify v1.5.4
go: downloading github.com/mitchellh/mapstructure v1.5.0
go: downloading github.com/spf13/afero v1.8.2
go: downloading github.com/spf13/cast v1.5.0
go: downloading github.com/spf13/jwalterweatherman v1.1.0
go: downloading github.com/godbus/dbus/v5 v5.1.0
go: downloading golang.org/x/text v0.3.7
go: downloading golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
go: downloading golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c
go: downloading github.com/subosito/gotenv v1.4.0
go: downloading github.com/hashicorp/hcl v1.0.0
go: downloading gopkg.in/ini.v1 v1.66.6
go: downloading github.com/magiconair/properties v1.8.6
go: downloading github.com/pelletier/go-toml/v2 v2.0.2
go: downloading gopkg.in/yaml.v3 v3.0.1
go: downloading github.com/gdamore/encoding v1.0.0
go: downloading github.com/pelletier/go-toml v1.9.5
go: downloading github.com/lucasb-eyer/go-colorful v1.2.0
go: downloading github.com/mattn/go-runewidth v0.0.13
go: downloading github.com/muesli/termenv v0.12.0
go: downloading github.com/yuin/goldmark v1.4.12
go: downloading github.com/yuin/goldmark-emoji v1.0.1
go: downloading github.com/rivo/uniseg v0.2.0
go: downloading github.com/alecthomas/chroma v0.10.0
go: downloading github.com/microcosm-cc/bluemonday v1.0.18
go: downloading github.com/muesli/reflow v0.3.0
go: downloading github.com/olekukonko/tablewriter v0.0.5
go: downloading github.com/aymerick/douceur v0.2.0
go: downloading golang.org/x/net v0.0.0-20220621193019-9d032be2e588
go: downloading github.com/dlclark/regexp2 v1.4.0
go: downloading github.com/gorilla/css v1.0.0 </span></pre>
</div>
<p>
Add <code>$HOME/go/bin</code> to the <code>PATH</code> so <code>jira</code> can be found:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida26140b7743d'><button class='copyBtn' data-clipboard-target='#ida26140b7743d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>echo 'PATH=$HOME/go/bin:$PATH' >> ~/.bashrc
<span class='unselectable'>$ </span>source ~/.bashrc
<span class='unselectable'>$ </span>which jira
<span class='unselectable'>/home/mslinn/go/bin/jira </span></pre>
</div>
<h2 id="usage">Usage</h2>
<p>This is the <code>jira </code> help message:</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3e240137c902'><button class='copyBtn' data-clipboard-target='#id3e240137c902' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jira
<span class='unselectable'>Interactive Jira CLI.<br/>
USAGE
jira [flags]<br/>
MAIN COMMANDS
board Board manages Jira boards in a project
epic Epic manage epics in a project
issue Issue manage issues in a project
open Open issue in a browser
project Project manages Jira projects
sprint Sprint manage sprints in a project board<br/>
OTHER COMMANDS
completion Output shell completion code for the specified shell (bash or zsh)
help Help about any command
init Init initializes jira config
man Help generate man(7) pages for Jira CLI.
me Displays configured jira user
version Print the app version information<br/>
FLAGS
-c, --config string Config file (default is /home/mslinn/.config/.jira/.config.yml)
--debug Turn on debug output
-h, --help help for jira
-p, --project string Jira project to look into (defaults to /home/mslinn/.config/.jira/.config.yml)<br/>
LEARN MORE
Use 'jira <command> <subcommand> --help' for more information about a command. </span></pre>
</div>
<p>
You need an Jira API token before you can use JiraCLI.
Get it from <a href='https://id.atlassian.com/manage-profile/security/api-tokens' target='_blank' rel='nofollow'>here</a>.
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/atlassian_api_token.webp" type="image/webp">
<source srcset="/blog/images/atlassian_api_token.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/atlassian_api_token.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
You need to configure the program before you use it.
Set the <code>JIRA_API_TOKEN</code> environment variable before running <code>jira init</code>.
If you do not, then you will get an error like
<code>Received unexpected response '401 Unauthorized' from jira.</code> in the next step.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ideb3910850f0b'><button class='copyBtn' data-clipboard-target='#ideb3910850f0b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>export JIRA_API_TOKEN=asdfasdfasdf
<span class='unselectable'>$ </span>jira init
<span class='unselectable'>? Installation type: Cloud
? Link to Jira server: https://xxx.atlassian.net/
? Login email: mslinn@mslinn.com
⠼ Verifying login details...
? Default project: My
? Default board: [Use arrows to move, type to filter, ? for more help]
> [Search...]
----------
My Scrum Board
None
✓ Configuration generated: /home/mslinn/.config/.jira/.config.yml </span></pre>
</div>
<p>
This is the configuration file that was generated:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>/home/mslinn/.config/.jira/.config.yml</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1056cb50adfa'><button class='copyBtn' data-clipboard-target='#id1056cb50adfa' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>board:
id: 350
name: My Scrum Board
type: scrum
epic:
name: customfield_10009
link: customfield_10008
installation: Cloud
issue:
fields:
custom:
- name: Epic Link
key: customfield_10008
schema:
datatype: any
- name: Epic Name
key: customfield_10009
schema:
datatype: string
types:
- id: "3"
name: Task
handle: Task
subtask: false
- id: "5"
name: Sub-task
handle: Sub-task
subtask: true
- id: "7"
name: Story
handle: Story
subtask: false
- id: "1"
name: Bug
handle: Bug
subtask: false
- id: "6"
name: Epic
handle: Epic
subtask: false
login: mslinn@mslinn.com
project:
key: My
type: classic
server: https://xxx.atlassian.net</pre>
</div>
<h2 id="ex">Example Commands</h2>
<p>
Help for the <code>jira issue</code> subcommand:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc8808df535ba'><button class='copyBtn' data-clipboard-target='#idc8808df535ba' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jira issue -h
<span class='unselectable'>Issue manage issues in a given project. See available commands below.<br/>
USAGE
jira issue [flags]<br/>
MAIN COMMANDS
assign Assign issue to a user
clone Clone duplicates an issue
comment Manage issue comments
create Create an issue in a project
delete Delete an issue
edit Edit an issue in a project
link Link connects two issues
list List lists issues in a project
move Transition an issue to a given state
unlink Unlink disconnects two issues from each other
view View displays contents of an issue
worklog Manage issue worklog<br/>
FLAGS
-h, --help help for issue<br/>
INHERITED FLAGS
-c, --config string Config file (default is /home/mslinn/.config/.jira/.config.yml)
--debug Turn on debug output
-p, --project string Jira project to look into (defaults to /home/mslinn/.config/.jira/.config.yml)<br/>
ALIASES
issues<br/>
LEARN MORE
Use 'jira <command> <subcommand> --help' for more information about a command.
</span></pre>
</div>
<p>
Help for the <code>jira issue list</code> sub-subcommand:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf2e59804b0b8'><button class='copyBtn' data-clipboard-target='#idf2e59804b0b8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jira issue list -h
<span class='unselectable'>List lists issues in a given project.<br/>
You can combine different flags to create a unique query. For instance,<br/>
# Issues that are of high priority, is in progress, was created this month, and has given labels
jira issue list -yHigh -s"In Progress" --created month -lbackend -l"high prio"<br/>
Issues are displayed in an interactive list view by default. You can use a --plain flag
to display output in a plain text mode. A --no-headers flag will hide the table headers
in plain view. A --no-truncate flag will display all available fields in plain mode.<br/>
USAGE
jira issue list [flags]<br/>
FLAGS
-t, --type string Filter issues by type
-R, --resolution string Filter issues by resolution type
-s, --status string Filter issues by status
-y, --priority string Filter issues by priority
-r, --reporter string Filter issues by reporter (email or display name)
-a, --assignee string Filter issues by assignee (email or display name)
-C, --component string Filter issues by component
-l, --label stringArray Filter issues by label
-P, --parent string Filter issues by parent
--history Issues you accessed recently
-w, --watching Issues you are watching
--created string Filter issues by created date
Accepts: today, week, month, year, or a date in yyyy-mm-dd and yyyy/mm/dd format,
or a period format using w = weeks, d = days, h = hours, m = minutes. eg: -10d
Created filter will have precedence over created-after and created-before filter
--updated string Filter issues by updated date
Accepts: today, week, month, year, or a date in yyyy-mm-dd and yyyy/mm/dd format,
or a period format using w = weeks, d = days, h = hours, m = minutes. eg: -10d
Updated filter will have precedence over updated-after and updated-before filter
--created-after string Filter by issues created after certain date
--updated-after string Filter by issues updated after certain date
--created-before string Filter by issues created before certain date
--updated-before string Filter by issues updated before certain date
-q, --jql string Run a raw JQL query in a given project context
--order-by string Field to order the list with (default "created")
--reverse Reverse the display order (default "DESC")
--paginate string Paginate the result. Max 100 at a time, format: <from>:<limit> where <from> is optional (default "0:100")
--plain Display output in plain mode
--no-headers Don't display table headers in plain mode. Works only with --plain
--no-truncate Show all available columns in plain mode. Works only with --plain
--columns string Comma separated list of columns to display in the plain mode.
Accepts: TYPE, KEY, SUMMARY, STATUS, ASSIGNEE, REPORTER, PRIORITY, RESOLUTION, CREATED, UPDATED
-h, --help help for list<br/>
INHERITED FLAGS
-c, --config string Config file (default is /home/mslinn/.config/.jira/.config.yml)
--debug Turn on debug output
-p, --project string Jira project to look into (defaults to /home/mslinn/.config/.jira/.config.yml)<br/>
EXAMPLES
$ jira issue list<br/>
# Limit list to 20 items
$ jira issue list --paginate 20<br/>
# Get 50 items starting from 10
$ jira issue list --paginate 10:50<br/>
# List issues in a plain table view without headers
$ jira issue list --plain --no-headers<br/>
# List some columns of the issue in a plain table view
$ jira issue list --plain --columns key,assignee,status<br/>
# List issues in a plain table view and show all fields
$ jira issue list --plain --no-truncate<br/>
# List issues of type "Epic" in status "Done"
$ jira issue list -tEpic -sDone<br/>
# List issues in status other than "Open" and is assigned to no one
$ jira issue list -s~Open -ax<br/>
# List issues from all projects
$ jira issue list -q"project IS NOT EMPTY"<br/>
ALIASES
lists
ls<br/>
LEARN MORE
Use 'jira <command> <subcommand> --help' for more information about a command. </span></pre>
</div>
<h3 id="updated">Issues Updated on a Certain Date</h3>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id827cc1a1a101'><button class='copyBtn' data-clipboard-target='#id827cc1a1a101' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jira issue list --updated 2021-11-17</pre>
</div>
<h3 id="updated">Issues Selected by Complex Criteria</h3>
<p>
The following command will yield a list of high-priority issues, created this month, with status <code>To Do</code>,
that are assigned to you, and have the label <code>backend</code>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id59d068d4b6a8'><button class='copyBtn' data-clipboard-target='#id59d068d4b6a8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jira issue list -yHigh -s"To Do" --created month -lbackend -a$(jira me)</pre>
</div>
<p>
Output might look something like the following, which was redacted:
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/jira_issues.webp" type="image/webp">
<source srcset="/blog/images/jira_issues.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/jira_issues.png"
style='width: 100%; '
/>
</picture>
</div>
<h3 id="ids">Issue Ids Only</h3>
<p>
The <code>--plain</code> and <code>--no-headers</code> options are useful for driving scripts.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb3ebf4c6edb8'><button class='copyBtn' data-clipboard-target='#idb3ebf4c6edb8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jira issue list --updated 2021-11-17 --plain --no-headers --columns KEY</pre>
</div>
<h3 id="detail">View Issue</h3>
<p>
The following returns the available information about an issue, in a plain-text format.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id58ad038b1f6f'><button class='copyBtn' data-clipboard-target='#id58ad038b1f6f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>jira issue view ISSUE-1 --comments 9999 --plain</pre>
</div>
<h2 id="verdict">Verdict</h2>
<p>
JiraCLI is useful project!
<span style='font-size: 3em;'>😁</span>
</p>
ImageMagick Slicing on Ubuntu/WSL2022-07-28T00:00:00-04:00https://mslinn.github.io/blog/2022/07/28/imagemagick-slicing<p>
Lawyers like the Microsoft Office software suite;
so when I am working on a court case as an expert,
I endeavor to provide my clients with Word documents that contain necessary information.
I like working in WSL/WSL2 because I can use Windows programs and Ubuntu programs together effectively.
</p>
<h2 id="steps">Grab Image, Then Slice</h2>
<p>
Recently, I used <a href='https://www.techsmith.com/screen-capture.html' target='_blank' rel='nofollow'>SnagIt</a>,
a Windows program, to capture large web pages as single images.
Some of these images were quite tall.
</p>
<p>
The CSS for the web pages made some content invisible on the printed page.
Yes, I could have injected CSS using a Chrome plugin like
<a href='https://chrome.google.com/webstore/detail/my-style/ljdhjpmbnkbengahefamnhmegbdifhlb' target='_blank' rel='nofollow'>My Style</a>
to ensure that all content will be printed, like this:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Injected Style</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc32e38d88328'>@media print {
* { display: initial; visibility: visible; }
}</pre>
</div>
<p>
I decided to use screen grabs,
which would guarantee that the contents of my report would exactly match what had been displayed on the screen,
without injecting anything into the web pages.
</p>
<p>
<a href='https://imagemagick.org/index.php' target='_blank' rel='nofollow'>ImageMagick</a> is preinstalled on Ubuntu Desktop.
I used ImageMagick to slice the image captures into smaller page-sized images,
so they could be inserted into a Word document.
</p>
<h2 id="grunt">The Computer Worked Hard</h2>
<p>
Grabbing such large web pages was a lot of work for my desktop computer.
The only programs active during the screen grab process were the Google Chrome browser and SnagIt.
I found that 10GB RAM and 30% of the GPU capability (an NVidia GTX 1660 Super) was used.
</p>
<p>
The screen grab failed if I did not start scrolling from the top of the web page;
while it is possible to scrub up and down smaller web pages in order to grab portions of interest,
this fails for large pages.
</p>
<p>
I also found that scrolling too fast caused the screen grabbing process to fail.
Clicking and holding the bottom scroll arrowhead at the bottom right of the screen
seemed to result in a smooth and optimal scrolling speed.
This meant that grabbing large web pages took a few minutes as the page slowly scrolled downward.
</p>
<h2 id="setup">Setting Up the Conversion</h2>
<p>
The Word documents I usually work with are formatted for North American standards.
This means one-inch margins on letter-sized paper (8.5" x 11"),
which gives a working area of 6.5" x 9", yielding an aspect ratio of 0.72.
</p>
<p>
The tall captured images needed to be sliced into rectangles that fit efficiently into Word documents.
The computations are as follows.
</p>
<ol>
<li>
Determine the width of a screen grab and save it into <code>W</code>.
The ImageMagick <code>identify</code> command does not provide a newline after its output,
however I have inserted one for readability:
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id33a9dbab472c'><button class='copyBtn' data-clipboard-target='#id33a9dbab472c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>identify -ping -format '%w' ../IMG2005.png
<span class='unselectable'>1536 </span>
<span class='unselectable'>$ </span>export W="$( identify -ping -format '%w' ../IMG2005.png )"</pre>
</div>
</li>
<li>
Determine the height of a screen grab and save it into <code>H</code>:
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide8a17e5d00e7'><button class='copyBtn' data-clipboard-target='#ide8a17e5d00e7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>identify -ping -format '%h' ../IMG2005.png
<span class='unselectable'>$ </span>export H="$( identify -ping -format '%h' ../IMG2005.png )"</pre>
</div>
</li>
<li>
The width can be divided by the aspect ratio to obtain the desired height of each
slice so they can be inserted optimally into the Word documents.
I used the <a href='https://linux.die.net/man/1/bc' target='_blank' rel='nofollow'><code>bc</code> calculator</a> provided with <code>Bash</code> to divide <code>W / ASPECT_RATIO</code>.
The <code>H2</code> integer variable contains the computed height for the images.
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id07cc9a34ccc7'><button class='copyBtn' data-clipboard-target='#id07cc9a34ccc7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>export ASPECT_RATIO=0.72
<span class='unselectable'>$ </span>export H2="$( echo "scale=0 ; $W / $ASPECT_RATIO" | bc )"</pre>
</div>
</li>
<li>
Now the image called <code>IMG2005.png</code> can be sliced using ImageMagick’s convert command.
The slices are stored into a subdirectory called <code>slices</code>, with file names like
<code>IMG2005-1.jpg</code>, <code>IMG2005-2.jpg</code>, etc.
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id15de9b30fab6'><button class='copyBtn' data-clipboard-target='#id15de9b30fab6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>convert IMG2005.png -crop ${W}x${H2} \
-quality 100% -scene 0 slices/IMG2005-%d.jpg</pre>
</div>
</li>
</ol>
<h2 id="script">Automating the Conversion</h2>
<p>
I wrote the following bash script, which incorporates the above computations.
It slices all the images in a directory and saves the results to a second directory.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>sliceImages</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf3052fd3c34a'><button class='copyBtn' data-clipboard-target='#idf3052fd3c34a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>#!/bin/bash
function help {
if [ "$1" ]; then echo "Error: $1"; fi
echo "
$(basename $0): slice all images in the given directory and place them into a specified directory,
which will be created if required.
"
exit 1
}
function setup {
export ASPECT_RATIO=0.72
export W="$( identify -ping -format '%w' "$1" )"
export H="$( identify -ping -format '%h' "$1" )"
export H2="$( echo "scale=0 ; $W / $ASPECT_RATIO" | bc )"
}
function convert1 {
FULLNAME=$(basename -- "$1")
FILENAME="${FULLNAME%.*}"
FILETYPE="${FULLNAME##*.}"
convert "$1" \
-crop "${W}x${H2}" \
-quality 100% \
-scene 0 \
"$DIR_OUTPUT/$FILENAME-%d.png"
}
if [ -z "$1" ]; then help "No directory path for images to be converted was provided."; fi
export DIR_INPUT="$( realpath $1 )"
if [ -z "$2" ]; then help "No directory path for the image slices to be saved into was provided."; fi
export DIR_OUTPUT="$( realpath $2 )"
mkdir -p "$DIR_OUTPUT"
find $DIR_INPUT -type f -exec file --mime-type {} \+ | awk -F: '{if ($2 ~/image\//) print $1}' |
while read FILE; do
setup "$FILE"
echo "Slicing $FILE into ${W}x${H2} pixels"
convert1 "$FILE"
done</pre>
</div>
<h2 id="limits">Overcoming ImageMagick Processing Limits</h2>
<p>
Some of the web pages that I needed to grab were quite long,
which resulted in those images requiring more computational resources
than the default Imagemagick configuration allows.
This caused errors such as the following to appear:
</p>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddc685971ab1d'>convert-im6.q16: no images defined `/mnt/c/images/slices/IMG1466-%d.png' @ error/convert.c/ConvertImageCommand/3229.
convert-im6.q16: cache resources exhausted `/mnt/c/images/IMG1091.png' @ error/cache.c/OpenPixelCache/4095.</pre>
</div>
<p>
Imagemagick defines computational resources limits in <code>/etc/ImageMagick-6/policy.xml</code>.
The default maximum memory is 256 KB,
the default maximum allowable height is 16,000 pixels (16KP),
and the default maximum area is 128M pixels.
These values are defined by the following entries:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>/etc/ImageMagick-6/policy.xml</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id58245093100d'><policy domain="resource" name="memory" value="256MiB"/>
<policy domain="resource" name="height" value="16KP"/>
<policy domain="resource" name="area" value="128MP"/></pre>
</div>
<p>
I changed the maximum memory limit to 2 GB RAM,
the maximum height limit to 10,000,000 pixels (10MP),
and the maximum area limit to 2G pixels with these entries:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>/etc/ImageMagick-6/policy.xml</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9f182009c8a8'><policy domain="resource" name="memory" value="2GiB"/>
<policy domain="resource" name="height" value="10MP"/>
<policy domain="resource" name="area" value="2GP"/></pre>
</div>
<p>
Alternatively, I could have simply commented out the limits,
as shown in highlighted text below.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>/etc/ImageMagick-6/policy.xml</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id281e43263139'><span class="bg_yellow"><!--</span>
<policy domain="resource" name="memory" value="256MiB"/>
<policy domain="resource" name="height" value="16KP"/>
<policy domain="resource" name="area" value="128MP"/>
<span class="bg_yellow">--></span></pre>
</div>
<p>
The largest web page to be sliced was converted to a very tall image, which was 83,703 pixels high.
It was sliced into 40 images.
</p>
<h2 id="macro">Word Macro</h2>
<p>
A Word macro is also needed to insert the images into the currently open Word document in alphabetical order.
I modified <a href='https://software-solutions-online.com/word-vba-insert-images/' target='_blank' rel='nofollow'>this one</a>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft Word Macro</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf9d9176ad77f'>Sub insertImages()
Dim intResult As Integer
Dim strPath As String
Dim strFolderPath As String
Dim objFSO As Object
Dim objFolder As Object
Dim objFile As Object
Dim i As Integer
intResult = Application.FileDialog(msoFileDialogFolderPicker).Show
'Check if user canceled the dialog
If intResult <> 0 Then
'dispaly message box
strFolderPath = Application.FileDialog(msoFileDialogFolderPicker).SelectedItems(1)
'Create an instance of the FileSystemObject
Set objFSO = CreateObject("Scripting.FileSystemObject")
'Get the folder object
Set objFolder = objFSO.GetFolder(strFolderPath)
i = 1
'loops through each file in the directory and prints their names and path
For Each objFile In objFolder.Files
'get file path
strPath = objFile.Path
'insert the image
Selection.InlineShapes.AddPicture FileName:= _
strPath, LinkToFile:=False, _
SaveWithDocument:=True
Next objFile
End If
End Sub</pre>
</div>
<h2 id="done">Done!</h2>
<span style='font-size: 3em; float: right; margin-left: 5px;'>😁</span>
<p>
Thanks to the above automation,
I was able to deliver the Word documents containing the sliced web pages to my client soon after they were requested.
</p>
Uncomplicated Firewall on Ubuntu2022-07-16T00:00:00-04:00https://mslinn.github.io/blog/2022/07/16/ufw<p>
All websites, especially ecommerce sites, need to be secure.
A properly set up firewall is an essential component for a secure server.
</p>
<p>
Ubuntu 22.04 uses the
<a href='https://wiki.ubuntu.com/UncomplicatedFirewall' target='_blank' rel='nofollow'>Uncomplicated Firewall <code>ufw</code> firewall</a>
frontend by default.
<code>Ufw</code> has been provided for Ubuntu since v8.04 (Hardy Heron).
</p>
<h2 id="tldr">Quick Setup</h2>
<p>
Enable <code>ufw</code> as follows:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbf7e5bc1bbbd'><button class='copyBtn' data-clipboard-target='#idbf7e5bc1bbbd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo ufw enable
<span class='unselectable'>Firewall is active and enabled on system startup </span></pre>
</div>
<p>
Enable the
<a href='https://github.com/ageis/ufw-application-profiles' target='_blank' rel='nofollow'><code>ufw</code> application profiles</a>
for <code>ssh</code> and nginx (HTTP/HTTPS) like this:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfcf98145ab62'><button class='copyBtn' data-clipboard-target='#idfcf98145ab62' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo ufw allow OpenSSH
<span class='unselectable'>Output
Rule added
Rule added (v6) </span>
<span class='unselectable'>$ </span>sudo ufw allow 'Nginx Full'
<span class='unselectable'>Output
Rule added
Rule added (v6) </span></pre>
</div>
<h3 id="ufwDetails">Diving Deeper</h3>
<p>
The following is mostly true:
</p>
<div class='quote'>
<div class='quoteText clearfix'>
The default firewall on Ubuntu 22.04 Jammy Jellyfish is <code>ufw</code>,
which is short for “uncomplicated firewall.”
<code>Ufw</code> is a frontend for the typical Linux <code>iptables</code> commands,
but it is developed in such a way that basic firewall tasks can be performed without the knowledge of <code>iptables</code>.
<br><br>
Additionally, <code>ufw</code> can be managed from a graphical interface.
In this tutorial, you will learn how to enable and disable the <code>ufw</code> firewall on Ubuntu 22.04 Jammy Jellyfish from both command line and GUI.
</div><div class='quoteAttribution'> – From <a href='https://linuxconfig.org/how-to-enable-disable-firewall-on-ubuntu-22-04-lts-jammy-jellyfish-linux' rel='nofollow' target='_blank'>How to enable/disable firewall on Ubuntu 22.04 LTS Jammy Jellyfish Linux</a></div>
</div>
<p>
The above makes no mention of how Ubuntu 22.04 replaced <code>iptables</code> with <code>nftables</code>,
as described below.
</p>
<div class='quote'>
<div class='quoteText clearfix'>
<h2><span class="code">nftables</span> as the default firewall backend</h2>
Firewalling on Linux consists of two components –
the firewall mechanism within the Linux kernel,
and the tools used to configure this from userspace.
The Linux kernel has traditionally supported two different subsystems for firewall policies – <code>iptables</code> / <code>xtables</code>
and the newer <code>nftables</code>.
<br><br>
<code>Nftables</code> brings significant benefits both in terms of performance and flexibility when creating and deploying firewall rules, particularly for dual stack IPv4/IPv6 systems.
<br><br>
The traditional <code>iptables</code> userspace management tool now configures the <code>nftables</code> kernel backend, whilst the new
<code>nft</code> userspace tool is also present to allow the creation of more flexible rules not supported by the traditional iptables paradigm.
</div><div class='quoteAttribution'> – From <a href='https://ubuntu.com/blog/whats-new-in-security-for-ubuntu-22-04-lts/docs-internal-guid-117e0493-7fff-ea13-3537-bde70965f89d' rel='nofollow' target='_blank'>What’s new in Security for Ubuntu 22.04 LTS?</a></div>
</div>
<p>
Digital Ocean has a <a href='https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-with-ufw-on-ubuntu-18-04' target='_blank' rel='nofollow'>good <code>ufw</code> tutorial</a>.
</p>
Using Nginx As a Reverse Proxy With SSL2022-07-08T00:00:00-04:00https://mslinn.github.io/blog/2022/07/08/reverse-proxy<div class="right">
<div class='imgWrapper imgFlex inline' style=' '>
<figure>
<a href='https://www.ScalaCourses.com' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/webp">
<source srcset="/assets/images/ScalaCoursesLogo207x207.png" type="image/png">
<img alt='ScalaCourses.com'
class="imgImg rounded shadow"
src="/assets/images/ScalaCoursesLogo207x207.png"
style='width: 100%; '
title='ScalaCourses.com'
/>
</picture>
</a>
<figcaption class='imgFigCaption '>
<a href="https://www.ScalaCourses.com" target='_blank' >
ScalaCourses.com
</a>
</figcaption>
</figure>
</div>
</div>
<p>
Recently I <a href='https://mslinn.com/blog/2022/05/26/aws-hijacking.html' target='_blank' rel='nofollow'>migrated</a>
ScalaCourses.com from AWS EC2/S3/CloudFront
to a server in my apartment, which has fiber optic internet service.
The server’s motherboard is an ASUS Sabertooth x79 with an Intel i7 4820,
32 GB DDR3 RAM, and a 4TB SATA SSD.
This motherboard is unable to boot from NVMe drives, so SATA is necessary.
At the time of this writing, the server runs Ubuntu 22.04.
</p>
<p>
The backup server is currently being set up.
I am repurposing an old Hackintosh as a fallback to the Ubuntu server.
That motherboard is another ASUS Sabertooth x79, with 32 GB DDR3 RAM and two 2 TB SATA drives.
</p>
<h2 id="why">Why Am I Doing This?</h2>
<p>
Once again, I control my hardware, my software, my network, and all ancillary services.
After several years of using PaaS vendor servers,
I am now reverting to running ScalaCourses.com on my own hardware and software, using my own network.
</p>
<div class="pullQuote">
No longer will I subject myself to the unlimited financial liability that current PaaS vendors expose their customers to.
</div>
<h2 id="defs">Definitions</h2>
<p>
A <i>proxy</i> is a person or process serving as an authorized agent or substitute for another.
In computer science, a more specific term is <i>forward proxy</i>.
<br><br>
A <i>proxy server</i> is a server process that acts as an intermediary between a client requesting a resource,
and the process that provides the resource.
<br><br>
A <i>reverse proxy</i> is a process that sits in front of other processes, and forwards client requests to them.
The term <i>forwarding process</i> is similar to <i>reverse proxy</i>.
<br><br>
The definitions for <i>proxy</i>, <i>forward proxy</i> and <i>reverse proxy</i> all sound identical.
The key difference between a reverse proxy and a forward proxy
is that a <b>forward proxy enables computers isolated on a private network</b> to connect to the public internet,
while a <b>reverse proxy enables computers on the internet</b> to access a private subnet.
</p>
<h2 id="details">Dealing With Details</h2>
<p>
The old <a href='https://en.wikipedia.org/wiki/Pound_(networking)' target='_blank' rel='nofollow'>Pound</a>
v2.8-2 <a href='https://www.cloudflare.com/en-ca/learning/cdn/glossary/reverse-proxy/' target='_blank' rel='nofollow'>reverse proxy</a>
that was the front end for the old Play Framework app that
runs <a href='https://scalacourses.com' target='_blank' rel='nofollow'>ScalaCourses.com</a> is no longer viable,
and the new version 3 of Pound is incomplete.
Depending on configuration,
reverse proxies can provide extra security from external attacks on a website,
decrypt https requests to http, and act as stream editors for the content.
</p>
<p>
This site still uses AWS CloudFront, for the moment.
I am researching alternatives, with the goal of closing my AWS account...
unless they introduce a way to cap financial liability before I fully migrate off AWS.
</p>
<h2 id="vs">Apache httpd vs. Nginx</h2>
<p>
Two popular reverse proxies are
<a href='https://httpd.apache.org/docs/trunk/mod/mod_proxy_http2.html' target='_blank' rel='nofollow'>Apache <code>mod_proxy_http2</code></a> and
<a href='https://www.nginx.com/' target='_blank' rel='nofollow'>nginx</a>.
Anything Pound can do, both nginx and Apache httpd/2 can do.
While they both work well, Apache httpd has an older code base,
in fact, many of my websites ran on Apache httpd at the turn of the century.
</p>
<p>
Nginx is relatively newer than Apache httpd.
It is performant, well supported, well documented, and widely available.
I decided to use nginx as the reverse proxy because I had found that nginx worked well on previous projects.
</p>
<p>
Installing nginx and the Letsencrypt software is easy on Debian distros such as Ubuntu:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc058096777cd'><button class='copyBtn' data-clipboard-target='#idc058096777cd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo apt install nginx certbot python3-certbot-nginx</pre>
</div>
<h2 id="vs">Verifying the Nginx Build</h2>
<p>
When acting as a proxy server, nginx requires the <code>http_sub_module</code> to translate HTTP content.
This allows the website content to be stream edited,
so various links and paths that are not translated properly can be fixed up.
</p>
<p>
The nginx package provided by Ubuntu includes the
<code>‑‑with‑http_sub_module</code> option.
You can verify that your instance of nginx was built with the
<code>‑‑with‑http_sub_module</code> option as follows:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb908d3a00415'><button class='copyBtn' data-clipboard-target='#idb908d3a00415' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>nginx -V
<span class='unselectable'>nginx version: nginx/1.18.0 (Ubuntu)
built with OpenSSL 3.0.2 15 Mar 2022
TLS SNI support enabled
configure arguments: --with-cc-opt='-g -O2 -ffile-prefix-map=/build/nginx-9P0wNJ/nginx-1.18.0=.
-flto=auto -ffat-lto-objects -flto=auto -ffat-lto-objects -fstack-protector-strong -Wformat
-Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions
-flto=auto -ffat-lto-objects -flto=auto -Wl,-z,relro -Wl,-z,now -fPIC' --prefix=/usr/share/nginx
--conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log
--error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid
--modules-path=/usr/lib/nginx/modules --http-client-body-temp-path=/var/lib/nginx/body
--http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy
--http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-compat
--with-debug --with-pcre-jit --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module
--with-http_auth_request_module --with-http_v2_module --with-http_dav_module --with-http_slice_module
--with-threads --add-dynamic-module=/build/nginx-9P0wNJ/nginx-1.18.0/debian/modules/http-geoip2
--with-http_addition_module --with-http_gunzip_module --with-http_gzip_static_module
<span class="bg_yellow">--with-http_sub_module</span> </span></pre>
</div>
<h2 id="vs">Nginx SSL Configuration</h2>
<p>
Two files are needed for Letsencrypt to be able to create SSL certificates for nginx:
</p>
<ol>
<li>
The contents of <a href='https://github.com/certbot/certbot/blob/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf' target='_blank' rel='nofollow'><code>/etc/letsencrypt/options-ssl-nginx.conf</code></a>
need to be stored into <code>/etc/letsencrypt/options-ssl-nginx.conf</code>.
I later discovered this file was also available locally at
<code>/usr/lib/python3/dist-packages/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf</code>.
</li>
<li>
The contents of <a href='https://github.com/certbot/certbot/blob/master/certbot/certbot/ssl-dhparams.pem' target='_blank' rel='nofollow'><code>/etc/letsencrypt/ssl-dhparams.pem</code></a>
need to be stored into <code>/etc/letsencrypt/ssl-dhparams.pem</code>.
I later discovered that this file was also available locally at
<code>/usr/lib/python3/dist-packages/certbot/ssl-dhparams.pem</code>.
</li>
</ol>
<h2 id="ssl">Making a Wildcard SSL Certificate</h2>
<p>
I knew an SSL wildcard certificate would be needed, so I made one using Letsencrypt.
Please read <a href='/blog/2022/06/15/certbot.html'>Creating and Renewing Letsencrypt Wildcard SSL Certificates</a> for details.
</p>
<p class="alert rounded shadow">
Update – 8 months after writing this blog post,
I wrote about a better way to generate wildcard SSL certificates:
<a href='/blog/2023/03/02/lego.html'>Wildcard SSL Certificates for Let's Encrypt with Lego</a>.
</p>
<h2 id="vs">Defining the Nginx Website Reverse Proxy</h2>
<p>
Please see my blog post entitled <a href='/blog/2021/03/20/cors.html'>Cross-Origin Resource Sharing (CORS)</a>
for a discussion of how to configure servers whose content needs to be proxied.
</p>
<p>
I saved the following configuration in a new file called
<code>/etc/nginx/sites-available/scalacourses.com</code>.
Note that a single <code>server</code> block answers on ports 80 and 443,
using IPv4 and IPv6, for SSL and non-SSL requests,
for the apex domain (<code>scalacourses.com</code>) and the <code>www.scalacourses.com</code> subdomain.
No redirects are used.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1ce7ff0e924e'><button class='copyBtn' data-clipboard-target='#id1ce7ff0e924e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>server {
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
server_name scalacourses.com www.scalacourses.com;
ssl_certificate /home/mslinn/.certbot/scalacourses.com/config/live/scalacourses.com/fullchain.pem;
ssl_certificate_key /home/mslinn/.certbot/scalacourses.com/config/live/scalacourses.com/privkey.pem;
ssl_trusted_certificate /home/mslinn/.certbot/scalacourses.com/config/live/scalacourses.com/chain.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
root /var/www/html;
index index.html; # This gets served if the proxied website is down
location / {
proxy_pass http://localhost:9000/;
proxy_set_header Accept-Encoding ""; # sub_filter requires this
sub_filter 'https://localhost:9000/authenticate/' 'https://www.scalacourses.com/authenticate/';
sub_filter_once off;
}
}</pre>
</div>
<p>
I disabled the <code>default</code> site:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id295dc605507a'><button class='copyBtn' data-clipboard-target='#id295dc605507a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo rm /etc/nginx/sites-enabled/default</pre>
</div>
<p>
I enabled the new <code>scalacourses.com</code> site:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7ff385dea059'><button class='copyBtn' data-clipboard-target='#id7ff385dea059' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo ln /etc/nginx/sites-{available,enabled}/scalacourses.com</pre>
</div>
<p>
The <code>nginx</code> configuration was tested for syntax:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id42166c9094a0'><button class='copyBtn' data-clipboard-target='#id42166c9094a0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo nginx -t
<span class='unselectable'>nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful </span></pre>
</div>
<p>
The nginx configuration was reloaded:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcf9b3a565dd8'><button class='copyBtn' data-clipboard-target='#idcf9b3a565dd8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo systemctl reload nginx</pre>
</div>
<h2 id="flush">Flush DNS Cache</h2>
<p>
Ensure that DNS requests receive up-to-date values by flushing the DNS cache.
The following works on Ubuntu, but not when running on WSL/WSL2.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id257207ceea84'><button class='copyBtn' data-clipboard-target='#id257207ceea84' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo resolvectl flush-caches</pre>
</div>
<p>
The following works on Windows 10:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Command and PowerShell consoles</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id59c7f6c1377a'><button class='copyBtn' data-clipboard-target='#id59c7f6c1377a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>ipconfig /flushdns</pre>
</div>
<h2 id="verify">Verifying nginx Works</h2>
<p>
I verified that <code>nginx</code> was listening on ports 80 and 443.
I highlighted the <code>nginx</code> process number –
you might need to scroll the output below to the right to see it.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1dcb0621c7e2'><button class='copyBtn' data-clipboard-target='#id1dcb0621c7e2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo netstat -tulpn | grep ':\(443\|80\)'
<span class='unselectable'>tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN -
tcp6 0 0 :::80 :::* LISTEN -
tcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN <span class="bg_yellow">87487</span>/nginx: master </span></pre>
</div>
<span>
<p>
The executable for process 87487 can be found by:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id795fc196efed'><button class='copyBtn' data-clipboard-target='#id795fc196efed' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ls -l /proc/87487/exe
<span class='unselectable'>lrwxrwxrwx 1 mslinn mslinn 0 Jul 7 09:13 /proc/5166/exe -> <span class="bg_yellow">/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java*</span> </span></pre>
</div>
<p>
If more detail is desired, get it from the process list.
Enclosing a character of the process id within square brackets
is an old trick for only showing the desired process.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida3254dfc0f61'><button class='copyBtn' data-clipboard-target='#ida3254dfc0f61' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ps aux | grep [8]7487
<span class='unselectable'>mslinn 87487 2.4 1.3 6596476 439456 ? Sl 09:13 0:20 java -Xms1024m -Xmx1024m -Dhttp.port=9000 /path/to/jar </span></pre>
</div>
<p>
If there is any problem getting things to work,
it is often helpful to monitor the logs as you click on the web pages:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida190fd0de52b'><button class='copyBtn' data-clipboard-target='#ida190fd0de52b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo tail -f /var/log/nginx/*.log</pre>
</div>
<h2 id="persist">Finishing Up</h2>
<p>
Make nginx start each time the system starts as follows.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc2c75dae4234'><button class='copyBtn' data-clipboard-target='#idc2c75dae4234' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo systemctl enable nginx
<span class='unselectable'>Synchronizing state of nginx.service with SysV service script with /lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install enable nginx </span>
<span class='unselectable'>$ </span>sudo update-rc.d nginx defaults</pre>
</div>
<h3 id="performance">Performance Test</h3>
<p>
<a href='https://tools.keycdn.com/performance?url=https://www.scalacourses.com' target='_blank' rel='nofollow'>KeyCDN</a>
offfers free website performance statistics.
The result column labeled <b>TTFB</b> means “time to first byte”,
which is the length of time required for the website content to begin being received by a user's web browser.
</p>
<p>
I am in Montreal, Canada.
Response time for TTFB reported by KeyCDN varies,
from a minimum of 68ms in New York, to a maximum of 815ms in Bangalore.
This range of response times is typical for websites that have a centralized process.
The speed of light is finite, after all.
</p>
<h3 id="monitor">Free Availability Monitoring</h3>
<p>
<a href='https://hetrixtools.com/' target='_blank' rel='nofollow'>HetrixTools</a>
offers free availability monitoring for up to 10 websites.
It was quick and easy to set up.
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/hetrix.webp" type="image/webp">
<source srcset="/blog/images/hetrix.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/hetrix.png"
style='width: 100%; '
/>
</picture>
</div>
<h3 id="live">We Are Live!</h3>
<p>
<a href='https://www.scalacourses.com' target='_blank' rel='nofollow'><code>ScalaCourses.com</code></a>
now serves Scala students from its newly refurbished server!
<span style='font-size: 3em;'>😁</span>
</p>
Trialing mslinn.com on Linode Storage2022-07-01T00:00:00-04:00https://mslinn.github.io/blog/2022/07/01/trialing-linode-storage<edit-fold intro>
<p>
This is another post in my ongoing saga of moving off AWS, which
<a href='/blog/2022/05/26/aws-hijacking.html'>does not integrate security with real-time billing</a>.
I recently learned this the hard way:
when my AWS account was hijacked, in less than 15 minutes,
a huge bill was incurred.
</p>
<div class="pullQuote">
“Pay-as-you-go” is shorthand for “there is nothing you can do to limit your financial liability”
</div>
<p>
The world of pain that I experienced after the breach was inflicted by broken and wasteful AWS remedial processes,
and an ineffective AWS management structure.
This type of issue only is enabled because of deficiencies in the AWS architecture.
Those AWS architectural deficiencies feel like the result of an exploitive mindset:
</p>
<div class='quote'>
<div class='quoteText clearfix'>
Unlimited financial liability is our customer’s problem, not ours –
as a result, exploits are quite profitable for us.
</div><div class='quoteAttribution'> – From a mythical retrospective discussion at an AWS offsite.
</div>
</div>
<div class='quote'>
<div class='quoteText clearfix'>
At this moment that feature of setting limits does not exist,
Azure is not able to safeguard customers from unlimited financial liability.
</div><div class='quoteAttribution'> – From an email sent to me from Microsoft Azure support staff on 2022-06-22.
</div>
</div>
</edit-fold>
<div class="alert rounded shadow">
<h2>Demand Limits to Financial Liability From PaaS Vendors</h2>
<p>
PaaS vendors currently provide accounts with all services ready to go, without limit.
That is good for the vendor's bottom line, but highly dangerous for their customers.
</p>
<p>
All PaaS customers should demand the ability for themselves to be able to set firm limits on budgeted expenses,
along with the ability to deny all services not explicitly authorized.
</p>
<p>
You can buy insurance against losses resulting from various calamities,
but you cannot limit your financial liability with PaaS vendors.
</p>
<p>
<span style="font-size:larger; font-weight: bold;">Yet.</span>
</p>
</div>
<edit-fold market>
<h2 id="market">Website Hosting Market</h2>
<p>
Recently, I have spent a lot of time looking at options for hosting websites.
I found the following types of products:
</p>
<style>
.row th {
text-align: right;
}
</style>
<table class="table">
<tr>
<td></td>
<th>WordPress</th>
<th>General Web Server</th>
<th>VM</th>
<th>S3 Compatible</th>
</tr>
<tr class="row">
<th>CDN</th>
<td>No</td>
<td>No</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr class="row">
<th>Speed</th>
<td>Slow to Medium</td>
<td>Slow to Medium</td>
<td>Slow to Fast</td>
<td>Fast</td>
</tr>
<tr class="row">
<th>Reliability</th>
<td>Fair</td>
<td>Fair</td>
<td>Depends on you</td>
<td>Good</td>
</tr>
<tr class="row">
<th>Financial Liability</th>
<td>Fixed monthly cost</td>
<td>Fixed monthly cost</td>
<td class="bg_yellow">Depends</td>
<td class="bg_yellow">Unlimited</td>
</tr>
<tr class="row">
<th>Storage</th>
<td>Small</td>
<td>Small</td>
<td>Depends</td>
<td>Unlimited</td>
</tr>
<tr class="row">
<th>CLI & API</th>
<td>No</td>
<td>No</td>
<td>Depends</td>
<td>Yes</td>
</tr>
</table>
<p>
This website requires about 220 GB of storage.
It has numerous images, in 2 versions:
<a href='/blog/2020/08/15/converting-all-images-to-webp-format.html'><code>webp</code></a>
and <code>png</code>.
So for this website, ‘small’ means less than 250 GB storage.
</p>
<p>
I decided to give the S3-compatible product
<a href='https://www.linode.com/docs/products/storage/object-storage/' target='_blank' rel='nofollow'>Linode Storage</a> a try.
</p>
</edit-fold>
<edit-fold linode>
<h2 id="why">Why Linode?</h2>
<p>
First, the positive: <a href='https://www.linode.com/' target='_blank' rel='nofollow'>Linode</a> is a pioneer in virtual computing.
It has been on my short list of places for hosting my pet projects for many years.
Prices have always been quite competitive, products solid, with good support ...
and smart people have answered the phone when I have called.
Being able to easily speak with a capable human provides huge value.
</p>
<p>
On the other hand, as I shall demonstrate in this blog post,
Linode’s documentation presents as a significant barrier to customers considering adopting their services.
I had to work a lot harder than I should have to get my evaluation website up and running.
Hopefully, this document is complete enough,
so others can follow along and host their static websites on Linode Storage.
</p>
<h2 id="akamai">Acquired by Akamai</h2>
<p>
<a href='https://www.akamai.com/newsroom/press-release/akamai-completes-acquisition-of-linode' target='_blank' rel='nofollow'>Linode was acquired by Akamai</a>
3 months ago.
Akamai is the original CDN, and their network is gigantic.
</p>
<p>
Linode does not yet offer a CDN product,
but the person I spoke to at Linode when I started writing this post
suggested that a CDN product from Linode based on Akamai’s network might be available soon.
He also said that they were planning to working on a mechanism for limiting financial liability to their customers in 2022.
</p>
<p>
This was music to my financially risk-averse ears!
</p>
</edit-fold>
<div class="notepaper shadow" style="width: 80%;">
<p>
The remainder of this blog post will take you through all the steps necessary to:
</p>
<ol>
<li>Install and configure software tools on a WSL / WSL2 / Ubuntu computer</li>
<li>Make an S3-compatible bucket and set it up to hold a website</li>
<li>Upload the website, and easily handle mimetype issues</li>
<li>Generate and install a free 4096-bit SSL wildcard certificate</li>
<li>Get a security rating from Qualys / SSL Labs</li>
</ol>
</div>
<h2 id="setup">Trialing Linode Storage</h2>
<edit-fold s3cmd>
<h3 id="s3cmd">Installing <span class="code">s3cmd</span></h3>
<p>
I installed the recommended S3-compatible command-line program <code>s3cmd</code> on WSL2 / Ubuntu like this:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0b3c36c842e3'><button class='copyBtn' data-clipboard-target='#id0b3c36c842e3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install s3cmd
<span class='unselectable'>Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
python3-magic
The following NEW packages will be installed:
python3-magic s3cmd
0 upgraded, 2 newly installed, 0 to remove and 0 not upgraded.
Need to get 133 kB of archives.
After this operation, 584 kB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu jammy/main amd64 python3-magic all 2:0.4.24-2 [12.6 kB]
Get:2 http://archive.ubuntu.com/ubuntu jammy/universe amd64 s3cmd all 2.2.0-1 [120 kB]
Fetched 133 kB in 0s (278 kB/s)
Selecting previously unselected package python3-magic.
(Reading database ... 169821 files and directories currently installed.)
Preparing to unpack .../python3-magic_2%3a0.4.24-2_all.deb ...
Unpacking python3-magic (2:0.4.24-2) ...
Selecting previously unselected package s3cmd.
Preparing to unpack .../archives/s3cmd_2.2.0-1_all.deb ...
Unpacking s3cmd (2.2.0-1) ...
Setting up python3-magic (2:0.4.24-2) ...
Setting up s3cmd (2.2.0-1) ...
Processing triggers for man-db (2.10.2-1) ...
Scanning processes...
Scanning processor microcode...
Scanning linux images...
Failed to retrieve available kernel versions.
Failed to check for processor microcode upgrades.
No services need to be restarted.
No containers need to be restarted.
No user sessions are running outdated binaries.
No VM guests are running outdated hypervisor (qemu) binaries on this host. </span></pre>
</div>
<p>
Here is the <code>s3cmd</code> help message:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7a1e9634cd25'><button class='copyBtn' data-clipboard-target='#id7a1e9634cd25' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd
<span class='unselectable'>Usage: s3cmd [options] COMMAND [parameters]<br/>
S3cmd is a tool for managing objects in Amazon S3 storage. It allows for
making and removing "buckets" and uploading, downloading and removing
"objects" from these buckets.<br/>
Options:
-h, --help show this help message and exit
--configure Invoke interactive (re)configuration tool. Optionally
use as '--configure s3://some-bucket' to test access
to a specific bucket instead of attempting to list
them all.
-c FILE, --config=FILE
Config file name. Defaults to $HOME/.s3cfg
--dump-config Dump current configuration after parsing config files
and command line options and exit.
--access_key=ACCESS_KEY
AWS Access Key
--secret_key=SECRET_KEY
AWS Secret Key
--access_token=ACCESS_TOKEN
AWS Access Token
-n, --dry-run Only show what should be uploaded or downloaded but
don't actually do it. May still perform S3 requests to
get bucket listings and other information though (only
for file transfer commands)
-s, --ssl Use HTTPS connection when communicating with S3.
(default)
--no-ssl Don't use HTTPS.
-e, --encrypt Encrypt files before uploading to S3.
--no-encrypt Don't encrypt files.
-f, --force Force overwrite and other dangerous operations.
--continue Continue getting a partially downloaded file (only for
[get] command).
--continue-put Continue uploading partially uploaded files or
multipart upload parts. Restarts parts/files that
don't have matching size and md5. Skips files/parts
that do. Note: md5sum checks are not always
sufficient to check (part) file equality. Enable this
at your own risk.
--upload-id=UPLOAD_ID
UploadId for Multipart Upload, in case you want
continue an existing upload (equivalent to --continue-
put) and there are multiple partial uploads. Use
s3cmd multipart [URI] to see what UploadIds are
associated with the given URI.
--skip-existing Skip over files that exist at the destination (only
for [get] and [sync] commands).
-r, --recursive Recursive upload, download or removal.
--check-md5 Check MD5 sums when comparing files for [sync].
(default)
--no-check-md5 Do not check MD5 sums when comparing files for [sync].
Only size will be compared. May significantly speed up
transfer but may also miss some changed files.
-P, --acl-public Store objects with ACL allowing read for anyone.
--acl-private Store objects with default ACL allowing access for you
only.
--acl-grant=PERMISSION:EMAIL or USER_CANONICAL_ID
Grant stated permission to a given amazon user.
Permission is one of: read, write, read_acp,
write_acp, full_control, all
--acl-revoke=PERMISSION:USER_CANONICAL_ID
Revoke stated permission for a given amazon user.
Permission is one of: read, write, read_acp,
write_acp, full_control, all
-D NUM, --restore-days=NUM
Number of days to keep restored file available (only
for 'restore' command). Default is 1 day.
--restore-priority=RESTORE_PRIORITY
Priority for restoring files from S3 Glacier (only for
'restore' command). Choices available: bulk, standard,
expedited
--delete-removed Delete destination objects with no corresponding
source file [sync]
--no-delete-removed Don't delete destination objects [sync]
--delete-after Perform deletes AFTER new uploads when delete-removed
is enabled [sync]
--delay-updates *OBSOLETE* Put all updated files into place at end
[sync]
--max-delete=NUM Do not delete more than NUM files. [del] and [sync]
--limit=NUM Limit number of objects returned in the response body
(only for [ls] and [la] commands)
--add-destination=ADDITIONAL_DESTINATIONS
Additional destination for parallel uploads, in
addition to last arg. May be repeated.
--delete-after-fetch Delete remote objects after fetching to local file
(only for [get] and [sync] commands).
-p, --preserve Preserve filesystem attributes (mode, ownership,
timestamps). Default for [sync] command.
--no-preserve Don't store FS attributes
--exclude=GLOB Filenames and paths matching GLOB will be excluded
from sync
--exclude-from=FILE Read --exclude GLOBs from FILE
--rexclude=REGEXP Filenames and paths matching REGEXP (regular
expression) will be excluded from sync
--rexclude-from=FILE Read --rexclude REGEXPs from FILE
--include=GLOB Filenames and paths matching GLOB will be included
even if previously excluded by one of
--(r)exclude(-from) patterns
--include-from=FILE Read --include GLOBs from FILE
--rinclude=REGEXP Same as --include but uses REGEXP (regular expression)
instead of GLOB
--rinclude-from=FILE Read --rinclude REGEXPs from FILE
--files-from=FILE Read list of source-file names from FILE. Use - to
read from stdin.
--region=REGION, --bucket-location=REGION
Region to create bucket in. As of now the regions are:
us-east-1, us-west-1, us-west-2, eu-west-1, eu-
central-1, ap-northeast-1, ap-southeast-1, ap-
southeast-2, sa-east-1
--host=HOSTNAME HOSTNAME:PORT for S3 endpoint (default:
s3.amazonaws.com, alternatives such as s3-eu-
west-1.amazonaws.com). You should also set --host-
bucket.
--host-bucket=HOST_BUCKET
DNS-style bucket+hostname:port template for accessing
a bucket (default: %(bucket)s.s3.amazonaws.com)
--reduced-redundancy, --rr
Store object with 'Reduced redundancy'. Lower per-GB
price. [put, cp, mv]
--no-reduced-redundancy, --no-rr
Store object without 'Reduced redundancy'. Higher per-
GB price. [put, cp, mv]
--storage-class=CLASS
Store object with specified CLASS (STANDARD,
STANDARD_IA, ONEZONE_IA, INTELLIGENT_TIERING, GLACIER
or DEEP_ARCHIVE). [put, cp, mv]
--access-logging-target-prefix=LOG_TARGET_PREFIX
Target prefix for access logs (S3 URI) (for [cfmodify]
and [accesslog] commands)
--no-access-logging Disable access logging (for [cfmodify] and [accesslog]
commands)
--default-mime-type=DEFAULT_MIME_TYPE
Default MIME-type for stored objects. Application
default is binary/octet-stream.
-M, --guess-mime-type
Guess MIME-type of files by their extension or mime
magic. Fall back to default MIME-Type as specified by
--default-mime-type option
--no-guess-mime-type Don't guess MIME-type and use the default type
instead.
--no-mime-magic Don't use mime magic when guessing MIME-type.
-m MIME/TYPE, --mime-type=MIME/TYPE
Force MIME-type. Override both --default-mime-type and
--guess-mime-type.
--add-header=NAME:VALUE
Add a given HTTP header to the upload request. Can be
used multiple times. For instance set 'Expires' or
'Cache-Control' headers (or both) using this option.
--remove-header=NAME Remove a given HTTP header. Can be used multiple
times. For instance, remove 'Expires' or 'Cache-
Control' headers (or both) using this option. [modify]
--server-side-encryption
Specifies that server-side encryption will be used
when putting objects. [put, sync, cp, modify]
--server-side-encryption-kms-id=KMS_KEY
Specifies the key id used for server-side encryption
with AWS KMS-Managed Keys (SSE-KMS) when putting
objects. [put, sync, cp, modify]
--encoding=ENCODING Override autodetected terminal and filesystem encoding
(character set). Autodetected: UTF-8
--add-encoding-exts=EXTENSIONs
Add encoding to these comma delimited extensions i.e.
(css,js,html) when uploading to S3 )
--verbatim Use the S3 name as given on the command line. No pre-
processing, encoding, etc. Use with caution!
--disable-multipart Disable multipart upload on files bigger than
--multipart-chunk-size-mb
--multipart-chunk-size-mb=SIZE
Size of each chunk of a multipart upload. Files bigger
than SIZE are automatically uploaded as multithreaded-
multipart, smaller files are uploaded using the
traditional method. SIZE is in Mega-Bytes, default
chunk size is 15MB, minimum allowed chunk size is 5MB,
maximum is 5GB.
--list-md5 Include MD5 sums in bucket listings (only for 'ls'
command).
-H, --human-readable-sizes
Print sizes in human readable form (eg 1kB instead of
1234).
--ws-index=WEBSITE_INDEX
Name of index-document (only for [ws-create] command)
--ws-error=WEBSITE_ERROR
Name of error-document (only for [ws-create] command)
--expiry-date=EXPIRY_DATE
Indicates when the expiration rule takes effect. (only
for [expire] command)
--expiry-days=EXPIRY_DAYS
Indicates the number of days after object creation the
expiration rule takes effect. (only for [expire]
command)
--expiry-prefix=EXPIRY_PREFIX
Identifying one or more objects with the prefix to
which the expiration rule applies. (only for [expire]
command)
--progress Display progress meter (default on TTY).
--no-progress Don't display progress meter (default on non-TTY).
--stats Give some file-transfer stats.
--enable Enable given CloudFront distribution (only for
[cfmodify] command)
--disable Disable given CloudFront distribution (only for
[cfmodify] command)
--cf-invalidate Invalidate the uploaded filed in CloudFront. Also see
[cfinval] command.
--cf-invalidate-default-index
When using Custom Origin and S3 static website,
invalidate the default index file.
--cf-no-invalidate-default-index-root
When using Custom Origin and S3 static website, don't
invalidate the path to the default index file.
--cf-add-cname=CNAME Add given CNAME to a CloudFront distribution (only for
[cfcreate] and [cfmodify] commands)
--cf-remove-cname=CNAME
Remove given CNAME from a CloudFront distribution
(only for [cfmodify] command)
--cf-comment=COMMENT Set COMMENT for a given CloudFront distribution (only
for [cfcreate] and [cfmodify] commands)
--cf-default-root-object=DEFAULT_ROOT_OBJECT
Set the default root object to return when no object
is specified in the URL. Use a relative path, i.e.
default/index.html instead of /default/index.html or
s3://bucket/default/index.html (only for [cfcreate]
and [cfmodify] commands)
-v, --verbose Enable verbose output.
-d, --debug Enable debug output.
--version Show s3cmd version (2.2.0) and exit.
-F, --follow-symlinks
Follow symbolic links as if they are regular files
--cache-file=FILE Cache FILE containing local source MD5 values
-q, --quiet Silence output on stdout
--ca-certs=CA_CERTS_FILE
Path to SSL CA certificate FILE (instead of system
default)
--ssl-cert=SSL_CLIENT_CERT_FILE
Path to client own SSL certificate CRT_FILE
--ssl-key=SSL_CLIENT_KEY_FILE
Path to client own SSL certificate private key
KEY_FILE
--check-certificate Check SSL certificate validity
--no-check-certificate
Do not check SSL certificate validity
--check-hostname Check SSL certificate hostname validity
--no-check-hostname Do not check SSL certificate hostname validity
--signature-v2 Use AWS Signature version 2 instead of newer signature
methods. Helpful for S3-like systems that don't have
AWS Signature v4 yet.
--limit-rate=LIMITRATE
Limit the upload or download speed to amount bytes per
second. Amount may be expressed in bytes, kilobytes
with the k suffix, or megabytes with the m suffix
--no-connection-pooling
Disable connection re-use
--requester-pays Set the REQUESTER PAYS flag for operations
-l, --long-listing Produce long listing [ls]
--stop-on-error stop if error in transfer
--content-disposition=CONTENT_DISPOSITION
Provide a Content-Disposition for signed URLs, e.g.,
"inline; filename=myvideo.mp4"
--content-type=CONTENT_TYPE
Provide a Content-Type for signed URLs, e.g.,
"video/mp4"<br/>
Commands:
Make bucket
s3cmd mb s3://BUCKET
Remove bucket
s3cmd rb s3://BUCKET
List objects or buckets
s3cmd ls [s3://BUCKET[/PREFIX]]
List all object in all buckets
s3cmd la
Put file into bucket
s3cmd put FILE [FILE...] s3://BUCKET[/PREFIX]
Get file from bucket
s3cmd get s3://BUCKET/OBJECT LOCAL_FILE
Delete file from bucket
s3cmd del s3://BUCKET/OBJECT
Delete file from bucket (alias for del)
s3cmd rm s3://BUCKET/OBJECT
Restore file from Glacier storage
s3cmd restore s3://BUCKET/OBJECT
Synchronize a directory tree to S3 (checks files freshness using size and md5 checksum, unless overridden by options, see below)
s3cmd sync LOCAL_DIR s3://BUCKET[/PREFIX] or s3://BUCKET[/PREFIX] LOCAL_DIR or s3://BUCKET[/PREFIX] s3://BUCKET[/PREFIX]
Disk usage by buckets
s3cmd du [s3://BUCKET[/PREFIX]]
Get various information about Buckets or Files
s3cmd info s3://BUCKET[/OBJECT]
Copy object
s3cmd cp s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]
Modify object metadata
s3cmd modify s3://BUCKET1/OBJECT
Move object
s3cmd mv s3://BUCKET1/OBJECT1 s3://BUCKET2[/OBJECT2]
Modify Access control list for Bucket or Files
s3cmd setacl s3://BUCKET[/OBJECT]
Modify Bucket Policy
s3cmd setpolicy FILE s3://BUCKET
Delete Bucket Policy
s3cmd delpolicy s3://BUCKET
Modify Bucket CORS
s3cmd setcors FILE s3://BUCKET
Delete Bucket CORS
s3cmd delcors s3://BUCKET
Modify Bucket Requester Pays policy
s3cmd payer s3://BUCKET
Show multipart uploads
s3cmd multipart s3://BUCKET [Id]
Abort a multipart upload
s3cmd abortmp s3://BUCKET/OBJECT Id
List parts of a multipart upload
s3cmd listmp s3://BUCKET/OBJECT Id
Enable/disable bucket access logging
s3cmd accesslog s3://BUCKET
Sign arbitrary string using the secret key
s3cmd sign STRING-TO-SIGN
Sign an S3 URL to provide limited public access with expiry
s3cmd signurl s3://BUCKET/OBJECT <expiry_epoch|+expiry_offset>
Fix invalid file names in a bucket
s3cmd fixbucket s3://BUCKET[/PREFIX]
Create Website from bucket
s3cmd ws-create s3://BUCKET
Delete Website
s3cmd ws-delete s3://BUCKET
Info about Website
s3cmd ws-info s3://BUCKET
Set or delete expiration rule for the bucket
s3cmd expire s3://BUCKET
Upload a lifecycle policy for the bucket
s3cmd setlifecycle FILE s3://BUCKET
Get a lifecycle policy for the bucket
s3cmd getlifecycle s3://BUCKET
Remove a lifecycle policy for the bucket
s3cmd dellifecycle s3://BUCKET
List CloudFront distribution points
s3cmd cflist
Display CloudFront distribution point parameters
s3cmd cfinfo [cf://DIST_ID]
Create CloudFront distribution point
s3cmd cfcreate s3://BUCKET
Delete CloudFront distribution point
s3cmd cfdelete cf://DIST_ID
Change CloudFront distribution point parameters
s3cmd cfmodify cf://DIST_ID
Display CloudFront invalidation request(s) status
s3cmd cfinvalinfo cf://DIST_ID[/INVAL_ID]<br/>
For more information, updates and news, visit the s3cmd website:
http://s3tools.org </span></pre>
</div>
</edit-fold>
<edit-fold setup>
<h3 id="signup">Signup</h3>
<p>
I went to the <a href='https://login.linode.com/signup' target='_blank' rel='nofollow'>Linode signup page</a> and signed up with my email.
Using third parties for authentication means they track you more easily, and introduces an unnecessary dependency.
</p>
<p>
The email signup procedure requires a mobile phone for SMS-based MFA.
I would rather use a <a href='https://datatracker.ietf.org/doc/html/rfc6238' target='_blank' rel='nofollow'>TOTP authenticator app</a> instead of SMS for MFA.
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/linode/signup.webp" type="image/webp">
<source srcset="/blog/images/linode/signup.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/linode/signup.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
I want to ensure that I limit my financial liability.
One way of protecting myself is to limit the maximum amount that can be charged to the payment mechanism.
Because PayPal has no maximum transaction limit, it is not a suitable choice for a service with unlimited financial liability.
However, credit cards can have transaction limits set,
and Google Pay has the following limits:
</p>
<div class='quote'>
If you <a href='https://support.google.com/googlepay/answer/10623602' target='_blank' rel='nofollow'>set up your Google Pay balance to make contactless payments</a>,
there are some transaction limits:
<br><br>
Maximum single transaction amount: $2,000 USD.<br>
Daily maximum total transaction amount: $2,500 USD.<br>
Up to 15 transactions per day.<br>
Additional limits on the dollar amount or frequency of transactions may be imposed in accordance with the Google Pay Terms of Service.
<span class='quoteAttribution'> – From <a href='https://support.google.com/googlepay/answer/10187490#:~:text=If%20you%20set%20up%20your,to%2015%20transactions%20per%20day.' rel='nofollow' target='_blank'>Google Pay Help</a></span>
</div>
<p>
<a href='https://support.google.com/googlepay/answer/10187490' target='_blank' rel='nofollow'>Here</a> are additional limits for Google Pay.
Because of prior good experiences with how credit card processors handled fraud,
I decided to use a credit card with a low limit instead of Google Pay.
</p>
</edit-fold>
<edit-fold keys>
<h3 id="keys">Generating Linode Access Keys</h3>
<p>
I followed the <a href='https://www.linode.com/docs/products/storage/object-storage/guides/access-keys/' target='_blank' rel='nofollow'>directions</a>:
</p>
<ol>
<li>Logged into the <a href='https://cloud.linode.com/' target='_blank' rel='nofollow'>Cloud Manager</a>.</li>
<li>Selected the <b>Object Storage</b> menu item in the sidebar and clicked on the <b>Access Keys</b> tab.</li>
<li>Clicked on the <b>Create Access</b> Key button, which displays the <b>Create Access Key</b> panel.</li>
<li>Typed in the name <code>mslinn</code> as the label for the new access key.</li>
<li>Clicked the <kbd>Submit</kbd> button to create the access key.</li>
<li>The new access key and its secret key are displayed.
This is the only time that the secret key is visible.
I stored the access key and the secret key in my password manager, <a href='https://lastpass.com' target='_blank' rel='nofollow'><code>lastpass.com</code></a>.</li>
</ol>
</edit-fold>
<edit-fold s3cmd>
<h3 id="s3cmd_cfg">Configuring <span class="code">s3cmd</span></h3>
<p>
<code>s3cmd</code> is a Python program that works with all S3-compatible APIs, such as those provided by AWS,
Linode Storage, Google Cloud Storage and DreamHost DreamObjects.
I intend to use its <a href='https://s3tools.org/s3cmd-sync' target='_blank' rel='nofollow'><code>sync</code></a> subcommand to synchronize the
<code>www.mslinn.com</code> bucket contents in Linode Storage with the most recently generated version of this website by Jekyll.
</p>
<p>
<code>s3cmd</code> needs to be
<a href='https://s3tools.org/kb/item14.htm' target='_blank' rel='nofollow'>configured</a> for
<a href='https://www.linode.com/docs/products/storage/object-storage/guides/s3cmd' target='_blank' rel='nofollow'>Linode</a>
before it can be used.
Some configuration settings for Linode are non-obvious.
The following should work for most users,
with the caution that the access key and secret key are of course unique for every user.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcb313e4a1dfd'><button class='copyBtn' data-clipboard-target='#idcb313e4a1dfd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd --configure
<span class='unselectable'><br>Enter new values or accept defaults in brackets with Enter.
Refer to user manual for detailed description of all options.<br/>
Access key and Secret key are your identifiers for Amazon S3. Leave them empty for using the env variables.
Access Key [asdfasdf]: </span>asdfasdfasdf
<span class='unselectable'>Secret Key [asdfasdfasdf]: </span>asdfasdfasdf
<span class='unselectable'>Default Region [US]:<br/>
Use "s3.amazonaws.com" for S3 Endpoint and not modify it to the target Amazon S3.
S3 Endpoint [s3.amazonaws.com]: </span>us-east-1.linodeobjects.com<br/>
<span class='unselectable'>Use "%(bucket)s.s3.amazonaws.com" to the target Amazon S3. "%(bucket)s" and "%(location)s" vars can be used
if the target S3 system supports dns based buckets.
DNS-style bucket+hostname:port template for accessing a bucket [%(bucket)s.s3.amazonaws.com]: </span>%(bucket)s.us-east-1.linodeobjects.com<br/>
<span class='unselectable'>Encryption password is used to protect your files from reading
by unauthorized persons while in transfer to S3
Encryption password:
Path to GPG program [/usr/bin/gpg]:<br/>
When using secure HTTPS protocol all communication with Amazon S3
servers is protected from 3rd party eavesdropping. This method is
slower than plain HTTP, and can only be proxied with Python 2.7 or newer
Use HTTPS protocol [Yes]:<br/>
On some networks all internet access must go through a HTTP proxy.
Try setting it here if you can't connect to S3 directly
HTTP Proxy server name:<br/>
New settings:
Access Key: </span>asdfasdfasdf
<span class='unselectable'>Secret Key: </span>asdfasdfasdf
<span class='unselectable'>Default Region: US
S3 Endpoint: </span>https://us-east-1.linodeobjects.com
<span class='unselectable'>DNS-style bucket+hostname:port template for accessing a bucket: </span>%(bucket)s.us-east-1.linodeobjects.com
<span class='unselectable'>Encryption password:
Path to GPG program: /usr/bin/gpg
Use HTTPS protocol: True
HTTP Proxy server name:
HTTP Proxy server port: 0<br/>
Test access with supplied credentials? [Y/n] n
Save settings? [y/N] y
Configuration saved to '/home/mslinn/.s3cfg' </span></pre>
</div>
<p>
I was unable to successfully test access.
Two different errors appeared at different times:
</p>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf115391d51e0'>Please wait, attempting to list all buckets...
ERROR: Test failed: 403 (SignatureDoesNotMatch)
ERROR: Test failed: [Errno -2] Name or service not known<br/></pre>
</div>
<p>
Eventually I saved the configuration without testing as shown above.
This created a file called <code>$HOME/.s3cfg</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>~/.s3cfg</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id449a510c7a61'>[default]
access_key = asdfasdfasdf
access_token =
add_encoding_exts =
add_headers =
bucket_location = US
ca_certs_file =
cache_file =
check_ssl_certificate = True
check_ssl_hostname = True
cloudfront_host = cloudfront.amazonaws.com
connection_max_age = 5
connection_pooling = True
content_disposition =
content_type =
default_mime_type = binary/octet-stream
delay_updates = False
delete_after = False
delete_after_fetch = False
delete_removed = False
dry_run = False
enable_multipart = True
encoding = UTF-8
encrypt = False
expiry_date =
expiry_days =
expiry_prefix =
follow_symlinks = False
force = False
get_continue = False
gpg_command = /usr/bin/gpg
gpg_decrypt = %(gpg_command)s -d --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s
gpg_encrypt = %(gpg_command)s -c --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s
gpg_passphrase =
guess_mime_type = True
host_base = us-east-1.linodeobjects.com
host_bucket = %(bucket)s.us-east-1.linodeobjects.com
human_readablesizes = False
invalidate_default_index_on_cf = False
invalidate_default_index_root_on_cf = True
invalidate_on_cf = False
kms_key =
limit = -1
limitrate = 0
list_md5 = False
log_target_prefix =
long_listing = False
max_delete = -1
mime_type =
multipart_chunksize_mb = 15
multipart_copy_chunksize_mb = 1024
multipart_max_chunks = 10000
preserve_attrs = True
progress_meter = True
proxy_host =
proxy_port = 0
public_url_use_https = False
put_continue = False
recursive = False
recv_chunk = 65536
reduced_redundancy = False
requester_pays = False
restore_days = 1
restore_priority = Standard
secret_key = asdfasdfasdf
send_chunk = 65536
server_side_encryption = False
signature_v2 = False
signurl_use_https = False
simpledb_host = sdb.amazonaws.com
skip_existing = False
socket_timeout = 300
ssl_client_cert_file =
ssl_client_key_file =
stats = False
stop_on_error = False
storage_class =
throttle_max = 100
upload_id =
urlencoding_mode = normal
use_http_expect = False
use_https = True
use_mime_magic = True
verbosity = INFO
website_endpoint = http://%(bucket)s.s3-website-%(location)s.amazonaws.com/
website_error =
website_index = index.html</pre>
</div>
<p>
According to the docs
(<a href='https://www.linode.com/docs/products/storage/object-storage/guides/urls/' target='_blank' rel='nofollow'>Access Buckets and Files through URLs</a>),
the configuration file needs <code>website_endpoint: http://%(bucket)s.website-[cluster-url]/</code>.
However, this is an error; instead of <code>cluster-url</code>, which might be <code>https://us-east-1.linodeobjects.com</code>,
<code>cluster-id</code> should be used, which for me was simply <code>us-east-1</code>.
<span class="bg_yellow">Thus the endpoint for me,
and everyone with a bucket at <code>us-east-1</code> in Newark, New Jersey should be:<br>
<code>http://%(bucket)s.<span class="bg_yellow">website-</span>us-east-1.linodeobjects.com/</code></span>
</p>
<p>
After editing the file to manually change the value of <code>website_endpoint</code>,
I tried to list the buckets, and that worked:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id424f5772aaba'><button class='copyBtn' data-clipboard-target='#id424f5772aaba' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd ls
<span class='unselectable'>2022-07-01 17:32 s3://mslinn </span></pre>
</div>
</edit-fold>
<edit-fold linodecli>
<h3 id="linode-cli"><span class="code">linode-cli</span></h3>
<p>
Next I tried
<a href='https://github.com/linode/linode-cli' target='_blank' rel='nofollow'><code>linode-cli</code></a>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7369dea94374'><button class='copyBtn' data-clipboard-target='#id7369dea94374' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pip install linode-cli
<span class='unselectable'>Collecting linode-cli
Downloading linode_cli-5.21.0-py2.py3-none-any.whl (204 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 204.2/204.2 kB 4.8 MB/s eta 0:00:00
Collecting terminaltables
Downloading terminaltables-3.1.10-py2.py3-none-any.whl (15 kB)
Collecting PyYAML
Downloading PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (682 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 682.2/682.2 kB 16.8 MB/s eta 0:00:00
Requirement already satisfied: requests in /home/mslinn/venv/default/lib/python3.10/site-packages (from linode-cli) (2.28.0)
Requirement already satisfied: charset-normalizer~=2.0.0 in /home/mslinn/venv/default/lib/python3.10/site-packages (from requests->linode-cli) (2.0.12)
Requirement already satisfied: idna<4,>=2.5 in /home/mslinn/venv/default/lib/python3.10/site-packages (from requests->linode-cli) (3.3)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in /home/mslinn/venv/default/lib/python3.10/site-packages (from requests->linode-cli) (1.26.9)
Requirement already satisfied: certifi>=2017.4.17 in /home/mslinn/venv/default/lib/python3.10/site-packages (from requests->linode-cli) (2022.6.15)
Installing collected packages: terminaltables, PyYAML, linode-cli
Successfully installed PyYAML-6.0 linode-cli-5.21.0 terminaltables-3.1.10 </span></pre>
</div>
<p>
The documentation fails to mention that the first time <code>linode-cli</code> is executed, and every time it is invoked with the <code>configure</code> subcommand,
it attempts to open a web browser.
WSL and WSL2 will not respond appropriately
unless an X client is already running, for example
<a href='https://apps.microsoft.com/store/detail/gwsl/9NL6KD1H33V3' target='_blank' rel='nofollow'>GWSL</a>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id305a18a21180'><button class='copyBtn' data-clipboard-target='#id305a18a21180' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>linode-cli configure
<span class='unselectable'><br>Welcome to the Linode CLI.
This will walk you through some initial setup.<br/>
The CLI will use its web-based authentication to log you in.
If you prefer to supply a Personal Access Token, use
<code>linode-cli configure --token</code><br/>
Press enter to continue.
This will open a browser and proceed with authentication.
A browser should open directing you to this URL to authenticate:<br/>
https://login.linode.com/oauth/authorize?client_id=asdfasdfasdf&response_type=token&scopes=*&redirect_uri=http://localhost:45789<br/>
If you are not automatically directed there, please copy/paste the link into your browser
to continue..<br/><br/>
Configuring mslinn<br/><br/>
Default Region for operations. Choices are:
1 - ap-west
2 - ca-central
3 - ap-southeast
4 - us-central
5 - us-west
6 - us-southeast
7 - us-east
8 - eu-west
9 - ap-south
10 - eu-central
11 - ap-northeast<br/>
Default Region (Optional): 7<br/>
Default Type of Linode to deploy. Choices are:
1 - g6-nanode-1
2 - g6-standard-1
3 - g6-standard-2
4 - g6-standard-4
5 - g6-standard-6
6 - g6-standard-8
7 - g6-standard-16
8 - g6-standard-20
9 - g6-standard-24
10 - g6-standard-32
11 - g7-highmem-1
12 - g7-highmem-2
13 - g7-highmem-4
14 - g7-highmem-8
15 - g7-highmem-16
16 - g6-dedicated-2
17 - g6-dedicated-4
18 - g6-dedicated-8
19 - g6-dedicated-16
20 - g6-dedicated-32
21 - g6-dedicated-48
22 - g6-dedicated-50
23 - g6-dedicated-56
24 - g6-dedicated-64
25 - g1-gpu-rtx6000-1
26 - g1-gpu-rtx6000-2
27 - g1-gpu-rtx6000-3
28 - g1-gpu-rtx6000-4<br/>
Default Type of Linode (Optional):<br/>
Default Image to deploy to new Linodes. Choices are:
1 - linode/almalinux8
2 - linode/almalinux9
3 - linode/alpine3.12
4 - linode/alpine3.13
5 - linode/alpine3.14
6 - linode/alpine3.15
7 - linode/alpine3.16
8 - linode/arch
9 - linode/centos7
10 - linode/centos-stream8
11 - linode/centos-stream9
12 - linode/debian10
13 - linode/debian11
14 - linode/debian9
15 - linode/fedora34
16 - linode/fedora35
17 - linode/fedora36
18 - linode/gentoo
19 - linode/kali
20 - linode/debian11-kube-v1.20.15
21 - linode/debian9-kube-v1.20.7
22 - linode/debian9-kube-v1.21.1
23 - linode/debian11-kube-v1.21.12
24 - linode/debian9-kube-v1.22.2
25 - linode/debian11-kube-v1.22.9
26 - linode/debian11-kube-v1.23.6
27 - linode/opensuse15.3
28 - linode/opensuse15.4
29 - linode/rocky8
30 - linode/slackware14.2
31 - linode/slackware15.0
32 - linode/ubuntu16.04lts
33 - linode/ubuntu18.04
34 - linode/ubuntu20.04
35 - linode/ubuntu21.10
36 - linode/ubuntu22.04
37 - linode/centos8
38 - linode/slackware14.1
39 - linode/ubuntu21.04<br/>
Default Image (Optional):
Active user is now mslinn<br/>
Config written to /home/mslinn/.config/linode-cli </span></pre>
</div>
<p>
<code>linode-cli</code> configuration created this file:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>~/.config/linode-cli</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb45f598600a0'>[DEFAULT]
default-user = mslinn
[mslinn]
token = asdfasdfasdfasdfasdfasdf
region = us-east</pre>
</div>
<p>
Right after I did the above, I was surprised to get the following email from Linode:
</p>
<div class="quote">
Hello,<br><br>
This is a notification to inform you that a device has been trusted to skip authentication on your Linode Account (mslinn).
The request came from the following IP address: 70.53.179.203.
<br><br>
The device will not be prompted for a username or password for 30 days.
<br><br>
If this action did not originate from you, we recommend logging in and changing your password immediately.
Also, as an extra layer of security, we highly recommend enabling two-factor authentication on your account.
Please see the link below for more information on how to enable two-factor authentication:
<br><br>
<a href='https://www.linode.com/docs/security/linode-manager-security-controls/#enable-two-factor-authentication' target='_blank' rel='nofollow'>Two-Factor Authentication</a>
<br><br>
If you have any questions or concerns, please do not hesitate to contact us 24/7 by opening a support ticket from within the Linode Manager,
giving us a call, or emailing <a href='mailto:support@linode.com'>support@linode.com</a>.
<br><br>
<a href='https://www.linode.com/contact' target='_blank' rel='nofollow'>Contact</a>
<br><br>
Thank you,<br>
The Linode.com Team<br>
<br><br>
<a href='mailto:support@linode.com'>Contact Us</a>
</div>
<p class="alert rounded shadow">
I began to suspect that Linode’s documentation had major deficiencies at this point.
Instead of merely configuring a command-line client, my IP was whitelisted unexpectedly.
I have nothing polite to say about this.
</p>
<p>
After poking around a bit, I discovered my
<a href='https://cloud.linode.com/profile/tokens' target='_blank' rel='nofollow'>API Tokens page</a> on Linode.
It seems that the <code>linode-cli</code> created a personal access token with a label containing the name of the computer used:
<code>Linode CLI @ camille</code>.
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/linode/apiTokens.webp" type="image/webp">
<source srcset="/blog/images/linode/apiTokens.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/linode/apiTokens.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
Clicking on the <kbd>View Scopes</kbd> button above displays the permissions granted to the token:
</p>
<div class='imgWrapper imgFlex center halfsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/linode/cliPermissions.webp" type="image/webp">
<source srcset="/blog/images/linode/cliPermissions.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/linode/cliPermissions.png"
style='width: 100%; '
/>
</picture>
</div>
<p class="alert rounded shadow">
This personal access token is all-powerful.
The <code>linode-cli</code> quietly created a personal access token with all permissions enabled,
and did not warn the user, and did not say how to reduce or limit the permissions.
<br><br>
Security is enhanced when the minimum permission necessary is provided to accomplish necessary tasks.
Instead, this token silently maximizes the financial risk to the person or entity who pays for the account.
</p>
</edit-fold>
<edit-fold bucket>
<h3 id="bucket">Creating the Website Bucket</h3>
<p class="alert rounded shadow">
The name of the bucket must exactly match the name of the subdomain that the website is served from.
If you try to serve content from a misnamed bucket, you will not be able to apply an SSL certificate;
instead, the certificate will be issued by <code>linodeobjects.com</code>.
</p>
<p>
I wanted to test using the subdomain <code>linode.mslinn.com</code>,
and then if all was well, switch <code>www.mslinn.com</code> over to Linode Storage.
Because buckets cannot be renamed, I had to make 2 buckets with identical contents,
one called <code>linode.mslinn.com</code> and the other called <code>www.mslinn.com</code>.
If I decide to stay with Linode,
I will delete the bucket I am using for testing, called <code>linode.mslinn.com</code>,
and run this website from the <code>www.mslinn.com</code> bucket.
</p>
<p>
I created a website-enabled bucket called <code>www.mslinn.com</code> as follows:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idad34c29e5e2b'><button class='copyBtn' data-clipboard-target='#idad34c29e5e2b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd mb --acl-public s3://www.mslinn.com
<span class='unselectable'>Bucket 's3://www.mslinn.com/' created </span>
<span class='unselectable'>$ </span>s3cmd ls
<span class='unselectable'>2022-07-01 17:32 s3://www.mslinn.com </span>
<span class='unselectable'>$ </span>s3cmd ws-create \
--ws-index=index.html \
--ws-error=404.html \
s3://www.mslinn.com
<span class='unselectable'>Bucket 's3://www.mslinn.com/': website configuration created. </span></pre>
</div>
<p>
The <code>s3cmd ws-info</code> subcommand displays the Linode Object Storage bucket URL as the <b>Website endpoint</b>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id03b44ea0841f'><button class='copyBtn' data-clipboard-target='#id03b44ea0841f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd ws-info s3://www.mslinn.com
<span class='unselectable'>Bucket s3://www.mslinn.com/: Website configuration
Website endpoint: http://www.mslinn.com.website-us-east-1.linodeobjects.com/
Index document: index.html
Error document: 404.html </span></pre>
</div>
<h3 id="subdomain">Defining a CNAME for the Bucket</h3>
<p>
Although Linode provides a <a href='https://www.linode.com/docs/guides/dns-manager/' target='_blank' rel='nofollow'>DNS Manager</a>,
while I am testing out Linode I wanted to continue using Namecheap for DNS, so I navigated to
<a href='https://ap.www.namecheap.com/Domains/DomainControlPanel/mslinn.com/advancedns' target='_blank' rel='nofollow'><code>ap.www.namecheap.com/Domains/DomainControlPanel/mslinn.com/advancedns</code></a>
and created CNAMEs like this:
</p>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3fd0d9ccdded'><button class='copyBtn' data-clipboard-target='#id3fd0d9ccdded' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>linode.mslinn.com CNAME linode.mslinn.com.<span class="bg_yellow">website-</span>us-east-1.linodeobjects.com
www.mslinn.com CNAME www.mslinn.com.<span class="bg_yellow">website-</span>us-east-1.linodeobjects.com</pre>
</div>
<p class="alert rounded shadow">
Warning: if the <code>CNAME</code> does not point to a URL with the word
<code>website</code> in it, for example
<code>www.mslinn.com.us-east-1.linodeobjects.com</code>,
the default index file and the error file will not automatically be served when expected.
</p>
</edit-fold>
<edit-fold cert>
<h3 id="cert">Making a Free SSL Certificate</h3>
<p>
<a href='https://www.ssllabs.com/ssltest/' target='_blank' rel='nofollow'>Qualys / SSL Labs</a>
rates the SSL security provided by AWS CloudFront using the AWS-generated SSL
certificates with a “B” grade.
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/aws/sslReport.webp" type="image/webp">
<source srcset="/blog/images/aws/sslReport.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/aws/sslReport.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
Lets see if Linode Object Storage can host a more secure website when provided
with a 4096-bit certificate generated by <code>certbot</code>/<code>letsencrypt</code>.
The conclusion of this post will reveal the answer.
</p>
<p>
Please read <a href='/blog/2022/06/15/certbot.html'>Creating and Renewing Letsencrypt Wildcard SSL Certificates</a> for details.
</p>
</edit-fold>
<edit-fold linode_dns>
<h4 id="linodeDns">Linode DNS Authentication</h4>
<p>
Eventually, if I stick with Linode, I could use the
<a href='https://certbot-dns-linode.readthedocs.io/en/stable/' target='_blank' rel='nofollow'><code>certbot</code> DNS plugin for Linode</a>,
but I will not use it right now because I am trialing Linode.
Use of this plugin requires a configuration file containing Linode API credentials,
obtained from your Linode account’s
<a href='https://cloud.linode.com/profile/tokens' target='_blank' rel='nofollow'>Applications & API Tokens</a> page.
I stored the credentials in <code>~/.certbot/linode.ini</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>~/.certbot/linode.ini</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb0103392772c'><button class='copyBtn' data-clipboard-target='#idb0103392772c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button># Linode API credentials used by Certbot
dns_linode_key = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64
dns_linode_version = [<blank>|3|4]</pre>
</div>
<p>
Here is how to create an SSL certificate <code>certbot</code> using Linode DNS authentication:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id56f3ed8d46f0'><button class='copyBtn' data-clipboard-target='#id56f3ed8d46f0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>certbot certonly \
--dns-linode \
--dns-linode-credentials ~/.certbot/linode.ini \
--rsa-key-size 4096 \
-d mslinn.com \
-d *.mslinn.com \
--config-dir ~/.certbot/mslinn.com/config \
--logs-dir ~/.certbot/mslinn.com/logs \
--work-dir ~/.certbot/mslinn.com/work</pre>
</div>
<h2 id="certUp">Installing the Certificate</h2>
<p>
Now I created 2 certificates: one with the full chain of responsibility (<code>fullchain.pem</code>) and one containing my private key (<code>privkey.pem</code>).
I used bash environment variables called <code>CERT_DIR</code>, <code>CERT</code> and <code>KEY</code> to make the incantation convenient to type in.
<code>CERT</code> and <code>KEY</code> contain the contents of the files <code>fullchain.pem</code> and <code>privkey.pem</code>, respectively.
As you can see, the result was a valid (wildcard) SSL certificate.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddd653dbe01f9'><button class='copyBtn' data-clipboard-target='#iddd653dbe01f9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>CERT_DIR=/home/mslinn/.certbot/mslinn.com/config/live/mslinn.com
<span class='unselectable'>$ </span>CERT="$( cat $CERT_DIR/fullchain.pem )"
<span class='unselectable'>$ </span>KEY="$( cat $CERT_DIR/privkey.pem )"
<span class='unselectable'>$ </span>linode-cli object-storage ssl-upload \
us-east-1 www.mslinn.com \
--certificate "$CERT" \
--private_key "$KEY"
<div style="line-height: 100%;"><span class='unselectable'>┌──────┐
│ ssl │
├──────┤
│ True │
└──────┘ </span></div></pre>
</div>
</edit-fold>
<edit-fold sync>
<h3 id="sync">Syncing the Website Bucket</h3>
<p>
I wrote a bash script to build this Jekyll-generated website.
Jekyll places the generated website in a directory called <code>_site</code>.
I <a href='https://s3tools.org/s3cmd-sync' target='_blank' rel='nofollow'>synchronized</a>
the contents of the freshly generated <code>mslinn.com</code> website,
stored in <code>_site/</code>, with the bucket as follows.
Note the trailing slash on <code>_site<span class="bg_yellow">/</span></code>,
<a href='https://s3tools.org/s3cmd-sync' target='_blank' rel='nofollow'>it is significant</a>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbe5198410a9b'><button class='copyBtn' data-clipboard-target='#idbe5198410a9b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd sync \
--acl-public --delete-removed --guess-mime-type --quiet \
_site<span class="bg_yellow">/</span> \
s3://www.mslinn.com
<span class='unselectable'>INFO: No cache file found, creating it.
INFO: Compiling list of local files...
INFO: Running stat() and reading/calculating MD5 values on 2157 files, this may take some time...
INFO: [1000/2157]
INFO: [2000/2157]
INFO: Retrieving list of remote files for s3://www.mslinn.com/_site ...
INFO: Found 2157 local files, 0 remote files
INFO: Verifying attributes...
INFO: Summary: 2137 local files to upload, 20 files to remote copy, 0 remote files to delete
... lots more output ...
Done. Uploaded 536630474 bytes in 153.6 seconds, 3.33 MB/s. </span></pre>
</div>
<p>
Originally, I did not provide the <code>--acl-public</code> option to <code>s3cmd sync</code>.
That meant the files in the bucket were all private – the opposite of what is required for a website.
Here is the incantation for making all the files in the bucket publicly readable:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6a0c4e773520'><button class='copyBtn' data-clipboard-target='#id6a0c4e773520' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>s3cmd setacl s3://www.mslinn.com/ \
--acl-public --recursive --quiet</pre>
</div>
<p>
Linode’s S3 implementation faithfully mirrors a problem that AWS S3 has.
CSS files are automatically assigned the MIME type <code>text/plain</code>, instead of <code>text/css</code>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2a1c590cfafd'><button class='copyBtn' data-clipboard-target='#id2a1c590cfafd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl -sI http://linode.mslinn.com/assets/css/style.css | \
grep Content-Type
<span class='unselectable'>Content-Type: text/plain </span></pre>
</div>
<p>
I re-uploaded the CSS files with the proper mime type using this incantation:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddd976e044b79'><button class='copyBtn' data-clipboard-target='#iddd976e044b79' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cd _site
<span class='unselectable'>$ </span>find . -name *.css -exec \
s3cmd put -P --mime-type=text/css {} s3://www.mslinn.com/{} \;</pre>
</div>
<p>
It should be possible to provide a MIME-type mapping somehow!
Linode, if you are reading this, please provide an extension to the S3 functionality,
so customers could go beyond AWS's bugs.
</p>
</edit-fold>
<h2 id="result">The Result</h2>
<p>
Success!
The same content can be fetched with 3 different URLs:
</p>
<ul>
<li>
<a href='https://linode.mslinn.com' target='_blank' rel='nofollow'><code>https://linode.mslinn.com</code></a> uses the free wildcard certificate that was created above.
</li>
<li>
<a href='http://linode.mslinn.com.website-us-east-1.linodeobjects.com' target='_blank' rel='nofollow'><code>http://linode.mslinn.com.website-us-east-1.linodeobjects.com</code></a>
works without an SSL certificate.
</li>
<li>
<a href='https://linode.mslinn.com.website-us-east-1.linodeobjects.com' target='_blank' rel='nofollow'><code>https://linode.mslinn.com.website-us-east-1.linodeobjects.com</code></a>
uses a certificate from <code>us-east-1.linodeobjects.com</code>, which shows as an error, as it should.
The error would be prevented if the certificate was a wildcard certificate issued by <code>linodeobjects.com</code> instead.
</li>
</ul>
<p>
Qualys / SSL Labs rated the site on Linode Storage with the Let’s Encrypt SSL certificate:
and gave it the same rating as when the site was hosted on AWS S3 / CloudFront.
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/linode/sslReportLinode.webp" type="image/webp">
<source srcset="/blog/images/linode/sslReportLinode.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/linode/sslReportLinode.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
BTW, you can test the date range of a certificate with this incantation:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide58fdf47ba49'><button class='copyBtn' data-clipboard-target='#ide58fdf47ba49' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl https://linode.mslinn.com -vI --stderr - | grep 'date:'
<span class='unselectable'>* start date: Nov 25 17:46:07 2022 GMT
* expire date: Feb 23 17:46:06 2023 GMT </span></pre>
</div>
<p>
The site feels quite responsive.
<span style='font-size: 3em;'>😁</span>
</p>
Considering Microsoft Azure for Static Websites2022-06-20T00:00:00-04:00https://mslinn.github.io/blog/2022/06/20/azure<p>
After realizing that
<a href='/blog/2022/06/02/azure-security.html'>AWS does not integrate security with real-time billing</a>,
and being presented with a huge bill after my account was hijacked,
I decided to see if Microsoft Azure offered me better financial security.
</p>
<p>
In particular, I am looking for two things:
</p>
<ol>
<li>
<b>A more secure authentication mechanism</b> –
If AWS credentials are compromised, they can be used from anywhere in the world.
</li>
<li>
<b>Restricting scope and scale of authorized services</b> –
With AWS, if an IAM user has a role that allows EC2 instances to be launched,
any number of any size EC2 instances can be launched.
There is no way to cap the number or type of EC2 instances,
and AWS real-time billing is not integrated with the authentication mechanism.
Furthermore, AWS has a peculiar set of busy work tasks for victims of account hijacking
that appears to be targeted more towards increasing support revenue than address root issues.
This means that the bad guys have no limit to the financial damage they can incur on their victims.
</li>
</ol>
<p class="alert rounded shadow">
For PaaS vendors such as AWS, Azure, Digital Ocean, Cloudflare, ScaleWay, etc:
“pay-as-you-go” is shorthand for “there is nothing you can do to limit your financial liability”.
</p>
<p>
I set out to discover if Azure also suffers from these same problems.
</p>
<h2 id="policies_roles">Azure Roles and Policies</h2>
<div class='quote'>
<h2>Policies</h2>
<p>
<a href='https://docs.microsoft.com/en-us/azure/governance/policy/overview' target='_blank' rel='nofollow'>Azure policies</a>
can be used to define the desired behavior for your organization's
<a href='https://docs.microsoft.com/en-us/azure/virtual-machines/windows/policy' target='_blank' rel='nofollow'>Windows VMs</a> and
<a href='https://docs.microsoft.com/en-us/azure/virtual-machines/linux/policy' target='_blank' rel='nofollow'>Linux VMs</a>.
By using policies, an organization can enforce various conventions and rules throughout the enterprise. Enforcement of the desired behavior can help mitigate risk while contributing to the success of the organization.
</p>
<h3>Azure role-based access control</h3>
<p>
Using <a href='https://docs.microsoft.com/en-us/azure/role-based-access-control/overview' target='_blank' rel='nofollow'>Azure role-based access control (Azure RBAC)</a>,
you can segregate duties within your team and grant only the amount of access to users on your
VM that they need to perform their jobs.
Instead of giving everybody unrestricted permissions on the VM, you can allow only certain actions.
You can configure access control for the VM in the
<a href='https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal' target='_blank' rel='nofollow'>Azure portal</a>,
using the <a href='https://docs.microsoft.com/en-us/cli/azure/role' target='_blank' rel='nofollow'>Azure CLI</a>,
or <a href='https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-powershell' target='_blank' rel='nofollow'>Azure PowerShell</a>.
</p>
<span class='quoteAttribution'> – From <a href='https://docs.microsoft.com/en-us/azure/virtual-machines/security-policy' rel='nofollow' target='_blank'>Azure Virtual Machines Documentation</a></span>
</div>
<p>
Ekran Systems publishes a
<a href='https://www.ekransystem.com/en/blog/rbac-vs-abac' target='_blank' rel='nofollow'>good description</a>
of various flavors of RBAC, and the more complex ABAC.
Okta, Inc. also publishes a
<a href='https://www.okta.com/identity-101/role-based-access-control-vs-attribute-based-access-control/' target='_blank' rel='nofollow'>good overview of RBAC and ABAC</a>.
I have no connection with either company.
</p>
<p>
I soon discovered the following passage in the Azure documentation,
which appeared to exactly match the second item in the wish list at the top of this article:
</p>
<div class='quote'>
Azure RBAC focuses on managing user
<a href='https://docs.microsoft.com/en-us/azure/role-based-access-control/resource-provider-operations' target='_blank' rel='nofollow'>actions</a>
at different scopes.
If control of an action is required, then Azure RBAC is the correct tool to use.
Even if an individual has access to perform an action,
if the result is a non-compliant resource, Azure Policy still blocks the create or update.
<br><br>
The combination of Azure RBAC and Azure Policy provides full scope control in Azure.
<span class='quoteAttribution'> – From <a href='https://docs.microsoft.com/en-us/azure/governance/policy/overview#azure-policy-and-azure-rbac' rel='nofollow' target='_blank'>Azure documentation</a></span>
</div>
<p class="alert rounded shadow">
<a href='https://docs.aws.amazon.com/prescriptive-guidance/latest/saas-multitenant-api-access-authorization/access-control-types.html' target='_blank' rel='nofollow'>AWS RBAC</a>
does not provide functionality equivalent to that provided by Microsoft Azure RBAC plus Azure Policy,
even when combined with other AWS functionality.
</p>
<h2 id="rbacAgain">No Limits On Other Azure Services</h2>
<p>
The financial limitations that Azure allows you to impose are only available for virtual machines.
Users are subject to unlimited financial liability from other Azure services.
It seems I have <strike>two</strike>three choices:
</p>
<ol>
<li>Figure out how to set up RBAC and accept that other services might run up a huge bill.</li>
<li>Look for a fixed-price hosting service</li>
<li>Give <a href="#spendLimit">Azure Spending Limit</a> a try</li>
</ol>
<p>
Before I sign off today, I encountered a blocking issue with Azure that I'd like to mention.
</p>
<h2 id="ad">Active Directory?</h2>
<p>
I want to update my website by running a command-line command that runs something analogous to
<a href='https://linux.die.net/man/1/rsync' target='_blank' rel='nofollow'><code>rsync</code></a>.
I spent quite some time looking for how this might be done with Azure.
</p>
<p>
Before long, it seemed that Active Directory was the best way forward, however setting it up is daunting.
Apparently a <a href='https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals' target='_blank' rel='nofollow'>service principal</a>
is better suited for scripts that run on-premises, like my home office.
I got the impression that I need to register an app;
unsure what app might be required for hosting a web site on Azure Blob Storage with Azure CDN.
</p>
<p>
These Active Directory docs look like a lot of abstract, generalized information that is way more complex than I need.
I found a <a href='https://docs.microsoft.com/en-us/azure/storage/blobs/authorize-access-azure-active-directory' target='_blank' rel='nofollow'>streamlined article</a>
for my use case.
Not sure what this means:
“Only storage accounts created with the Azure Resource Manager deployment model support Azure AD authorization.”
This streamlined document is not very streamlined.
For me, this issue is a significant barrier to adoption.
</p>
<h2 id="spendLimit">Update: Azure Spending Limit</h2>
<p>
I just bumped into an article entitled
<a href='https://docs.microsoft.com/en-us/azure/cost-management-billing/manage/spending-limit' target='_blank' rel='nofollow'>Azure spending limit</a>.
Perhaps this might be useful.
I tried to find out more from Microsoft,
but at first they said I would have to subscribe to an expensive support plan before anyone would speak to me.
That is not how I wish to proceed.
Later, I opened a ticket inquiring about Azure customers facing unlimited financial liability.
</p>
<h2>Update 2022-06-29</h2>
<p>
The ticket was escalated twice,
then Microsoft sent me the following official response (emphasis is mine):
</p>
<div class='quote'>
Correct, at this moment that feature of setting limits does not exist,
<b class="bg_yellow">Azure is not able to safeguard customers from unlimited financial liability</b>.
<br><br>
We are a support team that we provide help with current account or billing issues,
in addition to that, I would like to mention that Azure products are constantly being updated,
also Azure is constantly improving to offer better services to our customers.
For this reason, I would like to invite you to leave your feedback in the Azure feedback forum
(<a href='https://feedback.azure.com/d365community/forum/79b1327d-d925-ec11-b6e6-000d3a4f06a4' target='_blank' rel='nofollow'>General Feedback · Community (azure.com)</a>),
so your feedback is forwarded through the right channel and to the Microsoft internal resources that
work on it.
</div>
<p>
I responded by indicating that I would close my Azure account.
</p>
<p class="alert rounded shadow">
While the practice of imposing unlimited financial liability on customers is common today for PaaS vendors,
it is unnecessary and predatory.
If in the future I need to use a product or service that incurs unlimited financial liability for a client,
I will require the client to accept the liability.
</p>
Creating and Renewing Letsencrypt Wildcard SSL Certificates2022-06-15T00:00:00-04:00https://mslinn.github.io/blog/2022/06/15/certbot<p>
The process of making wildcard SSL certificates can be hard to get right at first.
However, I use wildcard SSL certificates for most of my internet domains, so it is important to me.
This blog post distills the essence of what I have found to be important when creating these certificates.
</p>
<p class="alert rounded shadow">
Update – 9 months after writing this blog post,
I wrote about a better way to generate wildcard SSL certificates:
<a href='/blog/2023/03/02/lego.html'>Wildcard SSL Certificates for Letsencrypt with Lego</a>.
</p>
<edit-fold cert>
<div class='imgWrapper imgFlex right quartersize' style=' '>
<a href='https://letsencrypt.org/' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/ddns/letsencrypt-logo.webp" type="image/webp">
<source srcset="/blog/images/ddns/letsencrypt-logo.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/ddns/letsencrypt-logo.png"
style='width: 100%; '
/>
</picture>
</a>
</div>
<h2 id="cert">Making a Free Wildcard SSL Certificate</h2>
<p>
<a href='https://certbot.eff.org/' target='_blank' rel='nofollow'><code>Certbot</code></a> powers <a href='https://www.eff.org/' target='_blank' rel='nofollow'>EFF</a>’s
<a href='https://letsencrypt.org/' target='_blank' rel='nofollow'>Letsencrypt</a> capability.
Source code is on <a href='https://github.com/certbot/certbot' target='_blank' rel='nofollow'>GitHub</a>.
</p>
<p>
Install <code>certbot</code> on Debian distros such as Ubuntu as follows:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable clear' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3923f54c1d59'><button class='copyBtn' data-clipboard-target='#id3923f54c1d59' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo apt install certbot
<span class='unselectable'>Preparing to unpack .../06-python3-zope.event_4.4-3_all.deb ...
Unpacking python3-zope.event (4.4-3) ...
Selecting previously unselected package python3-zope.component.
Preparing to unpack .../07-python3-zope.component_4.3.0-3_all.deb ...
Unpacking python3-zope.component (4.3.0-3) ...
Selecting previously unselected package python3-certbot.
Preparing to unpack .../08-python3-certbot_1.21.0-1build1_all.deb ...
Unpacking python3-certbot (1.21.0-1build1) ...
Selecting previously unselected package python3-icu.
Preparing to unpack .../09-python3-icu_2.8.1-0ubuntu2_amd64.deb ...
Unpacking python3-icu (2.8.1-0ubuntu2) ...
Selecting previously unselected package certbot.
Preparing to unpack .../10-certbot_1.21.0-1build1_all.deb ...
Unpacking certbot (1.21.0-1build1) ...
Setting up python3-configargparse (1.5.3-1) ...
Setting up python3-requests-toolbelt (0.9.1-1) ...
Setting up python3-parsedatetime (2.6-2) ...
Setting up python3-icu (2.8.1-0ubuntu2) ...
Setting up python3-zope.event (4.4-3) ...
Setting up python3-zope.hookable (5.1.0-1build1) ...
Setting up python3-josepy (1.10.0-1) ...
Setting up python3-zope.component (4.3.0-3) ...
Setting up python3-acme (1.21.0-1) ...
Setting up python3-certbot (1.21.0-1build1) ...
Setting up certbot (1.21.0-1build1) ...
Created symlink /etc/systemd/system/timers.target.wants/certbot.timer → /lib/systemd/system/certbot.timer.
Processing triggers for man-db (2.10.2-1) ...
Scanning processes...
Scanning processor microcode...
Scanning linux images...
Failed to retrieve available kernel versions.
Failed to check for processor microcode upgrades.
No services need to be restarted.
No containers need to be restarted.
No user sessions are running outdated binaries.
No VM guests are running outdated hypervisor (qemu) binaries on this host. </span></pre>
</div>
<p>
<code>certbot</code> has 2 subcommands of interest: <code>certonly</code> (used when creating a certificate for the first time),
and <code>renew</code> (used when updating a pre-existing certificate).
The creation process requires the user to do things before it can complete, so it can only be run interactively.
</p>
<p class="alert shadow rounded">
The files and directories creating by the process of creating a new SSL certificate should not be deleted.
Contrary to what the Letsencrypt <code>certbot</code> documentation says, I have found that
these files and directories allow the renewal process to proceed without requiring user interaction, so it can be scripted.
</p>
</edit-fold>
<edit-fold help>
<h2 id="help"><span class='code'>Certbot</span> Help Messages</h2>
<p>
Here is the help message for the <code>certbot certonly</code> subcommand:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id64861911c4cb'><button class='copyBtn' data-clipboard-target='#id64861911c4cb' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>certbot certonly --help
<span class='unselectable'>- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
certbot [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...<br/>
Certbot can obtain and install HTTPS/TLS/SSL certificates. By default,
it will attempt to use a webserver both for obtaining and installing the
certificate. The most common SUBCOMMANDS and flags are:<br/>
obtain, install, and renew certificates:
(default) run Obtain & install a certificate in your current webserver
certonly Obtain or renew a certificate, but do not install it
renew Renew all previously obtained certificates that are near
expiry
enhance Add security enhancements to your existing configuration
-d DOMAINS Comma-separated list of domains to obtain a certificate for<br/>
(the certbot apache plugin is not installed)
--standalone Run a standalone webserver for authentication
(the certbot nginx plugin is not installed)
--webroot Place files in a server's webroot folder for authentication
--manual Obtain certificates interactively, or using shell script
hooks<br/>
-n Run non-interactively
--test-cert Obtain a test certificate from a staging server
--dry-run Test "renew" or "certonly" without saving any certificates
to disk<br/>
manage certificates:
certificates Display information about certificates you have from Certbot
revoke Revoke a certificate (supply --cert-name or --cert-path)
delete Delete a certificate (supply --cert-name)<br/>
manage your account:
register Create an ACME account
unregister Deactivate an ACME account
update_account Update an ACME account
--agree-tos Agree to the ACME server's Subscriber Agreement
-m EMAIL Email address for important account notifications<br/>
More detailed help:<br/>
-h, --help [TOPIC] print this message, or detailed help on a topic;
the available TOPICS are:<br/>
all, automation, commands, paths, security, testing, or any of the
subcommands or plugins (certonly, renew, install, register, nginx,
apache, standalone, webroot, etc.)
-h all print a detailed help page including all topics
--version print the version number
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - </span></pre>
</div>
<p>
Here is the help message for the <code>certbot certonly</code> subcommand:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idaa5df8885fa7'><button class='copyBtn' data-clipboard-target='#idaa5df8885fa7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>certbot renew --help
<span class='unselectable'>- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -<br/>
certbot [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...<br/>
Certbot can obtain and install HTTPS/TLS/SSL certificates. By default,
it will attempt to use a webserver both for obtaining and installing the
certificate. The most common SUBCOMMANDS and flags are:<br/>
obtain, install, and renew certificates:
(default) run Obtain & install a certificate in your current webserver
certonly Obtain or renew a certificate, but do not install it
renew Renew all previously obtained certificates that are near
expiry
enhance Add security enhancements to your existing configuration
-d DOMAINS Comma-separated list of domains to obtain a certificate for<br/>
(the certbot apache plugin is not installed)
--standalone Run a standalone webserver for authentication
(the certbot nginx plugin is not installed)
--webroot Place files in a server's webroot folder for authentication
--manual Obtain certificates interactively, or using shell script
hooks<br/>
-n Run non-interactively
--test-cert Obtain a test certificate from a staging server
--dry-run Test "renew" or "certonly" without saving any certificates
to disk<br/>
manage certificates:
certificates Display information about certificates you have from Certbot
revoke Revoke a certificate (supply --cert-name or --cert-path)
delete Delete a certificate (supply --cert-name)<br/>
manage your account:
register Create an ACME account
unregister Deactivate an ACME account
update_account Update an ACME account
show_account Display account details
--agree-tos Agree to the ACME server's Subscriber Agreement
-m EMAIL Email address for important account notifications<br/>
More detailed help:<br/>
-h, --help [TOPIC] print this message, or detailed help on a topic;
the available TOPICS are:<br/>
all, automation, commands, paths, security, testing, or any of the
subcommands or plugins (certonly, renew, install, register, nginx,
apache, standalone, webroot, etc.)
-h all print a detailed help page including all topics
--version print the version number
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - </span></pre>
</div>
<p>
Complete documentation for <code>certbot</code> is <a href='https://eff-certbot.readthedocs.io/en/stable/intro.html' target='_blank' rel='nofollow'>here</a>.
</p>
</edit-fold>
<edit-fold dns>
<h2 id="genericDns">Generic DNS Authentication</h2>
<p>
I used <code>certbot</code> to create a special file within the website as follows, without using <code>sudo</code>.
I want a <a href='https://en.wikipedia.org/wiki/Wildcard_certificate' target='_blank' rel='nofollow'>wildcard SSL certificate</a>, which
<a href='https://certbot.eff.org/faq#does-let-s-encrypt-issue-wildcard-certificates' target='_blank' rel='nofollow'>requires certbot to use DNS authentication</a>.
</p>
<p>
BTW, the following options include two <code>-d</code> options,
one for the domain apex (<code>mslinn.com</code>) and one for the subdomains (<code>*.mslinn.com</code>)
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id04f5919ed3c3'><button class='copyBtn' data-clipboard-target='#id04f5919ed3c3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>certbot certonly \
--manual \
--agree-tos \
--preferred-challenges dns-01 \
--rsa-key-size 4096 \
-d mslinn.com -d *.mslinn.com \
--config-dir ~/.certbot/mslinn.com/config \
--logs-dir ~/.certbot/mslinn.com/logs \
--work-dir ~/.certbot/mslinn.com/work
<span class='unselectable'>Saving debug log to /home/mslinn/.certbot/mslinn.com/logs/letsencrypt.log
Enter email address (used for urgent renewal and security notices)
(Enter 'c' to cancel): </span> mslinn@mslinn.com
<br/><span class='unselectable'>- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing, once your first certificate is successfully issued, to
share your email address with the Electronic Frontier Foundation, a founding
partner of the Let's Encrypt project and the non-profit organization that
develops Certbot? We'd like to send you email about our work encrypting the web,
EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: </span>n
<span class='unselectable'>Account registered.
Requesting a certificate for mslinn.com and *.mslinn.com<br/>
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name:<br/>
_acme-challenge.mslinn.com.<br/>
with the following value:<br/>
n6o3qMw5N7qxAzV4uNQePKxJjaw0f-Bo32CybWr-lhE<br/>
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue
(This must be set up in addition to the previous challenges; do not remove,
replace, or undo the previous challenge tasks yet. Note that you might be
asked to create multiple distinct TXT records with the same name. This is
permitted by DNS standards.)
Before continuing, verify the TXT record has been deployed. Depending on the DNS
provider, this may take some time, from a few seconds to multiple minutes. You can
check if it has finished deploying with aid of online tools, such as the Google
Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.mslinn.com.
Look for one or more bolded line(s) below the line ';ANSWER'. It should show the
value(s) you’ve just added.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue
Successfully received certificate.
Certificate is saved at: /home/mslinn/.certbot/mslinn.com/config/live/mslinn.com/fullchain.pem
Key is saved at: /home/mslinn/.certbot/mslinn.com/config/live/mslinn.com/privkey.pem
This certificate expires on 2022-09-29.
These files will be updated when the certificate renews.
NEXT STEPS:
- This certificate will not be renewed automatically.
Autorenewal of --manual certificates requires the use of an authentication hook script
(--manual-auth-hook) but one was not provided.
To renew this certificate, repeat this same certbot command before the certificate’s expiry date.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
* Donating to ISRG / Let’s Encrypt: https://letsencrypt.org/donate
* Donating to EFF: https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - </span></pre>
</div>
<p>
I made a bash script (<code>~/.local/bin/sslCert</code>) to create SSL certificates, which performs the same steps as the above,
but it works for any site, and can renew certificates as well as create them:
</p>
<div class="codeLabel"><a href='data:text/plain;charset=UTF-8,sslCert' download='sslCert'
title='Click on the file name to download the file'>~/.local/bin/sslCert</a>
</div>
<pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id1e0d291ee497">#!/bin/bash
function help {
echo "Creates or updates an wildcard SSL certificate
Usage: $(basename $0) DOMAIN
Example: $(basename $0) scalacourses.com
The relevant files are stored in ~/.certbot/DOMAIN/
Creating a new certificate requires an interactive user session;
updating a certificate can be done in a cron job.
Currently, the renew verb is capable of either renewing all installed
certificates that are due to be renewed or renewing a single certificate
specified by its name. If you would like to renew specific certificates
by their domains, use the certonly command instead.
The renew verb may provide other options for selecting certificates to
renew in the future.
Ask for help or search for solutions at https://community.letsencrypt.org
See the logfile /home/mslinn/.certbot/ancientwarmth.com/logs/letsencrypt.log
or re-run Certbot with -v for more details.
BTW, you can test the date range of the certificate with this incantation:
curl https://domain.com -vI --stderr - | grep 'date:'
"
exit 1
}
if [ -z "$1" ]; then help; fi
DOMAIN="$1"
if [ -d "$HOME/.certbot/$DOMAIN" ]; then # Renew existing certificate
CMD=certonly # renew
OPTIONS=--force-renew
else # Make new certificate (interactively)
CMD=certonly
unset OPTIONS
fi
certbot "$CMD" $OPTIONS \
--agree-tos \
--config-dir "$HOME/.certbot/$DOMAIN/config" \
-d "$DOMAIN" -d "*.$DOMAIN" \
--email mslinn@mslinn.com \
--logs-dir "$HOME/.certbot/$DOMAIN/logs" \
--manual \
--preferred-challenges dns-01 \
--rsa-key-size 4096 \
--work-dir "$HOME/.certbot/$DOMAIN/work"
</pre>
<p>
Here is the help message for the script:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd8b38f6114cd'><button class='copyBtn' data-clipboard-target='#idd8b38f6114cd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sslCert
<span class='unselectable'>Creates or updates an wildcard SSL certificate
Usage: sslCert DOMAIN
Example: sslCert scalacourses.com
The relevant files are stored in ~/.certbot/DOMAIN/
Creating a new certificate requires an interactive user session;
updating a certificate can be done in a cron job. </span></pre>
</div>
<p>
The following <code>crontab</code> entry causes the script to run every 60 days.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>crontab</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfe82b332f5ee'><button class='copyBtn' data-clipboard-target='#idfe82b332f5ee' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>0 0 1 */2 * .local/bin/sslCert mslinn.com</pre>
</div>
<p>
The next time the script was run, output looked like:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id81dcf0e8baca'><button class='copyBtn' data-clipboard-target='#id81dcf0e8baca' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sslCert mslinn.com
<span class='unselectable'>Saving debug log to /home/mslinn/.certbot/mslinn.com/logs/letsencrypt.log
Renewing an existing certificate for mslinn.com and *.mslinn.com<br/>
Successfully received certificate.
Certificate is saved at: /home/mslinn/.certbot/mslinn.com/config/live/mslinn.com/fullchain.pem
Key is saved at: /home/mslinn/.certbot/mslinn.com/config/live/mslinn.com/privkey.pem
This certificate expires on 2022-12-13.
These files will be updated when the certificate renews.<br/>
NEXT STEPS:
- This certificate will not be renewed automatically.
Autorenewal of --manual certificates requires the use of an authentication hook script (--manual-auth-hook)
but one was not provided. To renew this certificate, repeat this same certbot command before the
certificate's expiry date.<br/>
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
* Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
* Donating to EFF: https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - </span></pre>
</div>
<span style='font-size: 3em; float: right; margin-left: 5px;'>😁</span>
Considering Cloudflare R2 for Static Websites2022-06-09T00:00:00-04:00https://mslinn.github.io/blog/2022/06/09/cloudflare-r2<p>
After deciding to <a href='/blog/2022/05/26/aws-hijacking.html'>close my AWS account</a>,
I started looking for alternatives to host my static websites.
Without getting into my selection criteria, my short list of possible hosting companies was
<a href='https://azure.microsoft.com/' target='_blank' rel='nofollow'>Microsoft Azure</a>,
<a href='https://www.cloudflare.com' target='_blank' rel='nofollow'>Cloudflare</a>,
<a href='https://www.digitalocean.com/' target='_blank' rel='nofollow'>Digital Ocean</a>,
<a href='https://www.linode.com' target='_blank' rel='nofollow'>Linode</a>,
and
<a href='https://www.netlify.com/' target='_blank' rel='nofollow'>Netlify</a>.
Many other options exist.
</p>
<p>
Cloudflare has a free tier that has no time limit.
It includes an S3 work-alike called R2, SSL and a built-in world-wide CDN that works automatically.
250 GB of storage and 1 TB/month of transfer are provided at no charge, forever.
There are also no ingress or egress charges.
<a href='https://www.fool.com/investing/2022/05/02/this-tumbling-cloud-stock-is-still-way-too-expensi/' target='_blank' rel='nofollow'>Cloudflare’s edge network</a>
now spans 275 cities around the world, with nearly all Internet users within 50 milliseconds of a Cloudflare server.
</p>
<p>
This blog post documents my experience with Cloudflare.
If you don't care about details, and want to know my verdict on Cloudflare R2,
<a href='#verdict'>skip to the end</a>.
</p>
<p class="alert rounded shadow">
For PaaS vendors such as AWS, Azure, Digital Ocean, Cloudflare, ScaleWay, etc.:
“pay-as-you-go” is shorthand for “there is nothing you can do to limit your financial liability”.
</p>
<h2 id="history">This is what I did</h2>
<p>
After creating an account,
I followed the directions at <a href='https://developers.cloudflare.com/r2/get-started/' target='_blank' rel='nofollow'>R2 get started guide</a>.
</p>
<h2 id="wrangler">Wrangler</h2>
<p>
The directions told me to set up <a href='https://github.com/cloudflare/wrangler2' target='_blank' rel='nofollow'>Wrangler v2</a>,
a command-line interface for transferring files to R2.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id603172dcc8ab'><button class='copyBtn' data-clipboard-target='#id603172dcc8ab' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>npm install -g wrangler
<span class='unselectable'>npm WARN config global `--global`, `--local` are deprecated. Use `--location=global` instead.
npm WARN deprecated rollup-plugin-inject@3.0.2: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.<br/>
added 56 packages, and audited 57 packages in 8s<br/>
10 high severity vulnerabilities<br/>
To address issues that do not require attention, run:
npm audit fix<br/>
To address all issues, run:
npm audit fix --force<br/>
Run `npm audit` for details. </span></pre>
</div>
<p>
The above messages do not inspire confidence.
Wrangler is written using Node.
I believe that Node has more security issues than any other computer language,
particularly in <a href='https://turingpoint.de/en/blog/node-package-manager-security-everything-about-npm-package-security/' target='_blank' rel='nofollow'>package management</a>.
<a href='https://cheatsheetseries.owasp.org/cheatsheets/NPM_Security_Cheat_Sheet.html' target='_blank' rel='nofollow'>The OWasp recommendations</a>
do not address the <a href='https://news.ycombinator.com/item?id=29245080' target='_blank' rel='nofollow'>fundamental security vulnerabilities</a>
in the Node package management infrastructure.
</p>
<h2 id="rclone">RClone</h2>
<p>
I looked for alternatives to Wrangler and found <a href='https://rclone.org/docs/' target='_blank' rel='nofollow'>RClone</a>.
RClone is a command-line program to manage files on cloud storage.
It has <a href='https://rclone.org/docs/#subcommands' target='_blank' rel='nofollow'>many subcommands</a>,
including two types of sync.
Although the RClone documentation does not mention Cloudflare, the
<a href='https://developers.cloudflare.com/r2/examples/rclone/' target='_blank' rel='nofollow'>Cloudflare docs described how to set up RClone</a>.
</p>
<h2 id="limits">Limits</h2>
<p>
<a href='https://developers.cloudflare.com/workers/platform/limits/' target='_blank' rel='nofollow'>Platform limits</a> are important.
Not only are the technical limits important for defining inputs and outputs,
users should be particularly interested in spend limits, so they are not subject to unlimited financial liability.
</p>
<div class='quote'>
<div class='quoteText clearfix'>
Workers Paid plan is separate from any other Cloudflare plan (Free, Professional, Business) you may have.
If you are an Enterprise customer, reach out to your account team to confirm pricing details.
<br><br>
Only requests that hit a Worker will count against your limits and your bill.
</div><div class='quoteAttribution'> – From <a href='https://developers.cloudflare.com/workers/platform/pricing/#fine-print' rel='nofollow' target='_blank'>Cloudflare Workers Pricing Fine Print</a></div>
</div>
<h2 id="setup">Setup</h2>
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/cloudflare/bucket0.webp" type="image/webp">
<source srcset="/blog/images/cloudflare/bucket0.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/cloudflare/bucket0.png"
style='width: 100%; '
/>
</picture>
</div>
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/cloudflare/analytics0.webp" type="image/webp">
<source srcset="/blog/images/cloudflare/analytics0.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/cloudflare/analytics0.png"
style='width: 100%; '
/>
</picture>
</div>
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/cloudflare/billing0.webp" type="image/webp">
<source srcset="/blog/images/cloudflare/billing0.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/cloudflare/billing0.png"
style='width: 100%; '
/>
</picture>
</div>
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/cloudflare/workers0.webp" type="image/webp">
<source srcset="/blog/images/cloudflare/workers0.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/cloudflare/workers0.png"
style='width: 100%; '
/>
</picture>
</div>
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/cloudflare/workers1.webp" type="image/webp">
<source srcset="/blog/images/cloudflare/workers1.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/cloudflare/workers1.png"
style='width: 100%; '
/>
</picture>
</div>
<h2 id="cache">Caching</h2>
<p>
<a href='https://dash.cloudflare.com/de87d0e0cbc74c70e8feb87e9671cdd0/mslinn.com/caching/configuration' target='_blank' rel='nofollow'>Cache control</a>.
</p>
<h2 id="pages">Pages</h2>
<div class='quote'>
<div class='quoteText clearfix'>>
Cloudflare Pages is a JAMstack platform for frontend developers to collaborate and deploy websites.
</div><div class='quoteAttribution'> – From <a href='https://pages.cloudflare.com' rel='nofollow' target='_blank'>pages.cloudflare.com</a></div>
</div>
<p>
Cloudflare Pages supports <a href='https://developers.cloudflare.com/pages/framework-guides/deploy-a-jekyll-site/' target='_blank' rel='nofollow'>Jekyll sites</a>.
However, it looks like Cloudflare Pages builds the site in the cloud.
While this might be a useful mechanism for many,
my Jekyll builds need to access my local machine,
and use my <a href="/jekyll/3000-jekyll-plugins.html">Jekyll plugins</a>.
</p>
<p>
<a href='https://developers.cloudflare.com/workers/platform/sites/start-from-existing' target='_blank' rel='nofollow'>Cloudflare Workers Sites</a> suits my use case.
The <a href='https://developers.cloudflare.com/workers/platform/sites/start-from-existing' target='_blank' rel='nofollow'>Start From Existing</a> documentation looks appropriate,
except it is written for using Wrangler, which I view as a security threat.
</p>
<h2 id="verdict">The Verdict: No to Cloudflare</h2>
<p class="alert rounded shadow">
CloudFlare does not offer a spend limit for accounts on paid plans.
This is unacceptable.
I tried to remove my credit card, but found I could not.
I then deleted my user account, and saw:
</p>
<p class="warning shadow rounded">
It could take up to 12 months to delete your information completely.
</p>
<p>
There is no way to frame this as an example of how Cloudflare is looking out for the best interests of their customers.
</p>
Upgrading PostgreSQL Ubuntu2022-05-28T00:00:00-04:00https://mslinn.github.io/blog/2022/05/28/psql-upgrade<!-- #region intro -->
<p>
This blog post describes my recommended steps to follow when
upgrading a Postgres database forced upon you when upgrading Ubuntu,
and the new OS includes a new release of Postgres.
</p>
<p>
I use PostgreSQL as the database for <a href='https://ScalaCourses.com' target='_blank' rel='nofollow'><code>ScalaCourses.com</code></a>.
The site is served from an Ubuntu instance, and there is a backup instance.
</p>
<p>
When upgrading to Ubuntu 22.04, PostgreSQL upgraded from v13 to v14;
I used <code>pg_upgrade</code>, which required temporarily swapping the ports that Postgres used.
</p>
<p>
However, I used <code>pg_upgradecluster</code> when upgrading to Ubuntu 23.04;
and PostgreSQL upgraded from v14 to v15.
Unfortunately, the documented procedure to use <code>pg_upgradecluster</code> has no
consideration for <code>systemd</code>,
and this makes the upgrade slightly more awkward than it might otherwise be.
</p>
<p>
While both approaches have issues,
I found that it is easier and faster to upgrade a Postgres installation
using <code>pg_upgradecluster</code> than using <code>pg_upgrade</code>.
</p>
<!-- endregion -->
<!-- #region Upgrade Backup System First -->
<h2 id="upgradeubuntu">Upgrade Backup System First</h2>
<p>
One of the benefits of a backup system is that you can work through upgrade problems on the backup,
before doing the same with the live instance.
</p>
<p>
<a href='https://gorails.com/guides/upgrading-postgresql-version-on-ubuntu-server' target='_blank' rel='nofollow'>This article</a>
suggests to upgrade the entire system to Ubuntu 23.04,
then upgrade the active Postgres database cluster.
The alternative would be to add a new PPA for Postgres,
and just update that program before upgrading the entire system;
this merely substitutes one set of potential issues for another.
The article also demonstrates renaming the extra database cluster that the Postgres upgrade creates,
but that just creates cruft since it is not required for anything.
</p>
<p>
I decided to delete the new database cluster instead,
since upgrading the old database cluster is all that is required.
As you will read in the <a href='#ug'>Upgrade Procedure</a> section,
this is also the advice given during the upgrade procedure.
</p>
<p>
I decided to first
<a href='https://help.ubuntu.com/community/LunarUpgrades' target='_blank' rel='nofollow'>upgrade the Ubuntu OS</a>,
then migrate the Postgres database cluster.
</p>
<!-- endregion -->
<!-- #region -->
<h2 id="nvidia">Upgrade Removed NVidia Support</h2>
<p>
After running <code>do-release upgrade</code> on the production system,
the system worked fine, except the console had no graphics.
I fixed it by automatically installing all video drivers:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf27d3033feb3'><button class='copyBtn' data-clipboard-target='#idf27d3033feb3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo ubuntu-drivers autoinstall</pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- endregion -->
<!-- #region pg_lsclusters -->
<h2 id="pg_lsclusters">Examining the Postgres Databases</h2>
<p>
<code>pg_lsclusters</code> displays the available PostgreSQL database clusters.
Prior to upgrading to Postgres 15,
I had never deleted the old versions.
They had quietly accumulated over the 11 years that I had been running ScalaCourses:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfcbe8a79ca31'><button class='copyBtn' data-clipboard-target='#idfcbe8a79ca31' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pg_lsclusters
<span class='unselectable'>Ver Cluster Port Status Owner Data directory Log file
9.5 main 5431 online postgres /var/lib/postgresql/9.5/main /var/log/postgresql/postgresql-9.5-main.log
9.6 main 5433 online postgres /var/lib/postgresql/9.6/main /var/log/postgresql/postgresql-9.6-main.log
10 main 5434 online postgres /var/lib/postgresql/10/main /var/log/postgresql/postgresql-10-main.log
12 main 5432 online postgres /var/lib/postgresql/12/main /var/log/postgresql/postgresql-12-main.log
13 main 5435 online postgres /var/lib/postgresql/13/main /var/log/postgresql/postgresql-13-main.log
14 main 5436 online postgres /var/lib/postgresql/14/main /var/log/postgresql/postgresql-14-main.log </span></pre>
</div>
<p>
Listing the databases in the active cluster showed an unnecessary database:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id27c2f19180cd'><button class='copyBtn' data-clipboard-target='#id27c2f19180cd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo -iu postgres psql -l
<span class='unselectable'>List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
--------------+----------+----------+-------------+-------------+-----------------------
bix_dev | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 |
postgres | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 |
scalacourses | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 |
template0 | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | =c/postgres +
| | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | =c/postgres +
| | | | | postgres=CTc/postgres
(5 rows) </span></pre>
</div>
<p>
I do not care about the <code>bix_dev</code> database, so I decided to delete it.
The database that I care about is called <code>scalacourses</code>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id11410982ff58'><button class='copyBtn' data-clipboard-target='#id11410982ff58' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo -iu postgres psql -c 'drop database bix_dev;'
<span class='unselectable'>psql (14.7 (Ubuntu 14.7-0ubuntu0.22.10.1))
Type "help" for help.
DROP DATABASE </span></pre>
</div>
<!-- endregion -->
<!-- #region Upgrade Procedure -->
<h2 id="ug">Upgrade Procedure</h2>
<p>
Ubuntu is normally upgraded by running <code>do-release-upgrade</code>.
Near the end of that process, the following message appeared.
</p>
<p>
At this point <code>postgresql-15</code> and <code>postgresql-client-15</code>
had just been installed.
The message did not reflect the true state of the system,
which caused me some consternation for a moment.
</p>
<p class="quote shadow rounded">
The PostgreSQL version 14 is obsolete,
but the server or client packages are still installed.
Please install the latest packages
(<code>postgresql-15</code> and <code>postgresql-client-15</code>)
and upgrade the existing clusters with pg_upgradecluster
(<a href='https://manpages.ubuntu.com/manpages/trusty/man8/pg_upgradecluster.8.html' target='_blank' rel='nofollow'>see manpage</a>).
<br><br>
Please be aware that the installation of postgresql-15 will automatically create a default cluster 15/main.
If you want to upgrade the 14/main cluster, you need to remove the already existing 15 cluster
(<code>pg_dropcluster --stop 15 main</code>, see manpage
for details).
<br><br>
The old server and client packages are no longer supported.
After the existing clusters are upgraded, the <code>postgresql-14</code> and
<code>postgresql-client-14</code> packages should be removed.
<br><br>
Please see /usr/share/doc/postgresql-common/README.Debian.gz for details.
</p>
<p>
Following is <code>/usr/share/doc/postgresql-common/README.Debian.gz</code>,
which was referenced by the above message.
Most of that file is uninteresting, however two sections are important, and we will look at them.
</p>
<!-- #region README.Debian.gz -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>/usr/share/doc/postgresql-common/README.Debian.gz</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id32d16826f2cf'>PostgreSQL for Debian
=====================<br/>
PostgreSQL is a fully featured object-relational database management system. It
supports a large part of the SQL standard and is designed to be extensible by
users in many aspects. Its features include ACID transactions, foreign keys,
views, sequences, subqueries, triggers, outer joins, multiversion concurrency
control, and user-defined types and functions.<br/>
Since the on-disk data format of all major PostgreSQL versions (like 9.6,
11, etc.) is incompatible to each other, Debian's PostgreSQL packaging
architecture is designed to maintain clusters of different major versions in
parallel.<br/>
This postgresql-common package provides the common infrastructure and all
frontend programs that users and administrators use. The version specific
server and client programs are shipped in postgresql-*-<version> packages.<br/>
For a detailed description of the architecture, please see<br/>
/usr/share/doc/postgresql-common/README.md.gz<br/>
First steps for the impatient
-----------------------------
Eventually you will not get around reading at least some parts of the manual,
but if you want to get straight into playing SQL, here are the steps to create
a database user and a database for the Unix user 'joe':<br/>
1. Install a database server with the major version of your choice
('postgresql-XY', e. g. 'postgresql-11'). Preferably the latest
version, which you can get by installing the metapackage
'postgresql'. This will automatically create a default cluster
'main' with the database superuser 'postgres'.<br/>
2. Get a shell for the database superuser 'postgres'. If your system
has an active root user, use su:<br/>
# su -s /bin/bash postgres<br/>
If your system uses sudo to get administrative rights, use sudo instead:<br/>
joe$ sudo -u postgres bash<br/>
3. In this postgres shell, create a database user with the same name as your
Unix login:<br/>
$ createuser -DRS joe<br/>
For details about the options, see createuser(1).<br/>
4. Create a database "joework" which is owned by "joe":<br/>
$ createdb -O joe joework<br/>
For details about the options, see createdb(1).<br/>
5. Exit the postgres shell.<br/>
6. As user joe, you should now be able to connect to your database with<br/>
$ psql joework<br/>
Cluster management
------------------
For managing clusters, the following commands are provided (each with its own
manual page):<br/>
pg_createcluster - Create a new cluster or integrate an existing one into
the postgresql-common architecture.
pg_dropcluster - Completely remove a cluster.
pg_ctlcluster - Control the server process of a cluster (start, stop,
restart).
pg_lsclusters - Show a list of all existing clusters and their status.
pg_upgradecluster - Migrate a cluster from one major version to another one.
pg_renamecluster - Rename a cluster.<br/>
Please note that you can of course also use the upstream tools for
creating clusters, such as initdb(1). However, please note that in
this case you cannot expect *any* of above pg_* tools to work, since
they use different configuration settings (SSL, data directories,
etc.) and file locations (e. g.
/etc/postgresql/11/main/postgresql.conf). If in doubt, then do *not*
use initdb, but only pg_createcluster. Since merely installing
postgresql-NN will already set up a default cluster which is ready to
work, most people do not need to bother about initdb or
pg_createcluster at all.<br/>
Port assignment
---------------
Please note that the pg_* tools automatically manage the server ports
unless you specify them manually. The first cluster which is ever
created (by any major version) will run on the default port 5432, and
each new cluster will use the next higher free one.<br/>
E. g. if you first install "postgresql-11" on a clean system, the
default 11/main cluster will run on port 5432. If you then create
another 11 cluster, or install the "postgresql-12" package, that new
one will run on 5433.<br/>
Please use "pg_lsclusters" for displaying the cluster <-> port
mapping, and please have a look at the pg_createcluster manpage (the
--port option) for details.<br/>
Default clusters and upgrading
------------------------------
When installing a postgresql-NN package from scratch, a default
cluster 'main' will automatically be created. This operation is
equivalent to doing 'pg_createcluster NN main --start'.<br/>
Due to this default cluster, an immediate attempt to upgrade an
earlier 'main' cluster to a new version will fail and you need to
remove the newer default cluster first. E. g., if you have
postgresql-9.6 installed and want to upgrade to 11, you first install
postgresql-11:<br/>
apt-get install postgresql-11<br/>
Then drop the default 11 cluster that was just created:<br/>
pg_dropcluster 11 main --stop<br/>
And then upgrade the 9.6 cluster to the latest installed version (e. g. 11):<br/>
pg_upgradecluster 9.6 main<br/>
SSL
---
The PostgreSQL server packages support SSL, which provides encrypted and
authenticated network communication. SSL should be used if you have an
untrusted network between a database server and a client and these exchange
security sensitive data like passwords or confidential database contents.<br/>
When a cluster is created with pg_createcluster, SSL support will automatically
be enabled. postgresql-common makes use of the 'snakeoil' SSL certificate that
is generated by the ssl-cert package, so that SSL works out of the box
(ssl_cert_file, ssl_key_file). In addition, if /etc/postgresql-common/root.crt
exists, it will be used as CA certificate file (ssl_ca_file).<br/>
/etc/postgresql-common/root.crt is a dummy file by default, so that
client-side authentication is not performed. To enable it, you should
add some root certificates to it. A reasonable choice is to just
symlink the file to /etc/ssl/certs/ssl-cert-snakeoil.pem; in this
case, client certificates need to be signed by the snakeoil
certificate, which might be desirable in many cases. See<br/>
/usr/share/doc/postgresql-doc-11/html/ssl-tcp.html<br/>
for details (in package postgresql-doc).<br/>
Further documentation
---------------------
All commands shipped by postgresql-common have detailed manpages. See
postgresql-common(7) for the documentation of the database client program
wrapping, and user_clusters(5) and postgresqlrc(5) for the cluster
configuration.<br/>
The documentation of the database server and client functions, SQL commands,
modules, etc. documented is shipped in the per-version packages
postgresql-doc-<version>.</pre>
</div>
<!-- endregion -->
<p>
The <b>Cluster management</b> section in the above file provides some background information for our purposes,
so I repeat it here:
</p>
<!-- #region Cluster management -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Cluster management</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf0f8f0c0e257'><button class='copyBtn' data-clipboard-target='#idf0f8f0c0e257' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>For managing clusters, the following commands are provided (each with
its own manual page):
pg_createcluster - Create a new cluster or integrate an existing one
into the postgresql-common architecture.
pg_dropcluster - Completely remove a cluster.
pg_ctlcluster - Control the server process of a cluster
(start, stop, restart).
pg_lsclusters - Show a list of all existing clusters and their status.
pg_upgradecluster - Migrate a cluster from one major version to another one.
pg_renamecluster - Rename a cluster.
Please note that you can of course also use the upstream tools for
creating clusters, such as initdb(1). However, please note that in
this case you cannot expect *any* of above pg_* tools to work, since
they use different configuration settings (SSL, data directories,
etc.) and file locations (e. g.
/etc/postgresql/11/main/postgresql.conf). If in doubt, then do *not*
use initdb, but only pg_createcluster. Since merely installing
postgresql-NN will already set up a default cluster which is ready to
work, most people do not need to bother about initdb or
pg_createcluster at all.</pre>
</div>
<!-- endregion -->
<p>
The <b>Default clusters and upgrading</b> section is more important,
however it contained errors.
I fixed the errors in the following instructions.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Default clusters and upgrading</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1a1b67477a68'>When installing a postgresql-NN package from scratch, a default
cluster 'main' will automatically be created. This operation is
equivalent to doing 'pg_createcluster NN main --start'.
Due to this default cluster, an immediate attempt to upgrade an
earlier 'main' cluster to a new version will fail and you need to
remove the newer default cluster first. E. g., if you have
postgresql-9.6 installed and want to upgrade to 11, you first install
postgresql-11:
sudo apt install postgresql-11
Then drop the default 11 cluster that was just created:
sudo -iu postgres pg_dropcluster 11 main --stop
And then upgrade the 9.6 cluster to the latest installed version (e. g. 11):
sudo -iu postgres pg_upgradecluster 9.6 main</pre>
</div>
<!-- endregion -->
<p>
<a href='https://freedesktop.org/wiki/Software/systemd/' target='_blank' rel='nofollow'>Systemd</a>
can significantly affect the upgrade process,
and unfortunately it is completely ignored by the above instructions.
I muddled through, as you will see.
</p>
<!-- endregion -->
<!-- #region transcript -->
<h2 id="trans">Transcript</h2>
<p>
The following is a transcript of what I actually did:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1584a97e513e'><button class='copyBtn' data-clipboard-target='#id1584a97e513e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo -iu postgres pg_dropcluster --stop 15 main
<span class='unselectable'>Warning: stopping the cluster using pg_ctlcluster will mark the systemd
unit as failed. Consider using systemctl:
sudo systemctl stop postgresql@15-main
Warning: systemd was not informed about the removed cluster yet. Operations like
"service postgresql start" might fail. To fix, run:
sudo systemctl daemon-reload </span>
<span class='unselectable'>$ </span>sudo -iu postgres pg_upgradecluster 14 main
<span class='unselectable'>Stopping old cluster...
Warning: stopping the cluster using pg_ctlcluster will mark the systemd unit as failed.
Consider using systemctl:
sudo systemctl stop postgresql@14-main
Restarting old cluster with restricted connections...
Notice: extra pg_ctl/postgres options given, bypassing systemctl for start operation
Creating new PostgreSQL cluster 15/main ...
/usr/lib/postgresql/15/bin/initdb -D /var/lib/postgresql/15/main --auth-local peer --auth-host scram-sha-256 --no-instructions --encoding UTF8 --lc-collate en_CA.UTF-8 --lc-ctype en_CA.UTF-8
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.
The database cluster will be initialized with locale "en_CA.UTF-8".
The default text search configuration will be set to "english".
Data page checksums are disabled.
fixing permissions on existing directory /var/lib/postgresql/15/main ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... America/Toronto
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok
Warning: systemd does not know about the new cluster yet. Operations like
"service postgresql start" will not handle it. To fix, run:
sudo systemctl daemon-reload
Copying old configuration files...
Copying old start.conf...
Copying old pg_ctl.conf...
Starting new cluster...
Notice: extra pg_ctl/postgres options given, bypassing systemctl for start operation
Roles, databases, schemas, ACLs...
set_config
------------
(1 row)
set_config
------------
(1 row)
set_config
------------
(1 row)
set_config
------------
(1 row)
Fixing hardcoded library paths for stored procedures...
Upgrading database template1...
Analyzing database template1...
Fixing hardcoded library paths for stored procedures...
Upgrading database postgres...
Analyzing database postgres...
Fixing hardcoded library paths for stored procedures...
Upgrading database scalacourses...
Analyzing database scalacourses...
Stopping target cluster...
Stopping old cluster...
Disabling automatic startup of old cluster...
Starting upgraded cluster on port 5432...
Warning: the cluster will not be running as a systemd service.
Consider using systemctl:
sudo systemctl start postgresql@15-main
Success. Please check that the upgraded cluster works. If it does,
you can remove the old cluster with
pg_dropcluster 14 main
Ver Cluster Port Status Owner Data directory Log file
14 main 5433 down postgres /var/lib/postgresql/14/main /var/log/postgresql/postgresql-14-main.log
Ver Cluster Port Status Owner Data directory Log file
15 main 5432 online postgres /var/lib/postgresql/15/main /var/log/postgresql/postgresql-15-main.log </span></pre>
</div>
<!-- endregion -->
<p>
When I attempted to following the above instructions for systemd,
errors appeared:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id85ec6bcb6338'><button class='copyBtn' data-clipboard-target='#id85ec6bcb6338' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo systemctl daemon-reload
<span class='unselectable'>$ </span>sudo systemctl start postgresql@15-main
<span class='unselectable'>Job for postgresql@15-main.service failed because the service did not take the steps required by its unit configuration.
See "systemctl status postgresql@15-main.service" and "journalctl -xeu postgresql@15-main.service" for details. </span></pre>
</div>
<!-- endregion -->
<p>
Great, an obscure failure message.
<a href='https://askubuntu.com/questions/1311254/postgres-12-on-ubuntu-20-04-replication-checkpoint-has-wrong-magic-539122744-in' target='_blank' rel='nofollow'>Others have fussed with this problem.</a>
I expected that rebooting would be much faster than trying to fix it,
and that did prove to be the case:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3780ae83d576'><button class='copyBtn' data-clipboard-target='#id3780ae83d576' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo reboot</pre>
</div>
<!-- endregion -->
<p>
After rebooting, the upgraded Postgres cluster worked fine:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide72fc8ae1604'><button class='copyBtn' data-clipboard-target='#ide72fc8ae1604' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pg_lsclusters
<span class='unselectable'>Ver Cluster Port Status Owner Data directory Log file
14 main 5433 down postgres /var/lib/postgresql/14/main /var/log/postgresql/postgresql-14-main.log
15 main 5432 online postgres /var/lib/postgresql/15/main /var/log/postgresql/postgresql-15-main.log
</span>
<span class='unselectable'>$ </span>sudo -iu postgres psql -l
<span class='unselectable'>List of databases
Name | Owner | Encoding | Collate | Ctype | ICU Locale | Locale Provider | Access privileges
--------------+--------------+----------+-------------+-------------+------------+-----------------+-----------------------
postgres | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | | libc |
scalacourses | scalacourses | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | | libc |
template0 | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | | libc | =c/postgres +
| | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | en_CA.UTF-8 | en_CA.UTF-8 | | libc | =c/postgres +
| | | | | | | postgres=CTc/postgres
(4 rows) </span></pre>
</div>
<!-- endregion -->
<p>
Here is how I deleted the older database clusters:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb4e7aa0ce825'><button class='copyBtn' data-clipboard-target='#idb4e7aa0ce825' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>for X in 9.5 9.6 10 12 13; do
sudo -iu postgres pg_dropcluster --stop $X main
end</pre>
</div>
<!-- endregion -->
<span style='font-size: 3em; float: right; margin-left: 5px;'>😁</span>
<p>
All done!
</p>
<!-- endregion -->
Montréal International vs. Bill 962022-05-27T00:00:00-04:00https://mslinn.github.io/blog/2022/05/27/mi-bill96<p>
Last night I attended the 25th Annual General Meeting of
<a href='https://www.montrealinternational.com/en/' target='_blank' rel='nofollow'>Montréal International</a>,
an energetic organization dedicated to attracting foreign business to establish a presence in the greater Montréal region.
My key takeaways were:
</p>
<ol>
<li>
I spoke with about 20 Montréal International employees;
all of them were bilingual or trilingual, intelligent, motivated and well-informed.
Their talent and commitment is palpable.
Clearly, the hiring process is successful.
</li>
<li>
The event was marred by long-winded, self-congratulatory speeches by senior management, and former senior managers who were invited to a panel discussion.
This lack of discipline caused the event to significantly run overtime.
</li>
<li>
The proceedings were conducted entirely in French.
For an organization that calls itself “Montréal International”, this is unforgivable.
</li>
<li>
I asked every employee I spoke with if <a href='https://qcgn.ca/bill-96/' target='_blank' rel='nofollow'>Quebec’s new Bill 96</a> impacted their goals.
All of them expressed grave concern,
and said that their mandate to attract foreign business had become extremely difficult as a result.
However, not one mention was made by anyone on stage about Bill 96.
</li>
</ol>
<p>
Montréal International’s mandate is severely impacted by Quebec's Bill 96.
If any organization has an intrinsic mandate to demonstrate public leadership towards repealing or modifying Bill 96
so it respects basic human rights, it would be Montréal International.
Instead, Montréal International’s leadership,
which consists entirely of old white French-speaking alpha males,
is complicit in their silence, and by their actions.
</p>
Limit Your Financial Vulnerability From AWS Account Hijacking2022-05-26T00:00:00-04:00https://mslinn.github.io/blog/2022/05/26/aws-hijacking<p class="alert shadow rounded">
I am a free agent, independent and critical.
<a href='/softwareexpert/index.html'>You can buy my time as a software expert</a>, but not my opinion.
Just because Amazon / AWS hired me in the past to help them against a patent troll does not mean they own me.
Quite the contrary.
</p>
<p>
In the time you take a quick shower, your AWS account can be highjacked and tens of thousands of dollars in fees can be incurred.
EC2 is the service that provides the greatest financial risk.
You can take steps to limit your liability, and this blog post shows you how.
</p>
<h2 id="backlash">Out-of-Control Cloud Costs</h2>
<p>
Out-of-control costs are causing cloud customers to reduce or eliminate their cloud use.
<a href='https://techcrunch.com/2023/03/20/the-cloud-backlash-has-begun-why-big-data-is-pulling-compute-back-on-premises/' target='_blank' rel='nofollow'>The cloud backlash has begun: Why big data is pulling compute back on premises</a>
– Thomas Robinson, published by TechCrunch 2023-03-20.
</p>
<p>
<a href='https://world.hey.com/dhh/why-we-re-leaving-the-cloud-654b47e0' target='_blank' rel='nofollow'>Why we’re leaving the cloud</a>,
by David Heinemeier Hansson, creator of Ruby on Rails and CTO of 37signals.
Mr. Hansson also wrote
<a href='https://world.hey.com/dhh/we-stand-to-save-7m-over-five-years-from-our-cloud-exit-53996caa' target='_blank' rel='nofollow'>We stand to save $7m over five years from our cloud exit</a>.
</p>
<h2 id="nolimits">AWS IAM Users and Roles Have No Budget Limitations</h2>
<div class="pullQuote">
AWS customers are exposed to unlimited financial liability
</div>
<p>
I encountered two main issues when attempting to secure against AWS account hijacking:
</p>
<ol>
<li>
<b>AWS does not provide a mechanism to guard against launching expensive services.</b>
For example, if an IAM user has a role that allows EC2 instances to be launched, then
that user can launch an unlimited number of EC2 instances of any size.
<br><br>
The most expensive Amazon EC2 is currently the
<a href='https://aws.amazon.com/ec2/instance-types/p4/' target='_blank' rel='nofollow'><code>p4de.24xlarge</code></a>,
which costs $40.96 USD per hour on-demand in the US East (N. Virginia) region.
The instance comes with 96 vCPUs, 1152 GiB of RAM and eight 1TB NVMe SSDs,
and has eight NVIDIA A100 Tensor Core GPUs.
<br><br>
Using stolen credentials that allow EC2 instances to be launched,
an attacker using scripts could spin up an armada of <code>p4de.24xlarge</code> instances
and incur eye-popping costs in minutes.
</li>
<li>
AWS Budgets provides a convenient way to shut down pre-designated services when a total budgetary amount is exceeded.
<b>However, the AWS AMI security model is not integrated into real-time cost monitoring.</b>
Thus, there is no way to automatically shut down newly launched service instances that exceed a pre-authorized budgetary amount.
</li>
</ol>
<h2 id="orElse">Shared Security Responsibility</h2>
<p>
The
<a href='https://aws.amazon.com/agreement/' target='_blank' rel='nofollow'>AWS Customer Agreement</a> and
<a href='https://aws.amazon.com/compliance/shared-responsibility-model/' target='_blank' rel='nofollow'>Shared Responsibility Model</a>
describe how AWS shares responsibility for security with users.
If you follow the AWS security recommendations, described next,
you will not be held accountable for extra charges if your AWS account is hijacked.
</p>
<p>
The AWS Security Blog published a nice overview, entitled
<a href='https://aws.amazon.com/blogs/security/getting-started-follow-security-best-practices-as-you-configure-your-aws-resources/' target='_blank' rel='nofollow'>Getting Started: Follow Security Best Practices as You Configure Your AWS Resources</a>.
Some of the same information is also provided in
<a href='https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html' target='_blank' rel='nofollow'>Security best practices in IAM</a>.
</p>
<p class="alert rounded shadow">
Even though the AWS instructions do not prevent bad guys taking over your account,
follow them anyway so you won't be held liable for the costs incurred if your AWS account is hijacked.
</p>
<h2 id="recommendations">AWS Security Recommendations</h2>
<p>
Security needs to have depth.
Any single measure can fail, and given enough scale, all measures will eventually fail.
Layer your security measures so that one failure, no matter how grave, will not be fatal.
</p>
<p>
AWS recommends the following.
I have highlighted what AWS personnel have described to me as being the quickest and easiest path;
this blog post walks through those steps to the extent that I have been able.
</p>
<ol>
<li>
Set up at least two of the following services to monitor cost and usage:
<ol>
<li>
<a href='https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-managing-costs.html' class='bg_yellow>Managing' target='_blank' rel='nofollow'><span your costs with AWS Budgets </span></a>
</li>
<li>
<a href='https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/gs_monitor_estimated_charges_with_cloudwatch.html#gs_creating_billing_alarm' class='bg_yellow>Create' target='_blank' rel='nofollow'><span a billing alarm Using CloudWatch</span></a>.
</li>
<li>
<a href='https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-user-guide.html' target='_blank' rel='nofollow'>CloudTrail User Guide</a>.
</li>
<li>
<a href='https://docs.aws.amazon.com/waf/latest/developerguide/what-is-aws-waf.html' target='_blank' rel='nofollow'>Web Application Firewall (WAF)</a>.
</li>
<li>
<a href='https://docs.aws.amazon.com/awssupport/latest/user/get-started-with-aws-trusted-advisor.html' target='_blank' rel='nofollow'>Trusted Advisor</a>.
<span class="bg_yellow">I found that Trusted Advisor was somewhat easier to use than CloudWatch.</span>
</li>
</ol>
For more information about managing your AWS cost and usage, see the
<a href='https://docs.aws.amazon.com/cost-management/latest/userguide/what-is-costmanagement.html' target='_blank' rel='nofollow'>AWS Cost Management User Guide</a>.
</li>
<li>Set up at least one of the following security best practices:
<ol>
<li>
<a href='https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_enable.html' class='bg_yellow>Using' target='_blank' rel='nofollow'><span multi-factor authentication (MFA) in AWS</span></a>.
</li>
<li>
<a href='https://docs.aws.amazon.com/securityhub/index.html' target='_blank' rel='nofollow'>AWS Security Hub</a>.
</li>
<li>
<a href='https://docs.aws.amazon.com/guardduty/index.html' target='_blank' rel='nofollow'>Amazon GuardDuty</a>.
</li>
</ol>
</ol>
<h2 id="easiestFirst">Do The Easiest Things First</h2>
<p>
I usually prefer to do the easiest things first.
<a href='https://www.lifehack.org/articles/productivity/easy-tasks-difficult-tasks-first-which-one-more-productive.html' target='_blank' rel='nofollow'>Not everyone agrees.</a>
Doing even one highlighted item above provides a significant security improvement, so why wait?
</p>
<div class='quote'>
<div class='quoteText clearfix'>
The best is the enemy of the good.
</div><div class='quoteAttribution'> – Voltaire
</div>
</div>
<div class='quote'>
<div class='quoteText clearfix'>
Better a diamond with a flaw than a pebble without.
</div><div class='quoteAttribution'> – Confucius
</div>
</div>
<div class='quote'>
<div class='quoteText clearfix'>
Striving to better, oft we mar what’s well.
</div><div class='quoteAttribution'> – Shakespeare
</div>
</div>
<h2 id="root">Root Credentials</h2>
<p>
If a bad guy has your root credentials, they can change budget limits.
For greatest security, <a href='https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/security_credentials' target='_blank' rel='nofollow'>delete your root credentials</a>
by selecting <b>Access Keys (access key ID and secret access key)</b> as shown in the image below.
However, if you do so, <a href='https://docs.aws.amazon.com/general/latest/gr/root-vs-iam.html#aws_tasks-that-require-root' target='_blank' rel='nofollow'>many things become impossible</a>.
</p>
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/aws/rootKeys.webp" type="image/webp">
<source srcset="/blog/images/aws/rootKeys.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/aws/rootKeys.png"
style='width: 100%; '
/>
</picture>
</div>
<h3 id="mfa">Enabling MFA</h3>
<div class='imgWrapper imgBlock right' style='width: 10em; '>
<figure>
<a href='https://www.amazon.com/Yubico-YubiKey-NFC-Authentication-USB/dp/B07HBD71HL' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/aws/yubikey.webp" type="image/webp">
<source srcset="/blog/images/aws/yubikey.png" type="image/png">
<img alt='Yubikey NFC'
class="imgImg rounded shadow"
src="/blog/images/aws/yubikey.png"
style='width: 100%; '
title='Yubikey NFC'
/>
</picture>
</a>
<figcaption class='imgFigCaption '>
<a href="https://www.amazon.com/Yubico-YubiKey-NFC-Authentication-USB/dp/B07HBD71HL" target='_blank' >
Yubikey NFC
</a>
</figcaption>
</figure>
</div>
<p>
The easiest highlighted item above is to enable multifactor authentication (MFA).
</p>
<p>
I use a virtual MFA device,
<a href='https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa.html#enable-virt-mfa-for-root' target='_blank' rel='nofollow'>enabled for my root account</a>
Google Authenticator for <a href='https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2' target='_blank' rel='nofollow'>Android</a> and
<a href='https://apps.apple.com/us/app/google-authenticator/id388497605' target='_blank' rel='nofollow'>iOS</a>.
Google Authenticator features a <a href='https://tools.ietf.org/html/rfc6238' target='_blank' rel='nofollow'>RFC 6238</a> standards-based TOTP (time-based one-time password) algorithm.
</p>
<p>
I use a <a href='https://www.amazon.com/Yubico-YubiKey-NFC-Authentication-USB/dp/B07HBD71HL' target='_blank' rel='nofollow'>YubiKey</a>
to provide MFA for the AMI user ID that I use to log into the AWS console for everyday work.
</p>
<p class="pullQuote">
Only use root credentials when absolutely necessary.
</p>
<h2 id="trustedAdvisor">Trusted Advisor</h2>
<p>
After adding MFA to root credentials, I found that using
<a href='https://docs.aws.amazon.com/awssupport/latest/user/get-started-with-aws-trusted-advisor.html' target='_blank' rel='nofollow'>AWS Trusted Advisor</a>
was the next easiest thing to try to make my AWS account secure.
</p>
<p>
Once you visit the <a href='https://console.aws.amazon.com/trustedadvisor/home' target='_blank' rel='nofollow'>Trusted Advisor Console</a>, and agree to enable Trusted Advisor,
<a href='https://docs.aws.amazon.com/awssupport/latest/user/security-trusted-advisor.html' target='_blank' rel='nofollow'>IAM permissions are added to the AWS IAM user</a>
that you are logged in as.
</p>
<p>
Usage seems straightforward; however, I found the information for securing S3 buckets puzzling at first:
</p>
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/aws/trustedAdvisor1.webp" type="image/webp">
<source srcset="/blog/images/aws/trustedAdvisor1.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/aws/trustedAdvisor1.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
The column labeled <b>ACL Allows List</b> means that buckets flagged with <b>Yes</b> have an ACL that allows objects to be listed.
AWS recommends that S3 buckets not allow objects to be listed by the public.
To correct this:
</p>
<ol>
<li>Click on the bucket name.</li>
<li>Click on the <b>Permissions</b> tab in the page that opens next.</li>
<li>Scroll down to <b>Access Control List</b>.</li>
<li>Click on the <kbd>Edit</kbd> button.</li>
<li>Disable the items marked in red, as shown below.</li>
</ol>
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/aws/trustedAdvisor2.webp" type="image/webp">
<source srcset="/blog/images/aws/trustedAdvisor2.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/aws/trustedAdvisor2.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
Referring back to the previous screenshot, the <b>Policy Allows Access</b> column flags all S3 buckets used to serve webpages as insecure.
To serve HTML pages and assets stored in an S3 bucket, the permissions must be set to allow objects to be read by everyone.
Trusted Advisor flags these buckets as insecure, which does not make sense to me.
</p>
<p>
Perhaps there is a better security policy for when webpages are served via CloudFront,
but as is often the case with AWS documentation,
it is difficult to piece together how to optimally configure S3 permissions with CloudFront options.
</p>
<h3 id="evolution">Best Practices Evolve With Technology</h3>
<p>
The AWS documentation is written as if the products described have always existed in their present state.
In the course of researching this article, I discovered that AWS provides revision histories for their services.
For example, the revision history of S3 is documented in the
<a href='https://docs.aws.amazon.com/AmazonS3/latest/userguide/WhatsNew.html' target='_blank' rel='nofollow'>AWS S3 User Guide</a>,
and the CloudFront revision history is documented in the
<a href='https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/WhatsNew.html' target='_blank' rel='nofollow'>AWS CloudFront Developer Guide</a>.
Revision histories are useful for experienced technologists to review periodically,
so they can keep up with best practices as they evolve.
</p>
<p>
I started using AWS S3 to serve websites in 2013, and at that time, CloudFront did not exist yet.
As the two services matured, best practices, which were at first unknown, evolved and interactions between them became more complex.
In particular, CloudFront <code>http</code>/<code>https</code> promotion,
how origins are specified, and S3 permissions,
all interact in ways that were not well documented for several years.
This was a source of security problems and unwanted downtime for me.
</p>
<div class='imgWrapper imgFlex right' style='width: 12em; margin-right: 1em;'>
<picture class='imgPicture'>
<source srcset="/blog/images/aws/roadToHell.webp" type="image/webp">
<source srcset="/blog/images/aws/roadToHell.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/aws/roadToHell.png"
style='width: 100%; '
/>
</picture>
</div>
<p class="alert rounded shadow">
I have 15 buckets that are used to serve web pages.
On my <a href='https://us-east-1.console.aws.amazon.com/support/home?region=us-east-1&skipRegion=true#/' target='_blank' rel='nofollow'>AWS Support Center console</a>,
Trusted Advisor shows that “You have a yellow check affecting 15 of 24 resources”; however, the Trusted Advisor console
displays those items as red triangles with exclamation marks.
<br><br>
This is confusing, and became frustrating when an AWS security support technician suggested I disregard anything that seemed annoying.
For an IT/security person, that advice is the quickest way to hell that I know.
<br><br><br>
</p>
<h3 id="rough">Rough Spots That Need Love</h3>
<p>
While researching this article I found:
</p>
<div class='quote'>
<div class='quoteText clearfix'>
Under <b>Origin Settings</b>, for <b>Origin Domain Name</b>, choose the Amazon S3 bucket that you created previously.
<span class="bg_yellow">For <b>S3 bucket access</b>, select <b>Yes, use OAI (bucket can restrict access to only CloudFront)</b></span>.
For the <b>Origin access identity</b>,
you can choose from the list, or choose <b>Create new OAI</b> (both will work).
For <b>Bucket policy</b>, select <b>Yes, update the bucket policy</b>.
</div><div class='quoteAttribution'> – From <a href='https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/getting-started-cloudfront-overview.html#getting-started-cloudfront-distribution' rel='nofollow' target='_blank'>Use an Amazon CloudFront distribution to serve a static website</a></div>
</div>
<p>
However, I found conflicting information near the top of this article:
</p>
<div class='quote'>
<div class='quoteText clearfix'>
<h2>Important</h2>
<p>
If you use an Amazon S3 bucket configured as a website endpoint, you must set it up with CloudFront as a custom origin.
<span class="bg_yellow">You can’t use the origin access identity feature described in this topic.</span>
However, you can restrict access to content on a custom origin by setting up custom headers and configuring your origin to require them.
For more information, see
<a href='https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-overview.html#forward-custom-headers-restrict-access' target='_blank' rel='nofollow'>Restricting access to files on custom origins</a>.
</p>
</div><div class='quoteAttribution'> – From <a href='https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html' rel='nofollow' target='_blank'>Restricting access to Amazon S3 content by using an origin access identity (OAI)</a></div>
</div>
<p>
As if this is not confusing enough,
<a href='https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DownloadDistS3AndCustomOrigins.html#concept_S3Origin_website' target='_blank' rel='nofollow'>Using various origins with CloudFront distributions</a>
states that in order for Amazon S3 redirects to work,
a non-standard format for the origin must be used:<br><br>
<code>http://<span style="color: red;">bucket-name</span>.s3-website-<span style="color: red;">region</span>.amazonaws.com</code>
<br><br>
As I discussed in a <a href='/jekyll/10500-redirects.html'>previous blog post</a>,
AWS S3 buckets support two types of redirects, but there is no indication of which type might or might not work with the non-standard origin format.
<br><br>
Also, there is no mention of whether <code>https</code> would work or not,
or if these disparate instructions even work together.
As usual, there is lots of uncertainty about how things actually work on AWS,
and for how long before things change.
</p>
<p>
As per standard operating procedure with AWS,
one would have to muck about extensively to determine which of the conflicting statements above is generally prevalent,
and under what circumstances.
I expect that one article is newer than the other, and it likely supercedes the older article somewhat to some degree under as-yet unknown circumstances.
If I get lucky, the Trusted Advisor warnings will go away.
Trusted Advisor should reference whichever article is currently correct.
</p>
<h3 id="2022-05-31">Update 2022-05-31</h3>
<p>
I received the following email from Trusted Advisor.
Unfortunately, the contents of the email were different from what I found on the Trusted Advisor console.
Perhaps this is because a higher-level, and more expensive, service contract would be required to see those messages.
If so, then this is not the way to gain happy customers.
The cost of the higher service level would be 4 times more than my total AWS costs at present,
clearly not something that I would want to do.
</p>
<div class="quote">
Dear AWS customer:<br><br>
AWS Trusted Advisor currently shows alerts for 3 checks (1 red and 2 yellow)
and $0 of potential monthly savings based on your usage.<br><br>
Here is a summary of status changes for this week:<br><br>
The alert severity of these checks has improved:<br><br>
From red to yellow:<br>
Amazon S3 Bucket Permissions<br><br>
From red to green:<br>
VPC<br>
Security Groups - Specific Ports Unrestricted<br><br>
From yellow to green:<br>
VPC Internet Gateways<br><br>
The alert status of 5 checks has not changed.
For details and recommendations, visit AWS Trusted Advisor.<br><br>
Sign in to the Trusted Advisor console to view your check results.<br><br>
Best,<br><br>
AWS Support<br>
Was this report helpful?
<a href='https://www.amazon.com/gp/f.html' target='_blank' rel='nofollow'>Send us your feedback.</a>
</div>
<p>
To add insult to injury, the final "Send us your feedback" just links to a generic form for AWS sales,
where one can <a href='https://writingexplained.org/idiom-dictionary/go-pound-sand' target='_blank' rel='nofollow'>pound sand</a>.
</p>
<p class="alert rounded shadow">
There is abundant evidence to demonstrate that whereas <code>amazon.com</code> is consumer-centric,
AWS marches to a very different drummer.
Perhaps small businesses are not presently viewed by AWS as a significant growth opportunity.
My personal experience is that the current risk/value proposition provided by AWS is no longer attractive for that demographic.
</p>
<h3 id="2022-05-31">Update 2022-06-03</h3>
<p>
The credit card I used to pay for my AWS usage had a very high limit.
In retrospect that was unwise.
</p>
<p>
AWS took weeks to agree to refund the charge on my credit card, and then they said it would take another week for the reversed charges to appear.
This meant that if I only paid for the other charges on that card,
the month-to-month credit card interest on the fraudulent AWS charges would have been problematic.
</p>
<p>
Just before the credit card billing period ended, I called Visa and asked what my options were.
Apparently I am not the first person who had this problem with AWS.
The answer was immediate: report the charges as fraud.
OK, that made sense.
This meant my card was immediately cancelled, the charges were forgiven by Visa, and a new card with a new number would arrive in about a week.
</p>
<h2 id="budgets">Managing Costs With AWS Budgets</h2>
<p>
Short-lived hijacking is the most serious financial threat,
and AWS Budgets was seemingly not designed to guard against that.
Ticking off this item will enable AWS to absolve you of financial responsibility for being hacked,
but you should not expect that it will do much to slow down an attacker.
</p>
<p>
AWS Budgets are easy to set up in a passive manner,
but they are clumsy to work with and not effective for preventing new services from being launched that would exceed the budget.
The cost limiting functionality is not automatic, it must be set up manually.
AWS documentation does not provide detailed how-to instructions for common scenarios.
Tens of thousands of words of documentation must be digested before the full capabilities can be harnessed.
</p>
<p>
Another significant issue is that only three types of actions are supported:
</p>
<ol>
<li>
<b>IAM Policy</b> – I am unclear on how to set this up so the bad guys cannot launch new services,
and I am not certain this is possible.
</li>
<li>
<a href='https://us-east-1.console.aws.amazon.com/organizations/v2/home/policies/service-control-policy' target='_blank' rel='nofollow'><b>Service Control Policy</b></a> –
Hopefully this could prevent new services from being launched that would exceed the budget; however,
my head exploded when I researched this.
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/aws/serviceControlPolicies1.webp" type="image/webp">
<source srcset="/blog/images/aws/serviceControlPolicies1.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/aws/serviceControlPolicies1.png"
style='width: 100%; '
/>
</picture>
</div>
Attempting to use this option to prevent new, expensive services from being launched would seem to introduce lots of complexity.
Perhaps AWS did not consider this use case, or deemed it unimportant.
</li>
<li>
<b>Automate Instances to Stop for EC2 or RDS</b> –
If you run a <a href='https://aws.amazon.com/ec2/instance-types/' target='_blank' rel='nofollow'><code>t3.nano</code></a> instance, for example,
bad guys can only incur a certain amount of financial damage.
Instead of shutting down the EC2 or RDS instances that I want to run,
my main concern is to prevent bad guys from launching expensive new services.
This option is therefore unsuitable for my use case.
</li>
</ol>
<p>
If AWS Budgets provides an easy way of preventing new services to be launched that would exceed the budget,
I could not find it after several hours of reading.
</p>
<h3 id="passive">Passively Monitoring Budget Spend</h3>
<p>
Simply follow <a href='https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-create.html' target='_blank' rel='nofollow'>the directions</a>
and accept the defaults.
Daily budgets do not support enabling forecasted alerts, or daily budget planning, so select <b>Monthly Budgets</b>.
There is no need to set up SNS alerts or AWS Chatbot alerts, email notification works just as well for most users.
</p>
<h3 id="passive">Attaching Actions</h3>
<p>
Following are my notes from my attempt to enable cost limiting.
I was unable to attach an action that prevented expensive new services from being started,
and it is unclear if resolving the problem will, in fact, provide the desired benefit.
Perhaps someone reading these notes will be able to tell me about an approach that might work for a reasonable amount of effort.
</p>
<p class="alert rounded shadow">
Because AWS will absolve you of financial liability if you create a Budget without any actions,
don't stress about the pointless exercise involved in making a Budget that cannot protect you.
Just do it and give AWS more time to figure out a better way of preventing account hijacking.
<br><br>
You can stop reading this article now, unless the details interest you.
</p>
<p class="alert rounded shadow">
For PaaS vendors such as AWS, Azure, Digital Ocean, Cloudflare, ScaleWay, etc:
“pay-as-you-go” is shorthand for “there is nothing you can do to limit your financial liability”.
</p>
<ol>
<li>When you get to the step labeled <b>Attach actions - Optional</b>, choose <b>Add Action</b>.</li>
<li>
You must choose between using an existing IAM role for AWS Budgets to use,
or you can create a new IAM role expressly for this purpose.
IAM roles are free.
I like the idea of a dedicated IAM role, so I clicked on
<a href='https://us-east-1.console.aws.amazon.com/iamv2/home?region=us-east-1#/roles' target='_blank' rel='nofollow'>manually create an IAM role</a>.<br>
<div class='imgWrapper imgFlex inline' style='width: 75%; '>
<picture class='imgPicture'>
<source srcset="/blog/images/aws/budgetIamRole1.webp" type="image/webp">
<source srcset="/blog/images/aws/budgetIamRole1.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/aws/budgetIamRole1.png"
style='width: 100%; margin-left: 1.2em;'
/>
</picture>
</div>
</li>
<li>
A new browser tab opened, and I clicked on the blue <kbd>Create role</kbd> button.
</li>
<li>
Now a confusing page appeared:
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/aws/budgetIamRole2.webp" type="image/webp">
<source srcset="/blog/images/aws/budgetIamRole2.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/aws/budgetIamRole2.png"
style='width: 100%; '
/>
</picture>
</div>
</li>
<li>
I selected the default <b>Trusted entity type</b>, AWS Service,
then from the pulldown menu labeled <b>Use cases for other AWS services:</b> I selected <b>Budgets</b>.
</li>
<li>
A new radio button appeared underneath the pull-down, also labeled <b>Budgets</b>, and I clicked on that.
This does not win any usability awards.
</li>
<li>
I clicked on the button labeled <kbd>Next</kbd>.
</li>
<li>
<div class='quote'>
<div class='quoteText clearfix'>
Managed policy name:
<code><a href='https://docs.aws.amazon.com/cost-management/latest/userguide/billing-permissions-ref.html#budget-managedIAM-SSM' target='_blank' rel='nofollow'><code>AWSBudgetsActionsRolePolicyForResourceAdministrationWithSSM</code></a></code><br><br>
This managed policy is focused on specific actions that AWS Budgets takes on your behalf when completing a specific action.
This policy gives AWS Budgets broad permission to control AWS resources.
For example, starts and stops Amazon EC2 or Amazon RDS instances by running AWS Systems Manager (SSM) scripts.
</div><div class='quoteAttribution'> – From <a href='https://docs.aws.amazon.com/cost-management/latest/userguide/billing-permissions-ref.html#budget-managedIAM-SSM' rel='nofollow' target='_blank'>Using identity-based policies (IAM policies) for AWS Cost Management</a></div>
</div>
On the page, I applied <code>AWSBudgetsActionsRolePolicyForResourceAdministrationWithSSM</code>,
then I clicked on <kbd>Next</kbd>.
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/aws/budgetIamRole3.webp" type="image/webp">
<source srcset="/blog/images/aws/budgetIamRole3.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/aws/budgetIamRole3.png"
style='width: 100%; '
/>
</picture>
</div>
</li>
<li>
On the final page, I named the IAM role <b>Budget</b> and clicked on <kbd>Create role</kbd>.
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/aws/budgetIamRole4.webp" type="image/webp">
<source srcset="/blog/images/aws/budgetIamRole4.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/aws/budgetIamRole4.png"
style='width: 100%; '
/>
</picture>
</div>
</li>
<li>
Back in the <b>Billing Management Console</b>, I refreshed the list of available IAM roles, then selected the new role called <b>Budget</b>.
<div class='imgWrapper imgFlex inline' style='width: 75%; '>
<picture class='imgPicture'>
<source srcset="/blog/images/aws/budgetIamRole5.webp" type="image/webp">
<source srcset="/blog/images/aws/budgetIamRole5.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/aws/budgetIamRole5.png"
style='width: 100%; '
/>
</picture>
</div>
</li>
<li>
I selected <b>Service Control Policy</b> from the pull-down that appeared next,
labeled <b>Which action type should be applied when the budget threshold has been exceeded?</b>.
I have no idea how to proceed, or even if going in this direction might provide the desired results.
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/aws/serviceControlPolicies2.webp" type="image/webp">
<source srcset="/blog/images/aws/serviceControlPolicies2.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/aws/serviceControlPolicies2.png"
style='width: 100%; '
/>
</picture>
</div>
</li>
</ol>
Microsoft Clarity Lets Me Watch You Click and Scroll2022-03-31T00:00:00-04:00https://mslinn.github.io/blog/2022/03/31/clarity<!-- #region intro -->
<p>
I spent a fair bit of time designing the plugins for this Jekyll-powered website for
<a href='https://moz.com/beginners-guide-to-seo' target='_blank' rel='nofollow'>SEO</a>.
These plugins are published as open source; click on the word Jekyll in the sentence above this paragraph to learn more.
You are welcome!
</p>
<p>
I do not use Google Analytics because it slows down page load time dramatically.
I do use <a href='https://search.google.com' target='_blank' rel='nofollow'>Google Search Console</a>, however, and recently started trying out
<a href='https://www.bing.com/webmasters/' target='_blank' rel='nofollow'>Microsoft Bing Webmaster Tools</a>.
</p>
<!-- endregion -->
<!-- #region clarity -->
<h2 id="clarity">Microsoft Clarity and Hotjar</h2>
<p>
The day before April Fool's Day in 2022,
I stumbled across <a href='https://clarity.microsoft.com/' target='_blank' rel='nofollow'>Microsoft Clarity</a>,
a free product that I knew nothing about.
I was curious to know what benefit it might provide.
</p>
<p>
I learned that one of the things it can do is provide videos of actual user sessions as they interact with the website.
Those videos are fantastic.
</p>
<p>
As a writer of a lot of online content,
I have spent hours watching you, dear readers.
Individually yet anonymously.
Watching people read teaches you a lot about what is important to them.
As a writer, knowing your audience allows you to be more relevant.
BTW, I do not know who or where my readers are, just the country they are in.
</p>
<!-- endregion -->
<!-- #region update 1 -->
<h2 id="upd">Update 2023-07-10</h2>
<p class="pullQuoteFull">
I no longer need to watch the videos.<br>
Microsoft Clarity's AI generates summaries.<br>
Incredibly actionable information.
</p>
<p>
Today I noticed the <b>insights</b> tab, visible when viewing an entry for a ‘hit’.
Seems like Microsoft has employed AI to generate the following impressive summary of the user's behavior.
I added punctuation, a light edit and a whimsical pad of notepaper.
</p>
<div class="notepaper">
<p style="text-align: left; ">
Visited URL matches regex: <code>^https://www\.mslinn\.com/softwareexpert/index\.html(\?.*)?$</code><br>
<b>Last 7 days<br><br>
Session insights</b><br>
Some key takeaways from this session are:<br><br>
The user was interested in the software expert witness and computer expert services offered by Mike Slinn, as they visited the homepage from Google and spent about four minutes there.
<br><br>
The user was also curious about the technology expert article series, especially the ones related to Git and version control systems. They visited the article index page multiple times and clicked on several articles, spending about 15 minutes in total on this topic.
<br><br>
The user was most engaged with the article on libgit2, a library that provides low-level access to Git operations. They spent about one and a half minutes on this article and clicked on a link to git-fame, a tool that shows the contribution statistics of a Git repository.
<br><br>
The user frequently resized their browser window, which may indicate that they were using a mobile device or adjusting their screen for better readability.
The user also switched between tabs or applications often, as indicated by the page hidden and page visible events. This may suggest that they were multitasking or comparing information from different sources.
<br><br>
01:47 / 47:31
</p>
</div>
<!-- endregion -->
<!-- #region update 2 -->
<h2 id="update2">Update 2023-07-18</h2>
<p>
Today I stumbled across the
<a href='https://chrome.google.com/webstore/detail/microsoft-clarity-live/cjfdbemmaeeohgibnhdhlakiahifjjcf/related' target='_blank' rel='nofollow'>Clarity Live</a>
plugin for the Google Chrome web browser.
There is no such plugin for Firefox, sadly.
</p>
<div class='quote'>
<div class='quoteText clearfix'>
View instant heatmaps right on your live site and watch recent session recordings for any page you are on with our extension.
<br><br>
<ul>
<li>GDPR & CCPA ready</li>
<li>No sampling</li>
<li>Built on open source</li>
</ul>
</div><div class='quoteAttribution'> – From <a href='https://clarity.microsoft.com/live-extension' rel='nofollow' target='_blank'>Microsoft Clarity</a></div>
</div>
<!-- endregion -->
<!-- #region video -->
<h2 id="video">Check out this video!</h2>
<p>
This is one of the first user sessions that
Microsoft Clarity recorded for this website.
</p>
<style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style><div class='embed-container'> <iframe title="YouTube video player" width="640" height="390" src="//www.youtube.com/embed/N-Tip0Y4GMU" frameborder="0" allowfullscreen></iframe></div>
<p>
As you can see,
Microsoft Clarity lets me watch movies of users clicking and scrolling through my website;
spooky yet very informative.
</p>
<p>
Clarity is open source.
<a href='https://github.com/microsoft/clarity' target='_blank' rel='nofollow'>Here</a> is the GitHub project.
</p>
<p>
I have since learned that <a href='https://www.hotjar.com/' target='_blank' rel='nofollow'>Hotjar</a> is similar to Microsoft Clarity.
</p>
<!-- endregion -->
<!-- #region behavior -->
<h2 id="behavior">Online Behavior Matches Real-World Behavior</h2>
<p>
The user in the above video read a bit about my experience as a software expert witness,
then straightaway downloaded my resume.
They were on the website for just over one minute.
Although they did call me, their online behavior showed a lack of urgency,
and their ‘real-world’ behavior mirrored what I saw in the movie.
</p>
<p>
A week later another party visited my site, and spent 80 minutes carefully reading 3 pages, among others.
Their ‘real-world’ behavior also matched their online behavior,
in that they exhibited a sense of urgency towards engaging a software expert.
</p>
<!-- endregion -->
<!-- #region downloads -->
<h2 id="downloads">Tracking Downloads And Other Behavior</h2>
<p>
Microsoft Clarity does not consider a download as a click.
It does not even notice downloads.
For me, downloads are what I care about most.
If you never download my resume, you probably are not a candidate for hiring me as a
<a href='/softwareexpert/index.html'>software expert</a>.
I want to track downloads, not clicks.
Clicks are nice, but there is a direct relationship between resume downloads and signing contracts with legal firms who represent new clients.
</p>
<p>
AWS <a href='https://aws.amazon.com/premiumsupport/knowledge-center/view-iam-history/' target='_blank' rel='nofollow'>CloudTrail</a> and
<a href='https://aws.amazon.com/aws-cost-management/aws-cost-optimization/monitor-track-and-analyze/' target='_blank' rel='nofollow'>CloudWatch</a>
can provide download details and much more.
For example, user IP addresses and geographic location can be captured when a monitored event occurs.
</p>
<!-- endregion -->
<!-- #region surveil -->
<h2 id="others">Many Websites Perform Surveillance</h2>
<p>
Wired Magazine published an article on a similar type of surveillance (keyloggers) on May 11, 2022.
I paraphrased two sentences from that article:
</p>
<div class='quote'>
<ul>
<li>1.8% of websites studied gathered an EU user's email address without their consent, and a staggering 2.95% logged a US user's email.</li>
<li>For US users, 8.4% of sites may have been leaking data to Meta, Facebook’s parent company, and 7.4% of sites may be impacted for EU users.</li>
</ul>
<span class='quoteAttribution'> – From <a href='https://www.wired.com/story/leaky-forms-keyloggers-meta-tiktok-pixel-study/' rel='nofollow' target='_blank'>Thousands of Popular Websites See What You Type—Before You Hit Submit</a></span>
</div>
<p>
Yours truly does nothing of the sort.
</p>
<!-- endregion -->
<!-- #region next -->
<h2 id="next">Looking Ahead</h2>
<p>
I would very much like to receive a continuous data feed of the above AI-generated per-visit summary,
in real time as it becomes available.
An email stream would be a quick way to start.
A streaming api for data would be a logical next step.
</p>
<!-- endregion -->
Make a Visual Studio Code Extension2022-03-01T00:00:00-05:00https://mslinn.github.io/blog/2022/03/01/make-vscode-extension<p>
I want to be able to move pages in my Jekyll-powered website without breaking links from the outside.
The <a href='https://github.com/jekyll/jekyll-redirect-from' target='_blank' rel='nofollow'><code>jekyll-redirect-from</code></a> Jekyll plugin
generates little HTML files that contain <code>http-meta-refresh</code> client-side redirects as desired.
I wanted an easy way to inject the names of those redirect pages into the Jekyll front matter.
Since I usually author my website using Visual Studio Code,
a custom Visual Studio Code extension seems like a good way to create the redirects.
</p>
<h2 id="requirements">Extension Requirements</h2>
<p>
All the extension has to do is discover the URL path component of the page currently being edited, and write an entry into the front matter.
For example, I have set up Jekyll such that given a page at <code>collections/_posts/2022/2022-02-21-jekyll-debugging.html</code>,
it would deploy to <code>/blog/2022/02/21/jekyll-debugging.html</code>.
The required front matter would be:
</p>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id304752498de5'><button class='copyBtn' data-clipboard-target='#id304752498de5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>redirect_from:
- /blog/2022/02/21/jekyll-debugging.html</pre>
</div>
<p>
With that in mind, if I want to move a published post to another location without breaking it,
the extension should:
</p>
<ol>
<li>Present a new menu option when a right-click on a file in the sidebar, or a right-click on a file name tab in the editor.</li>
<li>
When the menu item is selected, obtain the relative path to the file within the project directory
(for example: <code>collections/_posts/2022/2022-02-21-jekyll-debugging.html</code>)
</li>
<li>
Convert the relative path to the deployed relative path (for example, <code>/blog/2022/02/21/jekyll-debugging.html</code>)
</li>
<li>
Write the entry into the front matter of the file. For example:
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8fda86e34843'><button class='copyBtn' data-clipboard-target='#id8fda86e34843' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>redirect_from:<br> - /blog/2022/02/21/jekyll-debugging.html</pre>
</div>
</li>
</ol>
<h2 id="background">Background</h2>
<p>
The <a href='https://code.visualstudio.com/api/get-started/your-first-extension' target='_blank' rel='nofollow'>Microsoft documentation</a>
describes how to write a Visual Studio Code extension.
</p>
<h2 id="setup">Setup</h2>
<p>
Visual Studio Code extensions are written in JavaScript (actually, node.js) or TypeScript.
Ensure that a version of node.js has been installed:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idec33c9914e82'><button class='copyBtn' data-clipboard-target='#idec33c9914e82' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>which node
<span class='unselectable'>/home/mslinn/.nvm/versions/node/v17.3.1/bin/node </span></pre>
</div>
<p>
Ensure you have <a href='properly'>set up your global package manager</a> for node.js, then type:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ideecf2f766534'><button class='copyBtn' data-clipboard-target='#ideecf2f766534' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>npm install -g <a href='https://github.com/yeoman/yo/' target='_blank' rel='nofollow'>yo</a> <a href='https://github.com/microsoft/vscode-generator-code' target='_blank' rel='nofollow'>generator-code</a>
<span class='unselectable'> npm WARN deprecated har-validator@5.1.5: this library is no longer supported
npm WARN deprecated uuid@3.4.0: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.
npm WARN deprecated request@2.88.2: request has been deprecated, see https://github.com/request/request/issues/3142<br>
added 872 packages, and audited 873 packages in 57s<br>
15 vulnerabilities (13 moderate, 2 high)<br>
To address issues that do not require attention, run:
npm audit fix<br>
To address all issues (including breaking changes), run:
npm audit fix --force<br>
Run `npm audit` for details. </span></pre>
</div>
<p>
The <code>yo</code> module is the culprit; lets address its vulnerabilities:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3d441e1b3ed4'><button class='copyBtn' data-clipboard-target='#id3d441e1b3ed4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>npm install -g npm-check-updates
<span class='unselectable'>added 270 packages, and audited 271 packages in 9s<br>
found 0 vulnerabilities </span></pre>
</div>
<h2 id="generate">Generating the Skeleton</h2>
<p>
I decided to use <a href='https://www.typescriptlang.org/' target='_blank' rel='nofollow'>TypeScript</a> instead of JavaScript for the extension.
TypeScript code converts to JavaScript, however it uses type inference and integrates better with some editors.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id75bccb381275'><button class='copyBtn' data-clipboard-target='#id75bccb381275' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>mkdir redirect_generator<br>
<span class='unselectable'>$ </span>cd redirect_generator/<br>
<span class='unselectable'>$ </span>$ yo code
<span class='unselectable'>? ==========================================================================
We’re constantly looking for ways to make yo better!
May we anonymously report usage statistics to improve the tool over time?
More info: https://github.com/yeoman/insight & http://yeoman.io
========================================================================== </span> No
<span class='unselectable'><br>
_-----_ ╭──────────────────────────╮
| | │ Welcome to the Visual │
|--(o)--| │ Studio Code Extension │
`---------´ │ generator! │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `
? What type of extension do you want to create? </span> New Extension (TypeScript)
<span class='unselectable'>? What's the name of your extension? </span> redirect_generator
<span class='unselectable'>? What's the identifier of your extension? </span> redirect-generator
? What's the description of your extension? Injects the URL of a redirect page into Jekyll front matter
<span class='unselectable'>? Initialize a git repository? </span>Yes
<span class='unselectable'>? Bundle the source code with webpack? </span>No
<span class='unselectable'>? Which package manager to use? </span>npm
<span class='unselectable'>Writing in /mnt/f/work/jekyll/redirect_generator/redirect-generator...
create redirect-generator/.vscode/extensions.json
create redirect-generator/.vscode/launch.json
create redirect-generator/.vscode/settings.json
create redirect-generator/.vscode/tasks.json
create redirect-generator/package.json
create redirect-generator/tsconfig.json
create redirect-generator/.vscodeignore
create redirect-generator/vsc-extension-quickstart.md
create redirect-generator/README.md
create redirect-generator/CHANGELOG.md
create redirect-generator/src/extension.ts
create redirect-generator/src/test/runTest.ts
create redirect-generator/src/test/suite/extension.test.ts
create redirect-generator/src/test/suite/index.ts
create redirect-generator/.eslintrc.json<br>
Changes to package.json were detected.<br>
Running npm install for you to install the required dependencies.<br>
added 203 packages, and audited 204 packages in 29s<br>
found 0 vulnerabilities<br>
Your extension redirect-generator has been created!<br>
To start editing with Visual Studio Code, use the following commands:<br>
code redirect-generator<br>
Open vsc-extension-quickstart.md inside the new extension for further instructions
on how to modify, test and publish your extension.<br>
For more information, also visit http://code.visualstudio.com and follow us @code.<br><br>
? Do you want to open the new folder with Visual Studio Code? Open with `code`<br>
_-----_ ╭───────────────────────╮
| | │ Bye from us! │
|--(o)--| │ Chat soon. │
`---------´ │ Yeoman team │
( _´U`_ ) │ http://yeoman.io │
/___A___\ /╰───────────────────────╯
| ~ |
__'.___.'__
´ ` |° ´ Y ` </span></pre>
</div>
<p>
The instructions in the web page assume the
<a href='https://dictionary.cambridge.org/dictionary/english/happy-path' target='_blank' rel='nofollow'>happy path</a> is the only possibility.
Instead:
</p>
<ol>
<li>Do not work on the extension in a workspace that has any other projects in it.</li>
<li>The online instructions assume that TypeScript was used, not JavaScript, so the file types are assumed to be <code>.ts</code>.
</ol>
<p>
The instructions in <code>vsc-extension-quickstart.md</code> do not make assumptions about TypeScript.
</p>
<h2 id="debug">Debugging the Extension</h2>
<p>
I found the procedure awkward at first:
</p>
<ol>
<li>Press <kbd>F5</kbd> to activate the first defined launcher</li>
<li>
Once the new debug VSCode instance settles down, click in it and type
<kbd>Ctrl</kbd>-<kbd>Shift</kbd>-<kbd>P</kbd>, then type <code>redirect</code>.
</li>
<li>Any breakpoints in the original VSCode instance will trigger.</li>
<li>
Once the breakpoint is cleared, a message saying <b>Add redirecct from redirect_generator!</b>
will appear in the debugged instance of VSCode.
</li>
</ol>
<h2 id="anatomy">Extension Anatomy</h2>
<p>
I moved on to the next part of the tutorial,
<a href='https://code.visualstudio.com/api/get-started/extension-anatomy' target='_blank' rel='nofollow'>Extension Anatomy</a>.
</p>
Node.js, NVM, NPM and Yarn2022-03-01T00:00:00-05:00https://mslinn.github.io/blog/2022/03/01/node-package-managers<!-- #region -->
<p>
<a href='https://nodejs.dev/learn/a-brief-history-of-nodejs' target='_blank' rel='nofollow'><code>Node.js</code></a>,
a JavaScript runtime, is not my favorite programming environment.
Originally released 14 years ago by
<a href='https://en.wikipedia.org/wiki/Ryan_Dahl' target='_blank' rel='nofollow'>Ryan Dahl</a>,
a software engineer at Google,
it’s <a href='https://www.shiftleft.io/blog/node.js-vulnerability-cheatsheet/' target='_blank' rel='nofollow'>historical disregard</a> for
<a href='https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html' target='_blank' rel='nofollow'>security</a>
and stability is problematic.
</p>
<p>
However, <code>node.js</code> has significant traction,
especially amongst Millennial software technologists,
and is used by many Ruby and Scala projects for HTML
<a href='http://graphics.cs.cmu.edu/courses/15-466-f19/notes/asset-pipelines.html' target='_blank' rel='nofollow'>asset pipelines</a>.
</p>
<p>
I put together these notes for installing and maintaining <code>node.js</code> on Ubuntu/WSL using a virtualized version manager.
</p>
<h2 id="nvm">Why Virtualize <span class="code">Node.js</span>?</h2>
<div class='imgWrapper imgFlex right' style='width: 40%; '>
<picture class='imgPicture'>
<source srcset="/blog/images/node/pushback.webp" type="image/webp">
<source srcset="/blog/images/node/pushback.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/node/pushback.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
It is better to use virtualized user- and project-specific <code>node.js</code> instances,
instead of working with a system-wide installation of <code>node.js</code>.
This allows you to install and upgrade <code>node.js</code> packages without using supervisor privileges.
Also, virtualized instances allows you to work on many different independent <code>node.js</code> projects
at the same time, without package version collisions.
</p>
<p>
<code>Docker</code> is over-sold.
It adds unnecessary complexity to software projects.
Instead of virtualizing the entire software environment,
as <code>docker</code> attempts to do,
virtualizing the programming environment with <code>node.js nvm</code>,
<a href='/blog/2021/04/09/python-venvs.html'>Python <code>venv</code></a>,
or <a href='/ruby/1000-ruby-setup.html'>Ruby <code>rbenv</code></a>
are much easier and more productive approaches.
</p>
<p>
I think <code>docker</code> has been pushed hard in the media because it is a gateway technology to
<a href='https://www.infoworld.com/article/3223434/what-is-paas-platform-as-a-service-a-simpler-way-to-build-software-applications.html' target='_blank' rel='nofollow'>PaSS</a>.
This is a trend that PaSS vendors like AWS and Azure want to encourage, but
<a href='https://www.datacenterdynamics.com/en/news/37signals-spent-more-than-3-million-on-the-cloud-in-2022-for-basecamp-and-hey' target='_blank' rel='nofollow'>customers are pushing back</a>.
</p>
<!-- endregion -->
<!-- #region -->
<h2 id="nvm" class="clear"><span class="code">Nvm</span></h2>
<p>
<code>Nvm</code>, the Node Version Manager, makes it easy to install multiple virtualized
<code>node.js</code> instances,
and to easily switch between them.
<code>Nvm</code> retains a unique set of installed packages for each <code>node.js</code> instance.
The <code>node.js</code> version in each instance is distinct.
</p>
<p>
<code>Nvm</code> is installed and updated as follows:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfe1136150bb4'><button class='copyBtn' data-clipboard-target='#idfe1136150bb4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash
<span class='unselectable'> % Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 14926 100 14926 0 0 84806 0 --:--:-- --:--:-- --:--:-- 85291
=> nvm is already installed in /home/mslinn/.nvm, trying to update using git
=> => Compressing and cleaning up git repository
=> nvm source string already in /home/mslinn/.bashrc
=> bash_completion source string already in /home/mslinn/.bashrc
=> Close and reopen your terminal to start using nvm or run the following to use it now:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion </span></pre>
</div>
<p>
View the currently installed versions of <code>node.js</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida1c739f179c7'><button class='copyBtn' data-clipboard-target='#ida1c739f179c7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>nvm list
<span class='unselectable'>-> system
iojs -> N/A (default)
node -> stable (-> N/A) (default)
unstable -> N/A (default) </span></pre>
</div>
<!-- #region -->
<p>
View the very long list of available versions of <code>node.js</code> like this:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id00f834a0d3d5'><button class='copyBtn' data-clipboard-target='#id00f834a0d3d5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>nvm list-remote
<span class='unselectable'> v17.7.0
v17.7.1
v17.7.2
v17.8.0
v17.9.0
v17.9.1
v18.0.0
v18.1.0
v18.2.0
v18.3.0
v18.4.0
v18.5.0
v18.6.0
v18.7.0
v18.8.0
v18.9.0
v18.9.1
v18.10.0
v18.11.0
v18.12.0 (LTS: Hydrogen)
v18.12.1 (LTS: Hydrogen)
v18.13.0 (Latest LTS: Hydrogen)
v19.0.0
v19.0.1
v19.1.0
v19.2.0
v19.3.0
v19.4.0
v19.5.0 </span></pre>
</div>
<!-- endregion -->
<h2 id="npm_install_node">Using <span class="code">Nvm</span> to Install <span class="code">Node.js</span></h2>
<p>
Install the latest release of <code>node.js</code> using <code>nvm</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id946b952e14a0'><button class='copyBtn' data-clipboard-target='#id946b952e14a0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>nvm install <span class="bg_yellow">node</span>
<span class='unselectable'>Downloading and installing node v19.5.0...
Downloading https://nodejs.org/dist/v19.5.0/node-v19.5.0-linux-x64.tar.xz...
##################################################################### 100.0%
Computing checksum with sha256sum
Checksums matched!
Now using node v19.5.0 (npm v9.3.1)
Creating default alias: default -> node (-> v19.5.0) </span></pre>
</div>
<p>
In the above example, <code class="bg_yellow">node</code> is an alias for “the latest version of <code>node.js</code>”.
To install a specific version of <code>node.js</code>, for example 18.13.0:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id694af7f705f1'><button class='copyBtn' data-clipboard-target='#id694af7f705f1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>nvm install 18.13.0</pre>
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="yarn"><span class="code">Yarn</span></h2>
<div class='imgWrapper imgBlock right halfsize' style=' '>
<figure>
<picture class='imgPicture'>
<source srcset="/blog/images/node/needle_and_thread.webp" type="image/webp">
<source srcset="/blog/images/node/needle_and_thread.png" type="image/png">
<img alt='Needle and thread under an electron microscope'
class="imgImg rounded shadow"
src="/blog/images/node/needle_and_thread.png"
style='width: 100%; '
title='Needle and thread under an electron microscope'
/>
</picture>
<figcaption class='imgFigCaption halfsize'>
Needle and thread under an electron microscope
</figcaption>
</figure>
</div>
<p>
Whether you use <code>nvm</code> as your <code>node.js</code> virtualization mechanism,
you also need a <code>node.js</code> package manager to install and maintain project dependencies.
</p>
<p>
<a href='https://yarnpkg.com/' target='_blank' rel='nofollow'><code>Yarn</code></a>
supposedly stands for Yet Another Resource Negotiator,
and it is a package manager like <code>npm</code>,
described <a href="#npm">next</a>.
It was developed by Facebook and is now open-source.
<code>Yarn</code> was developed to address <code>npm</code>’s performance and security issues.
</p>
<p>
The name actually suggests the major advantage <code>yarn</code> has over its predecessor, <code>npm</code>:
multi-threading instead of single-threading.
</p>
<p>
Yarn generates a <code>yarn.lock</code> file, which helps easy merges.
The merges are predictable.
<code>Yarn</code> caches every package it has downloaded, so it never needs to download the same package again.
It also does almost everything concurrently to maximize resource utilization.
This means faster installs.
<code>Yarn</code> guarantees that any installation that works on one system will work exactly the same on another system, unlike <code>npm</code>.
</p>
<p>
The notes for the <a href='https://www.npmjs.com/package/yarn' target='_blank' rel='nofollow'><code>yarn</code> package</a> state that
<code>Yarn</code> was inspired by <a href='https://bundler.io/' target='_blank' rel='nofollow'>Bundler</a> and
<a href='https://doc.rust-lang.org/cargo/' target='_blank' rel='nofollow'>Cargo</a>,
and is nearly command-line compatible with <code>npm</code>.
</p>
<p>
<code>Yarn</code> introduces the zero-install concept, which means that a project should be able to be used as soon as it is cloned.
It uses Plug’n’Play to resolve dependencies via the <code>yarn</code> cache folder and not from <code>node_modules</code>.
The cache folder is by default stored within your project folder, in <code>.yarn/cache</code>.
</p>
<h2 id="install_yarn">Installing <span class="code">Yarn</span></h2>
<p>
To install <code>yarn</code> on Ubuntu:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idba36431b9131'><button class='copyBtn' data-clipboard-target='#idba36431b9131' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | \
sudo apt-key add -
<span class='unselectable'>$ </span>echo "deb https://dl.yarnpkg.com/debian/ stable main" | \
sudo tee /etc/apt/sources.list.d/yarn.list
<span class='unselectable'>$ </span>sudo apt update && sudo apt install yarn</pre>
</div>
<h2 id="yarn_config">Configuring a Project With Yarn</h2>
<p>
The <a href='https://classic.yarnpkg.com/lang/en/docs/cli/init/' target='_blank' rel='nofollow'><code>yarn init</code></a>
command initiates an interactive session that creates a <code>package.json</code> file.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2c13e680911f'><button class='copyBtn' data-clipboard-target='#id2c13e680911f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cd /path/to/my/node/project
<span class='unselectable'>$ </span>yarn init
<span class='unselectable'>yarn init v1.22.19
question name (node): blah
question version (1.0.0): 0.1.0
question description: Just a little song I wrote
question entry point (index.js):
question repository url: https://github.com/mslinn/blah
question author: Mike Slinn
question license (MIT):
question private:
success Saved package.json
Done in 48.29s. </span></pre>
</div>
<p>
The above dialog creates <code>package.json</code> in the current directory:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>package.json</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd1283feb7ecc'><button class='copyBtn' data-clipboard-target='#idd1283feb7ecc' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>{
"name": "blah",
"version": "0.1.0",
"description": "Just a little song I wrote",
"main": "index.js",
"repository": "https://github.com/mslinn/blah",
"author": "Mike Slinn",
"license": "MIT"
}</pre>
</div>
<h2 id="yarn_config">Adding a Dependency to a Project</h2>
<p>
Yarn stores dependencies locally. If the proper version is present locally,
it is fetched from the disk during a <code>yarn add</code> command, otherwise it is downloaded.
This is the general format of the command to add a dependency to a <code>node.js</code> project using <code>yarn</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf2e7c57a2ede'><button class='copyBtn' data-clipboard-target='#idf2e7c57a2ede' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yarn add package_name</pre>
</div>
<p>
To add a specific version of a package:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2851a12dcf43'><button class='copyBtn' data-clipboard-target='#id2851a12dcf43' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yarn add package_name<span class="bg_yellow">@version_number</span></pre>
</div>
<p>
To install a global package, the general formats are:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id789e95365f63'><button class='copyBtn' data-clipboard-target='#id789e95365f63' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yarn <span class="bg_yellow">global</span> add package_name
<span class='unselectable'>$ </span>yarn <span class="bg_yellow">global</span> add package_name@version_number</pre>
</div>
<h2 id="git_yarn"><span class="code">Yarn</span> and <span class="code">Git</span></h2>
The <a href='https://yarnpkg.com/getting-started/install' target='_blank' rel='nofollow'>official Yarn docs</a> say to add the following to
<code>.gitignore</code> for git projects that use <code>yarn</code>:
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>.gitignore</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id094ca1db19ec'><button class='copyBtn' data-clipboard-target='#id094ca1db19ec' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions</pre>
</div>
<h2 id="yarn_install">Installing Dependencies</h2>
<p>
To install all the dependencies in a <code>node.js</code> project,
type <a href='https://yarnpkg.com/getting-started/usage#installing-all-the-dependencies' target='_blank' rel='nofollow'><code>yarn</code> or <code>yarn install</code></a>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>shell – Updating All Dependencies With Yarn</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide8ecf2c9d2b6'><button class='copyBtn' data-clipboard-target='#ide8ecf2c9d2b6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cd /path/to/my/project
<span class='unselectable'>$ </span>yarn</pre>
</div>
<h2 id="yarn_update">Updating Dependencies</h2>
<p>
To upgrade all the dependencies in a <code>node.js</code> project,
use <a href='https://classic.yarnpkg.com/lang/en/docs/cli/upgrade/' target='_blank' rel='nofollow'><code>yarn upgrade</code></a>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>shell – Updating All Dependencies With Yarn</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida992894cc4cd'><button class='copyBtn' data-clipboard-target='#ida992894cc4cd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cd /path/to/my/project
<span class='unselectable'>$ </span>yarn upgrade</pre>
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="npm"><span class="code">Npm</span></h2>
<p>
<code>Npm</code> is the original package manager for the family of JavaScript programming languages,
and it is still the default package manager for <code>node.js</code>.
This will likely change as <code>yarn</code> matures.
<code>Npm</code> helps install libraries, plugins, frameworks and applications.
It consists of a command-line client, also called <code>npm</code>,
and an online database of public and paid-for private packages called the <code>npm</code> registry.
</p>
<div class="pullQuote">
If you are using <span class="code">yarn</span> then you do not need to use <span class="code">npm</span>.
</div>
<p>
<code>Npm</code> fetches dependencies from the <code>npm</code>
registry for every <code>npm install</code> command.
</p>
<p>
<code>Npm</code> generates a <code>package-lock.json</code> file.
The layout of this file is a trade-off between determinism and simplicity.
The same <code>node_modules/</code> folder will be generated by every version of <code>npm</code>.
Every dependency will have a version number associated with it in the <code>package-lock</code> file.
</p>
<h3 id="npm_yarn"><span class="code">Npm</span> vs. <span class="code">Yarn</span></h3>
<p>
When using <code>npm install</code>, dependencies are installed sequentially, one after another.
The output logs in the terminal are informative but a bit hard to read.
To install the packages with Yarn, run the <code>yarn</code> command.
Yarn installs packages in parallel, which is one of the reasons it is quicker than npm.
</p>
<p>
For more information, see
<a href='https://www.geeksforgeeks.org/difference-between-npm-and-yarn/' target='_blank' rel='nofollow'>Difference between npm and yarn</a>.
</p>
<h3 id="global_install">Install A Global Package</h3>
<p>
To install a global package, the command template for <code>npm</code> is:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id36cc8b1c8444'><button class='copyBtn' data-clipboard-target='#id36cc8b1c8444' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>npm install -g package_name[@version_number]</pre>
</div>
<p>
For example:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id570b69eea951'><button class='copyBtn' data-clipboard-target='#id570b69eea951' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>npm install -g broken-link-checker
<span class='unselectable'>npm WARN deprecated calmcard@0.1.1: no longer maintained
npm WARN deprecated nopter@0.3.0: try optionator
npm WARN deprecated uuid@2.0.3: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.
npm WARN deprecated urlobj@0.0.11: use universal-url, minurl, relateurl, url-relation
added 104 packages, and audited 105 packages in 5s </span></pre>
</div>
<!-- endregion -->
Fun With Python Enums2022-02-10T00:00:00-05:00https://mslinn.github.io/blog/2022/02/10/python-3.4-enums<p>
This blog post demonstrates how to define additional properties for
<a href='https://docs.python.org/3/library/enum.html' target='_blank' rel='nofollow'>Python 3 enums</a>.
Defining an additional property in a Python enum can provide a simple way to provide string values.
The concept is then expanded to demonstrate composition, an important concept for functional programming.
This post concludes with a demonstration of dynamic dispatch in Python, by further extending an enum.
</p>
<h2 id="enum">Adding Properties to Python Enums</h2>
<p>
Searching for <a href='https://www.google.com/search?q=python+enum+string+value' target='_blank' rel='nofollow'><code>python enum string value</code></a>
yields some complex and arcane ways to approach the problem.
</p>
<p>
Below is a short example of a Python enum that demonstrates a simple way to provide lower-case string values for enum constants:
a new property, <code>to_s</code>, is defined.
This property provides the string representation that is required.
You could define other properties and methods to suit the needs of other projects.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cad_enums.py</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd0c17e2bf108'><button class='copyBtn' data-clipboard-target='#idd0c17e2bf108' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>"""Defines enums"""<br/>
from enum import Enum, auto<br/>
class EntityType(Enum):
"""Types of entities"""
SITE = auto()
GROUP = auto()
COURSE = auto()
SECTION = auto()
LECTURE = auto()<br/>
<span class="bg_yellow"> @property
def to_s(self) -> str:
""":return: lower-case name of this instance"""
return self.name.lower()</span></pre>
</div>
<p>
Adding the following to the bottom of the program allows us to demonstrate it:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cad_enums.py (part 2)</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id40bdf96a57f4'><button class='copyBtn' data-clipboard-target='#id40bdf96a57f4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>if __name__ == "__main__": # Just for demonstration
print("Specifying individual values:")
print(f" {EntityType.SITE.value}: {EntityType.SITE.to_s}")
print(f" {EntityType.GROUP.value}: {EntityType.GROUP.to_s}")
print(f" {EntityType.COURSE.value}: {EntityType.COURSE.to_s}")
print(f" {EntityType.SECTION.value}: {EntityType.SECTION.to_s}")
print(f" {EntityType.LECTURE.value}: {EntityType.LECTURE.to_s}")
print("\nIterating through all values:")
for entity_type in EntityType:
print(f" {entity_type.value}: {entity_type.to_s}")</pre>
</div>
<p>
Running the program produces this output:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id311d6c2d7a8f'><button class='copyBtn' data-clipboard-target='#id311d6c2d7a8f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cad_enums.py
<span class='unselectable'>Specifying individual values:
1: site
2: group
3: course
4: section
5: lecture
Iterating through all values:
1: site
2: group
3: course
4: section
5: lecture </span></pre>
</div>
<div class="right" style="font-size: 3em;">😁</div>
<p>
Easy!
</p>
<h2 id="constructor">Constructing Enums</h2>
<p>
Enum constructors work the same as other Python class constructors.
There are several ways to make a new instance of a Python enum.
Let's try two ways by using the
<a href='https://docs.python.org/3/tutorial/interpreter.html' target='_blank' rel='nofollow'>Python interpreter</a>.
Throughout this blog post I've inserted a blank line between Python interpreter prompts for readability.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Python</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd7fedbd44f4d'><button class='copyBtn' data-clipboard-target='#idd7fedbd44f4d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>python
<span class='unselectable'>Python 3.9.7 (default, Sep 10 2021, 14:59:43)
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> </span>from cad_enums import EntityType
<span class='unselectable'>>>> </span># Specify the desired enum constant value symbolically
<span class='unselectable'>>>> </span><span class="bg_yellow">gtype = EntityType.GROUP</span>
<span class='unselectable'>>>> </span>print(gtype)
<span class='unselectable'>EntityType.GROUP </span>
<span class='unselectable'>>>> </span># Specify the desired enum constant value numerically
<span class='unselectable'>>>> </span><span class="bg_yellow">stype = EntityType(1)</span>
<span class='unselectable'>>>> </span>print(stype)
<span class='unselectable'>EntityType.SITE </span></pre>
</div>
<h2 id="ordering">Enum Ordering</h2>
<p>
A program I am working on needs to obtain the parent <code>EntityType</code>.
By 'parent' I mean the <code>EntityType</code> with the next lowest numeric value.
For example, the parent of <code>EntityType.GROUP</code> is <code>EntityType.SITE</code>.
We can obtain a parent enum by computing its numeric value
by adding the following method to the <code>EntityType</code> class definition.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Python</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3c7616809b0a'><button class='copyBtn' data-clipboard-target='#id3c7616809b0a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>@property
def parent(self) -> <span class="bg_yellow">'EntityType'</span>:
""":return: entity type of parent; site has no parent"""
return EntityType(max(self.value - 1, 1))</pre>
</div>
<p>
The <span class="bg_yellow">return type</span> above is enclosed in quotes
(<code>'EntityType'</code>) to keep Python's type checker happy,
because this is a <a href='https://www.python.org/dev/peps/pep-0484/#forward-references' target='_blank' rel='nofollow'>forward reference</a>.
This is a forward reference because the type is referenced before it is fully compiled.
</p>
<p>
The complete enum class definition is now:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cad_enums.py</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id95ec5877583a'><button class='copyBtn' data-clipboard-target='#id95ec5877583a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>"""Defines enums"""<br/>
from enum import Enum, auto<br/>
class EntityType(Enum):
"""Types of entities"""
SITE = auto()
GROUP = auto()
COURSE = auto()
SECTION = auto()
LECTURE = auto()<br/>
@property
def to_s(self) -> str:
""":return: lower-case name of this instance"""
return self.name.lower()<br>
@property
def parent(self) -> 'EntityType':
""":return: entity type of parent; site has no parent"""
return EntityType(max(self.value - 1, 1))</pre>
</div>
<p>
Lets try out the new <code>parent</code> property in the Python interpreter.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Python</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf3ca7c07c955'><button class='copyBtn' data-clipboard-target='#idf3ca7c07c955' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>>>> </span>EntityType.LECTURE.parent
<span class='unselectable'><EntityType.SECTION: 4> </span>
<span class='unselectable'>>>> </span>EntityType.SECTION.parent
<span class='unselectable'><EntityType.COURSE: 3> </span>
<span class='unselectable'>>>> </span>EntityType.COURSE.parent
<span class='unselectable'><EntityType.GROUP: 2> </span>
<span class='unselectable'>>>> </span>EntityType.GROUP.parent
<span class='unselectable'><EntityType.SITE: 1> </span>
<span class='unselectable'>>>> </span>EntityType.SITE.parent
<span class='unselectable'><EntityType.SITE: 1> </span></pre>
</div>
<h2 id="compose">Enum Composition</h2>
<p>
Like methods and properties in all other Python classes,
enum methods and properties compose if they return an instance of the class.
Composition is also known as <a href='https://en.wikipedia.org/wiki/Method_chaining' target='_blank' rel='nofollow'>method chaining</a>,
and also can apply to class properties.
Composition is an essential practice of a functional programming style.
</p>
<p>
The <code>parent</code> property returns an instance of the <code>EntityType</code> enum class,
so it can be composed with any other property or method of that class,
for example the <code>to_s</code> property shown earlier.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Python</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd061782537d7'><button class='copyBtn' data-clipboard-target='#idd061782537d7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>>>> </span>EntityType.LECTURE.parent.to_s
<span class='unselectable'>'section' </span>
<span class='unselectable'>>>> </span>EntityType.SECTION.parent.to_s
<span class='unselectable'>'course' </span>
<span class='unselectable'>>>> </span>EntityType.COURSE.parent.to_s
<span class='unselectable'>'group' </span>
<span class='unselectable'>>>> </span>EntityType.GROUP.parent.to_s
<span class='unselectable'>'site' </span>
<span class='unselectable'>>>> </span>EntityType.SITE.parent.to_s
<span class='unselectable'>'site' </span></pre>
</div>
<h2 id="dispatch">Dynamic Dispatch</h2>
<p>
The <a href='https://docs.python.org/3/library/typing.html#callable' target='_blank' rel='nofollow'>Python documentation</a>
might lead someone to assume that writing <a href='https://en.wikipedia.org/wiki/Dynamic_dispatch' target='_blank' rel='nofollow'>dynamic dispatch</a> code is more complex than it actually is.
</p>
<p>
To summarize the documentation, all Python classes, methods and instances are callable.
<a href='https://www.tutorialsteacher.com/python/callable-method' target='_blank' rel='nofollow'><code>Callable</code> functions</a>
have type <code>Callable[[InputArg1Type, InputArg2Type], ReturnType]</code>.
If you do not want any type checking, write <code>Callable[..., Any]</code>.
However, this is not very helpful information for dynamic dispatch.
Fortunately, working with <code>Callable</code> is very simple.
</p>
<p class="notepaper">
You can pass around any Python class,
constructor, function or method, and later provide it with the usual arguments.
Invocation just works.
</p>
<p>
Let me show you how easy it is to write dynamic dispatch code in Python,
let's construct one of five classes, depending on the value of an enum.
First, we need a class definition for each enum value:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Python</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id58c854951a53'><button class='copyBtn' data-clipboard-target='#id58c854951a53' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button># pylint: disable=too-few-public-methods
class BaseClass():
"""Demo only"""
class TestLecture(BaseClass):
"""This constructor has type Callable[[int, str], TestLecture]"""
def __init__(self, id_: int, action: str):
print(f"CadLecture constructor called with id {id_} and action {action}")
class TestSection(BaseClass):
"""This constructor has type Callable[[int, str], TestSection]"""
def __init__(self, id_: int, action: str):
print(f"CadSection constructor called with id {id_} and action {action}")
class TestCourse(BaseClass):
"""This constructor has type Callable[[int, str], TestCourse]"""
def __init__(self, id_: int, action: str):
print(f"CadCourse constructor called with id {id_} and action {action}")
class TestGroup(BaseClass):
"""This constructor has type Callable[[int, str], TestGroup]"""
def __init__(self, id_: int, action: str):
print(f"CadGroup constructor called with id {id_} and action {action}")
class TestSite(BaseClass):
"""This constructor has type Callable[[int, str], TestSite]"""
def __init__(self, id_: int, action: str):
print(f"CadSite constructor called with id {id_} and action {action}")</pre>
</div>
<p>
Now lets add another method, called <code>construct</code>, to <code>EntityType</code> that invokes
the appropriate constructor according to the value of an <code>EntityType</code> instance:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Python</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb28f891e91d4'><button class='copyBtn' data-clipboard-target='#idb28f891e91d4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>@property
def construct(self) -> Callable:
""":return: the appropriate Callable for each enum value"""
if self == EntityType.LECTURE:
return TestLecture
if self == EntityType.SECTION:
return TestSection
if self == EntityType.COURSE:
return TestCourse
if self == EntityType.GROUP:
return TestGroup
return TestSite</pre>
</div>
<p class="alert right rounded shadow" style="width: 50%;">
Using named arguments makes your code resistant to problems that might sneak in due to parameters changing over time.
</p>
<p>
I favor using named arguments at all times; it avoids many problems.
As code evolves, arguments might be added or removed, or even reordered.
</p>
<p class="clear">
Let's test out dynamic dispatch in the Python interpreter.
A class specific to each <code>EntityType</code> value is constructed by invoking the appropriate <code>Callable</code>
and passing it named arguments <a href='https://stackoverflow.com/a/28091085/553865' target='_blank' rel='nofollow'><code>id_</code></a> and <code>action</code>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Python</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6f11fb7fcf6d'><button class='copyBtn' data-clipboard-target='#id6f11fb7fcf6d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>>>> </span>EntityType.LECTURE.construct(id_=55, action="gimme_lecture")
<span class='unselectable'>TestLecture constructor called with id 55 and action gimme_lecture
<entity_types.TestLecture object at 0x7f9aac690070> </span>
<span class='unselectable'>>>> </span>EntityType.SECTION.construct(id_=13, action="gimme_section")
<span class='unselectable'>TestSection constructor called with id 13 and action gimme_section
<entity_types.TestSection object at 0x7f9aac5c1730> </span>
<span class='unselectable'>>>> </span>EntityType.COURSE.construct(id_=40, action="gimme_course")
<span class='unselectable'>TestCourse constructor called with id 40 and action gimme_course
<entity_types.TestCourse object at 0x7f9aac6900a0> </span>
<span class='unselectable'>>>> </span>EntityType.GROUP.construct(id_=103, action="gimme_group")
<span class='unselectable'>TestGroup constructor called with id 103 and action gimme_group
<entity_types.TestGroup object at 0x7f9aac4c6b20> </span>
<span class='unselectable'>>>> </span>EntityType.SITE.construct(id_=1, action="gimme_site")
<span class='unselectable'>TestSite constructor called with id 1 and action gimme_site
<entity_types.TestSite object at 0x7f9aac5c1730> </span></pre>
</div>
<p>
Because these factory methods return the newly created instance of the desired type, the string representation is printed on the console after the method finishes outputting its processing results, for example:
<code><entity_types.TestLecture object at 0x7f9aac690070></code>.
</p>
<p>
Using enums to construct class instances and/or invoke methods (aka dynamic dispatch) is super powerful.
It rather resembles generics, actually, even though
<a href='https://docs.python.org/3/library/typing.html#building-generic-types' target='_blank' rel='nofollow'>Python’s support for generics</a> is still in its infancy.
</p>
<p>
The complete Python program discussed in this post is <a href='/blog/python/entity_types.py'>here</a>.
</p>
Linking Directories on NTFS and Ext4 Volumes2022-02-07T00:00:00-05:00https://mslinn.github.io/blog/2022/02/07/wsl-volumes<p>
Sometimes I need to insert some code into a program that depends on the type of format that a drive volume has.
For example, today I need to either make a Windows junction to connect two directories on NTFS volumes.
On the other hand, if one or both directories were on other types of volumes,
I would have to connect the directories using a Linux symlink.
</p>
<p>
All of the bash scripts shown in this blog post are meant to run in a Bash shell running on WSL or WSL2.
</p>
<h2 id="possible">Use Windows Junctions When Possible</h2>
<p>
When working on WSL, Windows junctions are more desirable than Linux hard links and symlinks because junctions are visible in Windows and also in WSL.
Unlike Linux hard links, which only work within a single volume, Windows junctions can span two volumes.
Linux symlinks are only visible from WSL;
a symlinked directory only appears as a useless file when viewed from Windows.
</p>
<div class='imgWrapper imgFlex right' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/wsl-volumes/windowsFileMgr.webp" type="image/webp">
<source srcset="/blog/images/wsl-volumes/windowsFileMgr.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/wsl-volumes/windowsFileMgr.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
Both directories need to be on NTFS volumes to make a Windows junction between them.
Junctions are permitted within a single NTFS volume, or between two NTFS volumes.
Linux symlinks can be used on all volume types, but only work properly when viewed from Linux.
</p>
<p>
Windows junctions are shown with a small arrow icon in Windows File Manager.
In the image above, the <code>curriculum</code> directory is a junction.
</p>
<h2 id="volumeType">Determining a Volume Type</h2>
<p>
I wrote the <code>volumeType</code> bash function to obtain the type of the volume that contains a file or directory.
Linux <code>ext4</code> volumes have partition type <code>ext4</code>.
NTFS volumes have partition type <code>9p</code>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id70900390fcfd'><button class='copyBtn' data-clipboard-target='#id70900390fcfd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>function volumeType {
# Usually returns volume types ext4 or 9p (for NTFS)
df -Th "$1" | tail -n 1 | awk '{print $2}'
}</pre>
</div>
<p>
Here are examples of using <code>volumeType</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5daa5efb5e01'><button class='copyBtn' data-clipboard-target='#id5daa5efb5e01' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>volumeType /mnt/c # Under WSL/WSL2 this is usually NTFS
<span class='unselectable'>9p </span>
<span class='unselectable'>$ </span>volumeType / # For Ubuntu this defaults to ext4
<span class='unselectable'>ext4 </span></pre>
</div>
<p>
All of the remaining scripts on this page either return a value (indicating <code>true</code>),
or they do not return anything (indicating <code>false</code>).
</p>
<p>
Two more bash functions test if a file or directory is part of an NTFS or ext4 volume:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd9761c458f80'><button class='copyBtn' data-clipboard-target='#idd9761c458f80' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>function isNTFS {
if [ "$( volumeType "$1" )" == 9p ]; then echo yes; fi
}
function isExt4 {
if [ "$( volumeType "$1" )" == ext4 ]; then echo yes; fi
}</pre>
</div>
<p>
Here are examples of using <code>isNTFS</code> and <code>isExt4</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id11bf1782ae75'><button class='copyBtn' data-clipboard-target='#id11bf1782ae75' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>isNTFS /mnt/c
<span class='unselectable'>yes </span>
<span class='unselectable'>$ </span>isExt4 /mnt/c
<span class='unselectable'>$ </span>isNTFS /
<span class='unselectable'>$ </span>isExt4 /
<span class='unselectable'>yes </span></pre>
</div>
<h2 id="junctions">Windows Junctions</h2>
<p>
The <code>bothOnNTFS</code> bash function indicates if both of the paths passed to it are on NTFS volumes.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id50d7de5147b4'><button class='copyBtn' data-clipboard-target='#id50d7de5147b4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>function bothOnNTFS {
if [ "$( isNTFS "$1" )" ] && [ "$( isNTFS "$2" )" ]; then echo yes; fi
}</pre>
</div>
<p>
Let's try out <code>bothOnNTFS</code>.
</p>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2d81a4234c3f'><span class='unselectable'>$ </span>bothOnNTFS /mnt/c /mnt/f
<span class='unselectable'>yes </span>
<span class='unselectable'>$ </span>bothOnNTFS /mnt/c /</pre>
</div>
<p>
<code>bothOnNTFS</code> lets us decide how to connect two directories.
If they are both on NTFS volumes, we can connect them using a Windows junction; otherwise we'll need to symlink them.
</p>
<h2 id="connect">Connecting Via a Windows Junction or Linux Symlink</h2>
<p>
We could either make a Windows junction using the
<a href='https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/mklink' target='_blank' rel='nofollow'><code>mklink</code></a>
command, or we could make a Linux symlink using the
<a href='https://man7.org/linux/man-pages/man1/ln.1.html' target='_blank' rel='nofollow'><code>ln -s</code></a> command.
Notice how the order of parameters between <code>mklink</code> is the reverse of the order of the Linux <code>ln</code> command.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9afc3352a361'><button class='copyBtn' data-clipboard-target='#id9afc3352a361' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>if [ $( bothOnNTFS "$cadenzaCurriculum" . ) ]; then
WINDOWS_PATH="$( wslpath -w "$cadenzaCurriculum/site_$TITLE" )"
echo "Making Windows junction from $cadenzaCurriculum/site_$TITLE to curriculum/"
cmd.exe /C mklink /j curriculum "$WINDOWS_PATH"
else
echo "Symlinking $cadenzaCurriculum/site_$TITLE to curriculum/"
ln -s "$cadenzaCurriculum/site_$TITLE" curriculum
fi</pre>
</div>
When the above code ran it produced:
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idec19aa177ba5'>Making Windows junction from /mnt/f/work/cadenzaHome/cadenzaCurriculum/site_ScalaCourses.com to curriculum/
Junction created for curriculum <<===>> F:\work\cadenzaHome\cadenzaCurriculum\site_ScalaCourses.com</pre>
</div>
<!-- #region -->
<h2 id="">Windows Junction for Windows Home in Ubuntu</h2>
<p>
Neither Linux nor Java appreciate spaces in directories.
Spaces in home directories are by nature problematic.
If you have a space in your home directory on Windows, you
can use the PowerShell <code>New-Item</code> commandlet to create an equivalent junction to a new directory node without spaces in the name.
</p>
<p>
The following uses the PowerShell
<a href='https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/new-item?view=powershell-7.3' target='_blank' rel='nofollow'><code>New-Item</code></a>
commandlet to create a new directory called <code>C:\<wbr>Users\<wbr>mslinn</code>,
accessible from WSL as <code>/mnt/<wbr>c/<wbr>mslinn</code>,
which points to the same directory as <code>C:\Users\<wbr>Mike Slinn</code>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>PowerShell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id72f354b18075'><button class='copyBtn' data-clipboard-target='#id72f354b18075' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>PS C:\Users\Mike Slinn> </span>ni "C:\Users\mslinn" -i SymbolicLink -ta "C:\Users\Mike Slinn"</pre>
</div>
<!-- endregion -->
Handcrafted Dynamic DNS for AWS Route53 and Namecheap2022-01-30T00:00:00-05:00https://mslinn.github.io/blog/2022/01/30/ddns-route53<p>
Now that I have fiber-optic internet service in my apartment, with 500 GB/s upload and download,
I thought I would save money by hosting my <a href='https://scalacourses.com' target='_blank' rel='nofollow'><code>scalacourses.com</code></a> website on an Ubuntu server that runs here,
instead of AWS.
My home IP address is quite stable, and only changes when the fiber modem boots up.
The modem is branded as a
<a href='https://support.bell.ca/internet/products/home-hub-4000-modem' target='_blank' rel='nofollow'>Bell Home Hub 4000</a>,
but I believe it is actually made by Arris (formerly known as Motorola).
</p>
<p>
It makes little sense to pay the commercial cost of dedicated dynamic DNS services (typically $55 USD / year)
when it is so easy to automate,
and the operational cost is less than one cent per year.
</p>
<p>
I wrote two little scripts that automatically check
my public IP address, and modifies the DNS record for my home IP address whenever the IP address changes.
One script is for sites that use DNS provided by AWS Route53, and the other is for DNS provided by Namecheap.
</p>
<p>
The approach shown here could be used for all DNS servers that have a command-line interface.
This was originally written for AWS Route53,
but when I <a href='/blog/2022/05/26/aws-hijacking.html'>moved off AWS</a> I made a version for
<a href='https://www.namecheap.com/support/knowledgebase/article.aspx/29/11/how-to-dynamically-update-the-hosts-ip-with-an-http-request/' target='_blank' rel='nofollow'>Namecheap</a>.
Alternative DNS providers include
<a href='https://docs.microsoft.com/en-us/cli/azure/network/dns?view=azure-cli-latest' target='_blank' rel='nofollow'>Azure DNS</a>,
<a href='https://developers.cloudflare.com/cloudflare-one/tutorials/cli' target='_blank' rel='nofollow'>Cloudflare DNS</a>,
<a href='https://support.dnsmadeeasy.com/support/solutions/articles/47001119947-the-ddns-shell-script' target='_blank' rel='nofollow'>DNSMadeEasy</a>,
<a href='https://developer.dnsimple.com/libraries/' target='_blank' rel='nofollow'>DNSimple</a>,
<a href='https://cloud.google.com/sdk/gcloud/reference/dns' target='_blank' rel='nofollow'>Google Cloud DNS</a>,
and <a href='https://github.com/ultradns/dns_sprockets' target='_blank' rel='nofollow'>UltraDNS</a>.
</p>
<h2 id="fwd">Forwarding HTTP Requests</h2>
<p>
I added some entries to the modem so incoming HTTP traffic on ports 80 and 443 would be forwarded to ports
9000 and 9443 on my home server, <code>gojira</code>.
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/ddns/homeHubPortForward.webp" type="image/webp">
<source srcset="/blog/images/ddns/homeHubPortForward.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/ddns/homeHubPortForward.png"
style='width: 100%; '
/>
</picture>
</div>
<h2 id="usage">Using the Scripts</h2>
<p>
The version for AWS Route53 is called <code>dynamicDnsAws</code>,
and the version for Namecheap is called <code>dynamicDnsNamecheap</code>.
</p>
<p>
The scripts save the IP address to a file,
and periodically compare the saved value to the current value.
Then the scripts modify the DNS record for a specified subdomain
whenever the value of the public IP address changes.
</p>
<h3 id="aws">Using the AWS Script</h3>
<p>
Here is the help information for the script:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcf2ef79c89e1'><button class='copyBtn' data-clipboard-target='#idcf2ef79c89e1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>dynamicDnsAws
<span class='unselectable'>dynamicDnsAws - Maintains a dynamic DNS record in AWS Route53<br>
Saves data in '/home/mslinn/.dynamicDnsAws'<br>
Syntax:
dynamicDnsAws [OPTIONS] SUB_DOMAIN DOMAIN<br>
OPTIONS:
-v Verbose mode<br>
Example usage:
dynamicDnsAws www scalacourses.com
dynamicDnsAws -v www scalacourses.com </span></pre>
</div>
<p>
Here is a sample usage:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id09643f8964d9'><button class='copyBtn' data-clipboard-target='#id09643f8964d9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>dynamicDnsAws www scalacourses.com
<span class='unselectable'>{
"ChangeInfo": {
"Id": "/change/C075751811HI18SH4L8L0",
"Status": "PENDING",
"SubmittedAt": "2022-01-30T21:10:09.261Z",
"Comment": "UPSERT a record for www.scalacourses.com"
}
} </span></pre>
</div>
<h3 id="aws">Using the Namecheap Script</h3>
<p>
Here is the help information for the script:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfdf29dcabcec'><button class='copyBtn' data-clipboard-target='#idfdf29dcabcec' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>dynamicDnsNamecheap
<span class='unselectable'>dynamicDnsNamecheap - Maintains two Namecheap dynamic DNS records<br>
Saves data in '/home/mslinn/.dynamicDns'<br>
Syntax:
dynamicDnsNamecheap [OPTIONS] DOMAIN PASSWORD<br>
OPTIONS:
-v Verbose mode<br>
Example usage:
dynamicDnsNamecheap mydomain.com asdfasdfasdfasdfasdf
dynamicDnsNamecheap -v mydomain.com asdfasdfasdfasdf </span></pre>
</div>
<p>
Here is sample usage:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcf0476e997b4'><button class='copyBtn' data-clipboard-target='#idcf0476e997b4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>dynamicDnsNamecheap scalacourses.com asdfasdfasdfasdfasdf
<span class='unselectable'><?xml version="1.0" encoding="utf-16"?>
<interface-response>
<Command>SETDNSHOST</Command>
<Language>eng</Language>
<IP>142.126.4.220</IP>
<ErrCount>0</ErrCount>
<errors />
<ResponseCount>0</ResponseCount>
<responses />
<Done>true</Done>
<debug><![CDATA[]]></debug>
</interface-response><?xml version="1.0" encoding="utf-16"?>
<interface-response>
<Command>SETDNSHOST</Command>
<Language>eng</Language>
<IP>142.126.4.220</IP>
<ErrCount>0</ErrCount>
<errors />
<ResponseCount>0</ResponseCount>
<responses />
<Done>true</Done>
<debug><![CDATA[]]></debug>
</interface-response> </span></pre>
</div>
<h2 id="crontab">Invoking the Scripts from Crontab</h2>
<p>
A personal <code>crontab</code> can be modified by typing:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd733245d1d46'><button class='copyBtn' data-clipboard-target='#idd733245d1d46' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>crontab -e</pre>
</div>
<p>
I pasted in the following into <code>crontab</code> on my Ubuntu server, running at home.
These lines invoke the <code>dynamicDnsNamecheap</code> script via <code>crontab</code> every 5 minutes.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9bed5a488c84'><button class='copyBtn' data-clipboard-target='#id9bed5a488c84' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>*/5 * * * * /path/to/dynamicDnsNamecheap my_domain.com asdfasdfasdfasdfasdf</pre>
</div>
<p>
One the above is saved, <code>crontab</code> will run the script every 5 minutes.
</p>
<h2 id="source">Script Source Codes</h2>
<p>
Here are the bash scripts:
</p>
<div class="codeLabel"><a href='data:text/plain;charset=UTF-8,dynamicDnsAws' download='dynamicDnsAws'
title='Click on the file name to download the file'>dynamicDnsAws</a>
</div>
<pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="idd4bf40c3ddd8">#!/bin/bash
# Author: Mike Slinn mslinn@mslinn.com
# Written 2022-01-30
export SAVE_FILE_NAME="$HOME/.dynamicDns"
function help {
echo "$( basename $0 ) - Maintains a dynamic DNS record in AWS Route53
Saves data in '$SAVE_FILE_NAME'
Syntax:
$( basename $0) [OPTIONS] SUB_DOMAIN DOMAIN
OPTIONS:
-v Verbose mode
Example usage:
$( basename $0) my_subdomain mydomain.com
$( basename $0) -v my_subdomain mydomain.com
"
exit 1
}
function upsert {
export HOSTED_ZONES="$(
aws route53 list-hosted-zones
)"
export HOSTED_ZONE_RECORD="$(
jq -r ".HostedZones[] | select(.Name == \"$DOMAIN.\")" <<< "$HOSTED_ZONES"
)"
export HOSTED_ZONE_RECORD_ID="$(
jq -r .Id <<< "$HOSTED_ZONE_RECORD"
)"
aws route53 change-resource-record-sets \
--hosted-zone-id "$HOSTED_ZONE_RECORD_ID" \
--change-batch "{
\"Comment\": \"UPSERT a record for $SUBDOMAIN.$DOMAIN\",
\"Changes\": [{
\"Action\": \"UPSERT\",
\"ResourceRecordSet\": {
\"Name\": \"$SUBDOMAIN.$DOMAIN\",
\"Type\": \"A\",
\"TTL\": 300,
\"ResourceRecords\": [{ \"Value\": \"$IP\"}]
}
}]
}"
echo "$IP" > "$SAVE_FILE_NAME"
}
if [ "$1" == -v ]; then
export VERBOSE=true
shift
fi
if [ -z "$2" ]; then help; fi
set -e
export SUBDOMAIN="$1"
export DOMAIN="$2"
export IP="$( dig +short myip.opendns.com @resolver1.opendns.com )"
if [ ! -f "$SAVE_FILE_NAME" ]; then
if [ "$VERBOSE" ]; then echo "Creating $SAVE_FILE_NAME"; fi
upsert;
elif [ $( cat "$SAVE_FILE_NAME" ) != "$IP" ]; then
if [ "$VERBOSE" ]; then
echo "Updating $SAVE_FILE_NAME"
echo "'$IP' was not equal to '$( cat "$SAVE_FILE_NAME" )'"
fi
upsert;
else
if [ "$VERBOSE" ]; then echo "No change necessary for $SAVE_FILE_NAME"; fi
fi
</pre>
<div class="codeLabel"><a href='data:text/plain;charset=UTF-8,dynamicDnsNamecheap' download='dynamicDnsNamecheap'
title='Click on the file name to download the file'>dynamicDnsNamecheap</a>
</div>
<pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="idc84a6700e85f">#!/bin/bash
# Author: Mike Slinn mslinn@mslinn.com
# Modified from AWS version (dynameicDnsAws) 2022-06-30
# See https://www.namecheap.com/support/knowledgebase/article.aspx/36/11/how-do-i-start-using-dynamic-dns/
export SAVE_FILE_NAME="$HOME/.dynamicDns"
function help {
echo "$( basename $0 ) - Maintains two Namecheap dynamic DNS records
Saves data in '$SAVE_FILE_NAME'
Syntax:
$( basename $0) [OPTIONS] DOMAIN PASSWORD
OPTIONS:
-v Verbose mode
Example usage:
$( basename $0) mydomain.com asdfasdfasdfasdfasdf
$( basename $0) -v mydomain.com asdfasdfasdfasdf
"
exit 1
}
function upsert {
curl "https://dynamicdns.park-your-domain.com/update?host=@&domain=$DOMAIN&password=$PASSWORD&ip=$IP"
curl "https://dynamicdns.park-your-domain.com/update?host=www&domain=$DOMAIN&password=$PASSWORD&ip=$IP"
echo "$IP" > "$SAVE_FILE_NAME"
echo ""
}
if [ "$1" == -v ]; then
export VERBOSE=true
shift
fi
if [ -z "$2" ]; then help; fi
set -e
export DOMAIN="$1"
export PASSWORD="$2"
export IP="$( dig +short myip.opendns.com @resolver1.opendns.com )"
if [ ! -f "$SAVE_FILE_NAME" ]; then
if [ "$VERBOSE" ]; then echo "Creating $SAVE_FILE_NAME"; fi
upsert;
elif [ "$( cat "$SAVE_FILE_NAME" )" != "$IP" ]; then
if [ "$VERBOSE" ]; then
echo "Updating $SAVE_FILE_NAME"
echo "'$IP' was not equal to '$( cat "$SAVE_FILE_NAME" )'"
fi
upsert;
else
if [ "$VERBOSE" ]; then echo "No change necessary for $SAVE_FILE_NAME"; fi
fi
</pre>
Windows Diskpart Cooperates With Diskmgmt2022-01-14T00:00:00-05:00https://mslinn.github.io/blog/2022/01/14/diskpart<p>
Recently, I wanted to use some old hard drives as backup media.
That meant scrubbing all the partitions off the drives, and installing new partitions, which of course would be empty.
</p>
<div class="right rounded shadow warning" style="width: 45%">
<p>
Warning – Working with a command line program for system-level operations,
without having backed up the system,
is like walking on a tightrope without a net.
</p>
<p style="margin-bottom: 0">
A mistake could inadvertently wipe out a different hard drive on the computer than you intended.
Without the ability to restore the system disk from backup, your computer could become inoperable.
</p>
</div>
<p>
However, I was unable to repartition one of those old drives using the GUI-based Windows <code>diskmgmt.msc</code> disk manager.
The drive had soft errors.
For some reason, those errors made it impossible for <code>diskmgmt.msc</code> to scan the drive.
</p>
<p>
The more powerful Windows
<a href='https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/diskpart' target='_blank' rel='nofollow'><code>diskpart</code></a>,
a command line program, was able to get the job done.
</p>
<p>
This article <a href ="#gui">ends</a> by demonstrating how you can get benefit from the <code>diskmgmt</code>
GUI even when you are using the <code>diskpart</code> command line interface.
This is possible because every command you type into <code>diskpart</code> causes a Windows system event
to be published, and because <code>diskmgmt</code> subscribes to those events, it is able to display the results as they happen.
</p>
<h2 id="starting">Starting <span class="code">Diskpart</span></h2>
<p>
Run <code>diskpart</code> as administrator as follows:
</p>
<ol>
<li>
Press the <kbd>Windows</kbd> key.
Do not hold it down, just depress it once, as you would do for any other key, and let it go.
</li>
<li>Type <code>diskpart</code>.</li>
<li>Use the mouse or arrow keys to select <b>Run as administrator</b>.</li>
</ol>
<div class='imgWrapper imgFlex center' style='width: 75%; '>
<picture class='imgPicture'>
<source srcset="/blog/images/diskpart/diskpartLaunch.webp" type="image/webp">
<source srcset="/blog/images/diskpart/diskpartLaunch.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/diskpart/diskpartLaunch.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
A window should open up labeled <b>Microsoft DiskPart</b>.
Let's start by listing all the <code>diskpart</code> commands.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfdb3c8fe71ef'><button class='copyBtn' data-clipboard-target='#idfdb3c8fe71ef' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>Microsoft DiskPart version 10.0.19041.964
Copyright (C) Microsoft Corporation.
On computer: BEAR
DISKPART> </span>help
<span class='unselectable'>Microsoft DiskPart version 10.0.19041.964
ACTIVE - Mark the selected partition as active.
ADD - Add a mirror to a simple volume.
ASSIGN - Assign a drive letter or mount point to the selected volume.
ATTRIBUTES - Manipulate volume or disk attributes.
ATTACH - Attaches a virtual disk file.
AUTOMOUNT - Enable and disable automatic mounting of basic volumes.
BREAK - Break a mirror set.
CLEAN - Clear the configuration information, or all information, off the
disk.
COMPACT - Attempts to reduce the physical size of the file.
CONVERT - Convert between different disk formats.
CREATE - Create a volume, partition or virtual disk.
DELETE - Delete an object.
DETAIL - Provide details about an object.
DETACH - Detaches a virtual disk file.
EXIT - Exit DiskPart.
EXTEND - Extend a volume.
EXPAND - Expands the maximum size available on a virtual disk.
FILESYSTEMS - Display current and supported file systems on the volume.
FORMAT - Format the volume or partition.
GPT - Assign attributes to the selected GPT partition.
HELP - Display a list of commands.
IMPORT - Import a disk group.
INACTIVE - Mark the selected partition as inactive.
LIST - Display a list of objects.
MERGE - Merges a child disk with its parents.
ONLINE - Online an object that is currently marked as offline.
OFFLINE - Offline an object that is currently marked as online.
RECOVER - Refreshes the state of all disks in the selected pack.
Attempts recovery on disks in the invalid pack, and
resynchronizes mirrored volumes and RAID5 volumes
that have stale plex or parity data.
REM - Does nothing. This is used to comment scripts.
REMOVE - Remove a drive letter or mount point assignment.
REPAIR - Repair a RAID-5 volume with a failed member.
RESCAN - Rescan the computer looking for disks and volumes.
RETAIN - Place a retained partition under a simple volume.
SAN - Display or set the SAN policy for the currently booted OS.
SELECT - Shift the focus to an object.
SETID - Change the partition type.
SHRINK - Reduce the size of the selected volume.
UNIQUEID - Displays or sets the GUID partition table (GPT) identifier or
master boot record (MBR) signature of a disk. </span></pre>
</div>
<h2 id="listing">Listing Drives, Partitions and Volumes</h2>
<p>
Listing the drives is generally a good first step.
Let's discover the command for that:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id25dd41d2760f'><button class='copyBtn' data-clipboard-target='#id25dd41d2760f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>list
<span class='unselectable'>Microsoft DiskPart version 10.0.19041.964
DISK - Display a list of disks. For example, LIST DISK.
PARTITION - Display a list of partitions on the selected disk.
For example, LIST PARTITION.
VOLUME - Display a list of volumes. For example, LIST VOLUME.
VDISK - Displays a list of virtual disks. </span></pre>
</div>
<p>
OK, we can list disks, partitions, volumes and virtual disks.
Let's list the disk drives.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1e1b035f5295'><button class='copyBtn' data-clipboard-target='#id1e1b035f5295' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>list disk
<span class='unselectable'>Disk ### Status Size Free Dyn Gpt
-------- ------------- ------- ------- --- ---
Disk 0 Online 1863 GB 1024 KB *
Disk 1 Online 465 GB 1024 KB
Disk 2 Online 1863 GB 1024 KB *
Disk 3 Online 1863 GB 0 B
* Disk 5 Online 931 GB 931 GB * </span></pre>
</div>
<div class='imgWrapper imgBlock right quartersize' style=' '>
<figure>
<a href='https://www.amazon.com/NewerTech-Enclosure-Interface-NWTU3S3HD-hot-swapping/dp/B007TTQQIA' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/diskpart/newerTechVoyagerS3.webp" type="image/webp">
<source srcset="/blog/images/diskpart/newerTechVoyagerS3.png" type="image/png">
<img alt='NewerTech Voyager S3 caddy'
class="imgImg rounded shadow"
src="/blog/images/diskpart/newerTechVoyagerS3.png"
style='width: 100%; '
title='NewerTech Voyager S3 caddy'
/>
</picture>
</a>
<figcaption class='imgFigCaption quartersize'>
<a href="https://www.amazon.com/NewerTech-Enclosure-Interface-NWTU3S3HD-hot-swapping/dp/B007TTQQIA" target='_blank' >
NewerTech Voyager S3 caddy
</a>
</figcaption>
</figure>
</div>
<p>
At this point, I inserted the old 5.25" SATA drive that I wanted to repurpose into a
NewerTech Voyager S3 caddy,
connected to my computer via a USB 3 cable,
and Windows automatically mounted it.
The caddy also accepts 2.5" SATA drives, such as those commonly found in laptops.
</p>
<p>
Now I told <code>diskpart</code> to rescan the drives, and then I listed the volumes on all disks.
</p>
<div class="clear"></div>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4c7cce680a24'><button class='copyBtn' data-clipboard-target='#id4c7cce680a24' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>rescan
<span class='unselectable'>Please wait while DiskPart scans your configuration...
DiskPart has finished scanning your configuration.
DISKPART> </span>list volume
<span class='unselectable'>Volume ### Ltr Label Fs Type Size Status Info
---------- --- ----------- ----- ---------- ------- --------- --------
Volume 0 D DVD-ROM 0 B No Media
Volume 1 C BEAR_C NTFS Partition 1861 GB Healthy Boot
Volume 2 FAT32 Partition 100 MB Healthy System
Volume 3 NTFS Partition 539 MB Healthy Hidden
Volume 4 NTFS Partition 450 MB Healthy Hidden
Volume 5 F Work NTFS Partition 1863 GB Healthy
Volume 6 E BEAR_E NTFS Partition 1863 GB Healthy
<span class="bg_yellow"> Volume 8 FAT32 Partition 512 MB Healthy Hidden</span> </span></pre>
</div>
<p>
<code>Diskpart</code> displayed the hidden volume (#8) in the drive in the caddy.
This drive has <code>readonly</code> status set,
which prevents its contents from being modified or deleted.
That would be good if I wanted to use this drive as an archive,
but instead I want to scrub it and write new information on it.
The currently existing partitions on this drive cannot be erased until <code>readonly</code> is cleared.
Lets remove <code>readonly</code> status from the drive now:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7049252a5d63'><button class='copyBtn' data-clipboard-target='#id7049252a5d63' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>select volume 8
<span class='unselectable'>Volume 8 is the selected volume.
DISKPART> </span>attributes disk clear readonly
<span class='unselectable'>Disk attributes cleared successfully.
DISKPART> </span>rescan
<span class='unselectable'>Please wait while DiskPart scans your configuration...
DiskPart has finished scanning your configuration.
DISKPART> </span>list volume
<span class='unselectable'>Volume ### Ltr Label Fs Type Size Status Info
---------- --- ----------- ----- ---------- ------- --------- --------
Volume 0 D DVD-ROM 0 B No Media
Volume 1 C BEAR_C NTFS Partition 1861 GB Healthy Boot
Volume 2 FAT32 Partition 100 MB Healthy System
Volume 3 NTFS Partition 539 MB Healthy Hidden
Volume 4 NTFS Partition 450 MB Healthy Hidden
Volume 5 F Work NTFS Partition 1863 GB Healthy
Volume 6 E BEAR_E NTFS Partition 1863 GB Healthy
Volume 8 FAT32 Partition 512 MB Healthy Hidden </span></pre>
</div>
<h2 id="wipe">Wiping the Drive</h2>
<p>
Now it is time to wipe the drive clean, which removes all partitions.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id196f086ccc54'><button class='copyBtn' data-clipboard-target='#id196f086ccc54' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>select disk 5
<span class='unselectable'>Disk 5 is now the selected disk.
DISKPART> </span>list disk
<span class='unselectable'>Disk ### Status Size Free Dyn Gpt
-------- ------------- ------- ------- --- ---
Disk 0 Online 1863 GB 1024 KB *
Disk 1 Online 465 GB 1024 KB
Disk 2 Online 1863 GB 1024 KB *
Disk 3 Online 1863 GB 0 B
* Disk 5 Online 931 GB 931 GB *
DISKPART> </span>clean
<span class='unselectable'>DiskPart succeeded in cleaning the disk.
</span></pre>
</div>
<h2 id="setRO">Archiving a Drive</h2>
<p>
If instead of wiping the drive, I wanted to archive the drive,
I would want to set the read-only status.
To do that, first select the drive as before, then type:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id13e4b8dd8a78'><button class='copyBtn' data-clipboard-target='#id13e4b8dd8a78' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>list disk
<span class='unselectable'># Output as shown above </span>
<span class='unselectable'>DISKPART> </span>select disk N
<span class='unselectable'>Disk N is now the selected disk. </span>
<span class='unselectable'>DISKPART> </span>attributes disk set readonly</pre>
</div>
<p>
Now the drive's contents could not accidently be erased or modified.
</p>
<h2 id="partition">Create A New Partition</h2>
<p>
We need to create a new partition that spans the entire disk on the now-empty selected drive.
The <code>create</code> command can do that.
Let's look at the help before using the command:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3e06d0f672f2'><button class='copyBtn' data-clipboard-target='#id3e06d0f672f2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>create
<span class='unselectable'>Microsoft DiskPart version 10.0.19041.964
PARTITION - Create a partition.
VOLUME - Create a volume.
VDISK - Creates a virtual disk file.
DISKPART> </span>create partition
<span class='unselectable'>Microsoft DiskPart version 10.0.19041.964
EFI - Create an EFI system partition.
EXTENDED - Create an extended partition.
LOGICAL - Create a logical drive.
MSR - Create a Microsoft Reserved partition.
PRIMARY - Create a primary partition.
</span></pre>
</div>
<p>
To create a new partition that spans the entire disk on the now-empty selected drive
and make it active:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida6f2aea35020'><button class='copyBtn' data-clipboard-target='#ida6f2aea35020' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>create partition primary
<span class='unselectable'>DiskPart succeeded in creating the specified partition.
DISKPART> </span>select partition 1
<span class='unselectable'>Partition 1 is now the selected partition.
DISKPART> </span>active
<span class='unselectable'>DiskPart marked the current partition as active.
</span></pre>
</div>
<h2 id="format">Format A Volume</h2>
<p>
Let's format the entire selected drive as one volume.
Like the <code>clean</code> command,
the <code>format</code> command operates on the currently selected disk.
First, let's look at the help:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id888a3d2f462f'><button class='copyBtn' data-clipboard-target='#id888a3d2f462f' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>help format
<span class='unselectable'>Formats the specified volume for use with Windows.
Syntax: FORMAT [[FS=<FS>] [REVISION=<X.XX>] | RECOMMENDED] [LABEL=<"label">]
[UNIT=<N>] [QUICK] [COMPRESS] [OVERRIDE] [DUPLICATE] [NOWAIT]
[NOERR]
FS=<FS> Specifies the type of file system. If no file system is given,
the default file system displayed by the FILESYSTEMS command is
used.
REVISION=<X.XX>
Specifies the file system revision (if applicable).
RECOMMENDED If specified, use the recommended file system and revision
instead of the default if a recommendation exists. The
recommended file system (if one exists) is displayed by the
FILESYSTEMS command.
LABEL=<"label">
Specifies the volume label.
UNIT=<N> Overrides the default allocation unit size. Default settings
are strongly recommended for general use. The default
allocation unit size for a particular file system is displayed
by the FILESYSTEMS command.
NTFS compression is not supported for allocation unit sizes
above 4096.
QUICK Performs a quick format.
COMPRESS NTFS only: Files created on the new volume will be compressed
by default.
OVERRIDE Forces the file system to dismount first if necessary. All
opened handles to the volume would no longer be valid.
DUPLICATE UDF Only: This flag applies to UDF format, version 2.5 or
higher.
This flag instructs the format operation to duplicate the file
system meta-data to a second set of sectors on the disk. The
duplicate meta-data is used by applications, for example repair
or recovery applications. If the primary meta-data sectors are
found to be corrupted, the file system meta-data will be read
from the duplicate sectors.
NOWAIT Forces the command to return immediately while the format
process is still in progress. If NOWAIT is not specified,
DiskPart will display format progress in percentage.
NOERR For scripting only. When an error is encountered, DiskPart
continues to process commands as if the error did not occur.
Without the NOERR parameter, an error causes DiskPart to exit
with an error code.
A volume must be selected for this operation to succeed.
Examples:
FORMAT FS=NTFS LABEL="New Volume" QUICK COMPRESS
FORMAT RECOMMENDED OVERRIDE </span></pre>
</div>
<p>
<code>Diskpart</code> automatically chooses the optimal file system for the selected drive if you do not specify it.
Choices include FAT, FAT32 and NTFS.
To quick format the selected drive using the optimal file system:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbf7fd3283380'><button class='copyBtn' data-clipboard-target='#idbf7fd3283380' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>format quick</pre>
</div>
<p>
The default is to fully format the selected drive, which is what you want if the selected drive is suspect:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8391ad13f51e'><button class='copyBtn' data-clipboard-target='#id8391ad13f51e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>format</pre>
</div>
<p>
You could specify multiple parameters, for example:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7d5eb5014904'><button class='copyBtn' data-clipboard-target='#id7d5eb5014904' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>format fs=ntfs label="My Drive" quick</pre>
</div>
<h2 id="assign">Assigning a Drive Letter</h2>
<p>
Once the drive was formatted,
I assigned the selected drive the letter <code>G</code>.
You do not normally need to perform this step, unless you want to
<a href='https://www.groovypost.com/howto/assign-permanent-letter-removable-usb-drive-windows/' target='_blank' rel='nofollow'>define a default letter for this drive</a>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id963f49b00a3b'><button class='copyBtn' data-clipboard-target='#id963f49b00a3b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>assign letter=G</pre>
</div>
<h2 id="gui"><span class="code">Diskmgmt</span> GUI Shows Progress</h2>
<p>
Having a GUI continuously report the current state of the drive maintenance you are performing
using a command-line interface is a good practice.
</p>
<p>
The GUI-based Windows disk manager, <code>diskmgmt.msc</code>,
can show the instantaneous progress of all <code>diskpart</code> commands,
working on every drive,
including creating and deleting partitions, volumes, formatting and much more.
For example, formatting progress can be seen here:
</p>
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/diskpart/diskUI.webp" type="image/webp">
<source srcset="/blog/images/diskpart/diskUI.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/diskpart/diskUI.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
Run <code>diskmgmt.msc</code> by:
</p>
<ol>
<li>Press the <kbd>Windows</kbd> key once and let go.</li>
<li>Type <code>diskmgmt</code></li>
<li>Press <kbd>Enter</kbd></li>
</ol>
<h2 id="exit"><span class="code">Exit</span></h2>
<p>
To exit <code>diskpart</code>, either type <code>Exit</code> or press <kbd>Ctrl</kbd>-<kbd>C</kbd>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Microsoft DiskPart</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id581e07545580'><button class='copyBtn' data-clipboard-target='#id581e07545580' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>DISKPART> </span>exit</pre>
</div>
<p>
The drive is ready for its next assignment!
</p>
WSL / WSL 2 Backup and Restore2022-01-10T00:00:00-05:00https://mslinn.github.io/blog/2022/01/10/wsl-backup<!-- #region -->
<p>
I needed to back up my WSL2 installation before
<a href='https://www.microsoft.com/en-us/software-download/windows10' target='_blank' rel='nofollow'>reinstalling Windows 10</a>,
as one must do every 6 months if you work your machine like a developer.
It had been years since I had last refreshed this machine,
and it was now bluescreening several times a day.
Reboots took forever.
</p>
<div class="pullQuoteFull liImg">
This article presents the best WSL backup approaches I found
</div>
<p>
I wanted to retain my WSL2 instance.
When refreshing Windows you must reinstall all your programs,
and you lose all the WSL/WSL2 instances.
The refresh decommissions them,
then moves them into a hidden directory tree,
along with the rest of the stuff stored in your old Windows 10 profile directory tree.
</p>
<p>
I also wanted to be able to replicate my WSL2 instance reliably and easily.
It was very important to me to be able to administer WSL instances separately from my Windows 10 profile.
</p>
<!-- endregion -->
<!-- #region -->
<h2 id="location">Location, Location, Location</h2>
<p>
Only recently has it become possible to work with WSL/WSL2 images on non-system drives.
Most online documentation is now out of date in this regard.
Running Windows off a different drive than the drive that a WSL/WSL2 client OS runs from
can provide a dramatic performance boost for some applications.
</p>
<div class="pullQuoteFull liImg">
Reinstalling Windows is less traumatic if the WSL/WSL2 image elsewhere in the filesystem
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="dell">Dell’s Ignorance Causes Pain</h2>
<p>
I use Dell laptops, because I love their onsite warranty price and product features.
However, sometimes it feels like Dell’s solution to every problem is to replace the motherboard.
</p>
<p>
When Windows 10 encounters a new motherboard, it takes anti-piracy measures,
and refuses to do much of anything useful.
</p>
<p>
Refreshing Windows solves that problem,
but doing that blows away the standard WSL/WSL2 image.
Again. And again. And again!
</p>
<p>
Dell replaced the motherboard 4 times in 2 years for one of my laptops.
They simply do not understand how replacing the motherboard creates long-term issues.
</p>
<!-- endregion -->
<!-- #region -->
<h2 id="ready">Test Your Backup</h2>
<p>
Backing up WSL/WSL2 can be problematic.
Restoring it is even more delicate.
Knowing this, I decided to backup and test the restoration process before refreshing Windows and potentially losing my working system.
</p>
<p>
I have tried several times before to make this work.
As I said, it had been years since I allowed the OS in my main workstation to be refreshed.
The reason I resisted doing proper maintenance was because I wanted to preserve my WSL2 Ubuntu image.
Until today, I met with frustrating failures every time I attempted to use the standard tools.
Today I succeeded, via new software, hence the publication of my notes.
Who wants to read stories about all the things that do not work?
</p>
<p class="alert rounded shadow">
Following is my experience.
I began by following the directions in
<a href='https://www.windowscentral.com/how-backup-windows-subsystem-linux-wsl-distribution' target='_blank' rel='nofollow'>How to back up a Windows Subsystem for Linux (WSL) distribution</a>,
published 18 Feb 2021 by Windows Central.
It is a nice story, but I've tried this stuff on a variety of computers,
and this is not Microsoft’s most robust code base.
Read on and I will tell you of reality as I found it.
</p>
<!-- endregion -->
<!-- #region -->
<h2 id="wslOptions">WSL Command-Line Options</h2>
<p>
First let's look at the command-line options for the <code>wsl</code> command.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide56dced94774'><button class='copyBtn' data-clipboard-target='#ide56dced94774' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>Microsoft Windows [Version 10.0.19044.1415]
(c) Microsoft Corporation. All rights reserved. </span>
<span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --help
<span class='unselectable'>Copyright (c) Microsoft Corporation. All rights reserved.
For privacy information about this product please visit https://aka.ms/privacy.<br/>
Usage: wsl.exe [Argument] [Options...] [CommandLine]<br/>
Arguments for running Linux binaries:<br/>
If no command line is provided, wsl.exe launches the default shell.<br/>
--exec, -e <CommandLine>
Execute the specified command without using the default Linux shell.<br/>
--shell-type <Type>
Execute the specified command with the provided shell type.<br/>
Types:
standard
Execute the specified command using the default Linux shell.<br/>
login
Execute the specified command using the default Linux shell as a log-in shell.<br/>
none
Execute the specified command without using the default Linux shell.<br/>
--
Pass the remaining command line as-is.<br/>
Options:
--cd <Directory>
Sets the specified directory as the current working directory.
If ~ is used the Linux user's home path will be used. If the path begins
with a / character, it will be interpreted as an absolute Linux path.
Otherwise, the value must be an absolute Windows path.<br/>
--distribution, -d <Distro>
Run the specified distribution.<br/>
--user, -u <UserName>
Run as the specified user.<br/>
--system
Launches a shell for the system distribution.<br/>
Arguments for managing Windows Subsystem for Linux:<br/>
--help
Display usage information.<br/>
--debug-shell
Open a WSL2 debug shell for diagnostics purposes.<br/>
--install [Distro] [Options...]
Install a Windows Subsystem for Linux distribution.
For a list of valid distributions, use 'wsl.exe --list --online'.<br/>
Options:
--no-launch, -n
Do not launch the distribution after install.<br/>
--web-download
Download the distribution from the internet instead of the Microsoft Store.<br/>
--mount <Disk>
Attaches and mounts a physical or virtual disk in all WSL 2 distributions.<br/>
Options:
--vhd
Specifies that <Disk> refers to a virtual hard disk.<br/>
--bare
Attach the disk to WSL2, but don't mount it.<br/>
--name <Name>
Mount the disk using a customised name for the mount-point.<br/>
--type <Type>
Filesystem to use when mounting a disk, if not specified defaults to ext4.<br/>
--options <Options>
Additional mount options.<br/>
--partition <Index>
Index of the partition to mount, if not specified defaults to the whole disk.<br/>
--set-default-version <Version>
Changes the default install version for new distributions.<br/>
--shutdown
Immediately terminates all running distributions and the WSL 2
lightweight utility virtual machine.<br/>
--status
Show the status of Windows Subsystem for Linux.<br/>
--unmount [Disk]
Unmounts and detaches a disk from all WSL2 distributions.
Unmounts and detaches all disks if called without argument.<br/>
--update
Update the Windows Subsystem for Linux package.<br/>
Options:
--web-download
Download the update from the internet instead of the Microsoft Store.<br/>
--pre-release
Download a pre-release version if available. Implies --web-download.<br/>
--version, -v
Display version information.<br/>
Arguments for managing distributions in Windows Subsystem for Linux:<br/>
--export <Distro> <FileName> [Options]
Exports the distribution to a tar file.
The filename can be - for standard output.<br/>
Options:
--vhd
Specifies that the distribution should be exported as a .vhdx file.<br/>
--import <Distro> <InstallLocation> <FileName> [Options]
Imports the specified tar file as a new distribution.
The filename can be - for standard input.<br/>
Options:
--version <Version>
Specifies the version to use for the new distribution.<br/>
--vhd
Specifies that the provided file is a .vhdx file, not a tar file.
This operation makes a copy of the .vhdx file at the specified install location.<br/>
--import-in-place <Distro> <FileName>
Imports the specified .vhdx file as a new distribution.
This virtual hard disk must be formatted with the ext4 filesystem type.<br/>
--list, -l [Options]
Lists distributions.<br/>
Options:
--all
List all distributions, including distributions that are
currently being installed or uninstalled.<br/>
--running
List only distributions that are currently running.<br/>
--quiet, -q
Only show distribution names.<br/>
--verbose, -v
Show detailed information about all distributions.<br/>
--online, -o
Displays a list of available distributions for install with 'wsl.exe --install'.<br/>
--set-default, -s <Distro>
Sets the distribution as the default.<br/>
--set-version <Distro> <Version>
Changes the version of the specified distribution.<br/>
--terminate, -t <Distro>
Terminates the specified distribution.<br/>
--unregister <Distro>
Unregisters the distribution and deletes the root filesystem. </span></pre>
</div>
<!-- endregion -->
<!-- endregion -->
<!-- #region -->
<h2 id="wslBackup">Backing Up With WSL</h2>
<p>
Now let's use the <code>wsl</code> command to back up the Ubuntu VM in WSL2.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id4265ba9a7d72'><button class='copyBtn' data-clipboard-target='#id4265ba9a7d72' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --export Ubuntu ubuntuBear_2021-01-10.tar</pre>
</div>
<p>
Well, well, it backed up without any problem in about an hour!
Color me <s>surprised</s>happy.
File size was about 50 GB.
</p>
<p>
Alright, let's try importing the image now.
We'll locate the new image at <code>f:\ubuntuBear</code>.
It used to be that WSL did not support moving or installing a distro to non-system drives.
No longer!
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id177d32a32a21'><button class='copyBtn' data-clipboard-target='#id177d32a32a21' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --import Ubuntu f:\ubuntuBear ubuntuBear_2021-01-10.tar
<span class='unselectable'>A distribution with the supplied name already exists. </span></pre>
</div>
<p>
The error message <code>A distribution with the supplied name already exists</code>
makes sense because I backed up a WSL2 instance called <code>Ubuntu</code> and
instance names must be unique.
Let's import under the name <code>UbuntuBear</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id486f5b8a055c'><button class='copyBtn' data-clipboard-target='#id486f5b8a055c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --import UbuntuBear f:\ubuntuBear ubuntuBear_2021-01-10.tar
<span class='unselectable'>Unspecified error </span></pre>
</div>
<p>
Ahh, the dreaded <code>Unspecified error</code> that <code>wsl import</code> is infamous for.
I found a few potential solutions, detailed below; I show the most desirable solution first.
</p>
<!-- endregion -->
<!-- #region -->
<h2 id="vhdx">Solution 1: Importing the <span class="code">VDHX File</span></h2>
<p>
Importing the original <code>vhdx</code> file directly instead of
creating and importing the <code>tar</code> file has been reported to work by
<a href='https://github.com/microsoft/WSL/issues/4735#issuecomment-1370093052' target='_blank' rel='nofollow'>several people</a>.
This is a more desirable solution because it requires less work and is faster.
Note that the <kbd>^</kbd> character is a DOS command-line continuation character,
much like the Bash <kbd>\</kbd> character.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3b90287f3e86'><button class='copyBtn' data-clipboard-target='#id3b90287f3e86' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --import Ubuntu-20.04-d^
D:\WSL\Ubuntu-22.04\^
%HOMEDRIVE%%HOMEPATH%\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu20.04onWindows_79rhkp1fndgsc\LocalState\ext4.vhdx^
--vhd</pre>
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="vhdx">Solution 2: Updating Linux Kernel</h2>
<p>
<a href='https://github.com/microsoft/WSL/issues/4735' target='_blank' rel='nofollow'>The following</a> was written by
<a href='https://github.com/ken-cdit' target='_blank' rel='nofollow'><code>ken-cdit</code></a>:
</p>
<p>
Had the same issue.
I exported two WSL 2 distros and then did a clean install of my Windows OS.
"Unspecified error" came up when I tried to import them.
</p>
<p>
I fixed it by downloading the Linux kernel update package referred to at
<a href='https://docs.microsoft.com/en-us/windows/wsl/install-win10' target='_blank' rel='nofollow'><code>https:/<wbr>/<wbr>docs.microsoft.com/<wbr>en-us/<wbr>windows/<wbr>wsl/<wbr>install-win10</code></a>
and then running the command <code>wsl --set-default-version 2</code>.
</p>
<p>
After this, my two distros imported without error.
</p>
<p>
I had the whole thing still in the console so here is a screencap...
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/wslBackup/solution.webp" type="image/webp">
<source srcset="/blog/images/wslBackup/solution.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/wslBackup/solution.png"
style='width: 100%; '
/>
</picture>
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="LxRunOfflineInstall">Solution 3: <span class="code">LxRunOffline</span></h2>
<h3 id="LxRunOfflineInstall">Installing <span class="code">LxRunOffline</span></h3>
<p>
<a href='https://github.com/microsoft/WSL/issues/4735#issuecomment-800103777' target='_blank' rel='nofollow'>Google brought me</a>
to a potential solution,
<a href='https://github.com/DDoSolitary/LxRunOffline' target='_blank' rel='nofollow'><code>LxRunOffline</code></a>, which provided me limited success.
</p><p>
I downloaded the binaries in
<a href='https://github.com/DDoSolitary/LxRunOffline/releases/download/v3.5.0/LxRunOffline-v3.5.0-msvc.zip' target='_blank' rel='nofollow'><code>LxRunOffline-v3.5.0-msvc.zip</code></a>
directly from <a href='https://github.com/DDoSolitary/LxRunOffline/releases' target='_blank' rel='nofollow'>GitHub</a>
into <code>C:\Program Files\LxRunOffline</code>.
</p>
<p>
Now add <code>C:\Program Files\LxRunOffline</code> to the Windows <code>PATH</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5ec32c4b3381'><button class='copyBtn' data-clipboard-target='#id5ec32c4b3381' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>setx PATH "%PATH%;C:\Program Files\LxRunOffline"
SUCCESS: Specified value was saved. %}</pre>
</div>
<p>
I then ran the following in an administrative shell to register the DLL:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2b289423a640'><button class='copyBtn' data-clipboard-target='#id2b289423a640' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>regsvr32 "C:\Program Files\LxRunOffline\LxRunOfflineShellExt.dll"</pre>
</div>
<!-- endregion -->
<!-- #region -->
<h3 id="LxRunOffline" class="code">LxRunOffline Help Info</h3>
<p>
Let's progressively discover how this command-line program can be used.
First I'll just type the command name, which causes the program to list its top-level actions.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id254ca3202052'><button class='copyBtn' data-clipboard-target='#id254ca3202052' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>LxRunOffline
<span class='unselectable'>[ERROR] No action is specified.
Supported actions are:
l, list List all installed distributions.
gd, get-default Get the default distribution, which is used by bash.exe.
sd, set-default Set the default distribution, which is used by bash.exe.
i, install Install a new distribution.
ui, uninstall Uninstall a distribution.
rg, register Register an existing installation directory.
ur, unregister Unregister a distribution but not delete the installation directory.
m, move Move a distribution to a new directory.
d, duplicate Duplicate an existing distribution in a new directory.
e, export Export a distribution"s filesystem to a .tar.gz file, which can be imported by the "install" command.
r, run Run a command in a distribution.
di, get-dir Get the installation directory of a distribution.
gv, get-version Get the filesystem version of a distribution.
ge, get-env Get the default environment variables of a distribution.
se, set-env Set the default environment variables of a distribution.
ae, add-env Add to the default environment variables of a distribution.
re, remove-env Remove from the default environment variables of a distribution.
gu, get-uid Get the UID of the default user of a distribution.
su, set-uid Set the UID of the default user of a distribution.
gk, get-kernelcmd Get the default kernel command line of a distribution.
sk, set-kernelcmd Set the default kernel command line of a distribution.
gf, get-flags Get some flags of a distribution. See https://docs.microsoft.com/en-us/previous-versions/windows/desktop/api/wslapi/ne-wslapi-wsl_distribution_flags for details.
sf, set-flags Set some flags of a distribution. See https://docs.microsoft.com/en-us/previous-versions/windows/desktop/api/wslapi/ne-wslapi-wsl_distribution_flags for details.
s, shortcut Create a shortcut to launch a distribution.
ec, export-config Export configuration of a distribution to an XML file.
ic, import-config Import configuration of a distribution from an XML file.
sm, summary Get general information of a distribution.
version Get version information about this LxRunOffline.exe. </span></pre>
</div>
<!-- endregion -->
<p>
Alright, let's get information about the <code>i</code> (<code>install</code>) option.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida7761b69ee01'><button class='copyBtn' data-clipboard-target='#ida7761b69ee01' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>LxRunOffline i
<span class='unselectable'>[ERROR] the option '-d' is required but missing
Options:
-n arg Name of the distribution
-d arg The directory to install the distribution into.
-f arg The tar file containing the root filesystem of the
distribution to be installed. If a file of the same
name with a .xml extension exists and "-c" isn"t
specified, that file will be imported as a config file.
-r arg The directory in the tar file to extract. This argument
is optional.
-c arg The config file to use. This argument is optional.
-v arg (=2) The version of filesystem to use, latest available one
if not specified.
-s Create a shortcut for this distribution on Desktop. </span></pre>
</div>
<!-- endregion -->
<p>
I'll use the <code>-d</code> option to specify the directory to install into,
as well as the <code>-f</code> and <code>-n</code> options.
</p>
<!-- endregion -->
<!-- #region -->
<h3 id="LxRunOfflineImport"><span class="code">LxRunOffline</span> Import</h3>
<p>
I feel brave, let's try importing for real now:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd490848f4e7e'><button class='copyBtn' data-clipboard-target='#idd490848f4e7e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>LxRunOffline i -d f:/ubuntuBear -n UbuntuBear -f ubuntuBear_2021-01-10.tar
<span class='unselectable'>[ERROR] The distro "UbuntuBear" already exists. </span></pre>
</div>
<p>
Some debris remains from the failed import (remember the <code>Unspecified error</code> a moment ago?).
I will delete the b0rked <code>UbuntuBear</code> instance in a moment.
Let's call this newly cloned linux instance <code>UbuntuBear2</code>.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2405ffc796f3'><button class='copyBtn' data-clipboard-target='#id2405ffc796f3' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>LxRunOffline i -d f:/ubuntuBear -n UbuntuBear2 -f ubuntuBear_2021-01-10.tar
<span class='unselectable'>[WARNING] Ignoring an unsupported file "var/lib/docker/volumes/backingFsBlockDev" of type 0060000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-17041-8893592-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-10716-846285-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-1899-75308257-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-10151-8749190-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-18789-129803847-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-811-127664322-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-12447-115564106-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-24480-65839207-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-23230-107496852-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-23230-107496852-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-24480-65839207-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-14211-5836039-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-19778-110293600-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-1969-34761241-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-17041-8893592-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-1899-75308257-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-7642-17996-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-10151-8749190-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-19082-8772885-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-12857-5829248-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-26692-70009588-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-12447-115564106-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-22582-1181861-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-14211-5836039-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-1969-34761241-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-7642-17996-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-10716-846285-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-11424-57667328-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-28497-49814437-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-18789-129803847-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-2234-80045-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-19082-8772885-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-12857-5829248-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-22582-1181861-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-26692-70009588-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-10990-5814132-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-811-127664322-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-10990-5814132-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-11424-57667328-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-2234-80045-out" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-19778-110293600-in" of type 0010000.
[WARNING] Ignoring an unsupported file "tmp/clr-debug-pipe-28497-49814437-in" of type 0010000.
[WARNING] Ignoring an unsupported file "dev/random" of type 0020000.
[WARNING] Ignoring an unsupported file "dev/tty" of type 0020000.
[WARNING] Ignoring an unsupported file "dev/full" of type 0020000.
[WARNING] Ignoring an unsupported file "dev/urandom" of type 0020000.
[WARNING] Ignoring an unsupported file "dev/ptmx" of type 0020000.
[WARNING] Ignoring an unsupported file "dev/zero" of type 0020000.
[WARNING] Ignoring an unsupported file "dev/console" of type 0020000.
[WARNING] Ignoring an unsupported file "dev/null" of type 0020000.
[WARNING] Ignoring an unsupported file "dev/mapper/control" of type 0020000.
[ERROR] Couldn"t create the file "\\?\f:\ubuntuBear\rootfs\home\mslinn\.atom\packages\markdown-preview-plus\spec\fixtures\subdir\�cc�nt�d.md".
Reason: The file exists. </span></pre>
</div>
<!-- endregion -->
<p>
Most of the warnings do not seem to be important.
I do not care about Docker anyway.
Also, now I know that temporary files and <code>dev</code> nodes should all be deleted before running this program.
Even better, the program that created the tar should be modified to not attempt to replicate any temporary files.
</p>
<p>
I don't understand the problem with the atom package,
but I'll try to delete all of the atom settings from the tar,
along with the other problematic directories,
and then retry.
</p>
<!-- endregion -->
<!-- #region -->
<h3 id="cleanup">Cleaning Up WSL</h3>
<p>
First let's see the VMs registered with WSL:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id924710f0e2a7'><button class='copyBtn' data-clipboard-target='#id924710f0e2a7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl -l
<span class='unselectable'>Windows Subsystem for Linux Distributions:
Ubuntu (Default)
UbuntuBear
UbuntuBear2 </span></pre>
</div>
<p>
Let's delete the debris remaining from the failed <code>UbuntuBear</code> and <code>UbuntuBear2</code> imports:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idad9e025b647c'><button class='copyBtn' data-clipboard-target='#idad9e025b647c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --unregister UbuntuBear
<span class='unselectable'>Unregistering... </span>
<span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --unregister UbuntuBear2
<span class='unselectable'>Unregistering... </span></pre>
</div>
<p>
Attempting to delete the directory created by LxRunOffline hit a corrupted directory.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6441caaf3edc'><button class='copyBtn' data-clipboard-target='#id6441caaf3edc' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>del /s /q f:\ubuntuBear
<span class='unselectable'>Deleted file - f:\ubuntuBear\rootfs\init
Deleted file - f:\ubuntuBear\rootfs\lib
Deleted file - f:\ubuntuBear\rootfs\lib32
Deleted file - f:\ubuntuBear\rootfs\lib64
Deleted file - f:\ubuntuBear\rootfs\libx32
Deleted file - f:\ubuntuBear\rootfs\sbin
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.wget-hsts
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.Xauthority
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.zcompdump
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.zcompdump-Bear-5.8
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.zcompdump-localhost-5.8
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.zshenv
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.zshrc
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.zshrc.pre-oh-my-zsh
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.zsh_history
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\ancient_warmth_workspace.code-workspace
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\bear2.zip
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\bear3.zip
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\bearDirs.tar
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\dead.letter
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\django_bash_completion
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\jekyll_workspace.code-workspace
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\msp.txt
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\nodesource_setup.sh
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\package-lock.json
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\package.json
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\worldPeaceMusicCollective.png
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\worldPeaceMusicCollectiveBordered.png
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.vscode-server\extensions\wix.vscode-import-cost-2.15.0\package.json
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.vscode-server\extensions\wix.vscode-import-cost-2.15.0\pom.xml
Deleted file - f:\ubuntuBear\rootfs\home\mslinn\.vscode-server\extensions\wix.vscode-import-cost-2.15.0\README.md
The file or directory is corrupted and unreadable. </span></pre>
</div>
<!-- endregion -->
<p>
“The file or directory is corrupted and unreadable.”
That needs to be dealt with right away!
I fixed the directory errors in drive F like this:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc6bb8b4c4c59'><button class='copyBtn' data-clipboard-target='#idc6bb8b4c4c59' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Program Files> </span>chkdsk /F F:
<span class='unselectable'>The type of the file system is NTFS.
Chkdsk cannot run because the volume is in use by another
process. Chkdsk may run if this volume is dismounted first.
ALL OPENED HANDLES TO THIS VOLUME WOULD THEN BE INVALID. </span>y
<span class='unselectable'>Chkdsk cannot dismount the volume because it is a system drive or
there is an active paging file on it. Would you like to schedule
this volume to be checked the next time the system restarts? (Y/N)</span> y
<span class='unselectable'>This volume will be checked the next time the system restarts. </span></pre>
</div>
<p>
I rebooted the system, and it fixed the errors on drive F:.
</p>
<!-- endregion -->
<!-- #region -->
<h3 id="prune">Pruning the tar</h3>
<p>
First lets back up the tar that we want to prune.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5daef9ba3f32'><button class='copyBtn' data-clipboard-target='#id5daef9ba3f32' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>pushd '/mnt/c/Users/Mike Slinn/'
<span class='unselectable'>$ </span>ls -alF *.tar
<span class='unselectable'>-rwxr--r-- 1 mslinn mslinn 524482560 Jan 10 18:45 'bear_ubuntu_2021-01-10.tar'*
-rwxr--r-- 1 mslinn mslinn 51464028160 Jan 10 21:11 'ubuntuBear_2021-01-10.tar'* </span>
<span class='unselectable'>$ </span>cp ubuntuBear_2021-01-10.tar ubuntuBearPruned_2021-01-10.tar</pre>
</div>
<p>
Now lets try to prune out all the problematic, and unnecessary, files.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6082c1a43998'><button class='copyBtn' data-clipboard-target='#id6082c1a43998' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>tar -f ubuntuBearPruned_2021-01-10.tar \
--delete dev/* \
--delete tmp/* \
--delete home/mslinn/.atom/* \
--delete var/lib/docker/* \
--delete var/sitesUbuntu/* \
--delete var/work/* \
--delete var/tmp/*
<span class='unselectable'>tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.security.capability'
tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.security.capability'
tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.security.capability'
tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.security.capability'
tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.security.capability'
tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.user.fuseoverlayfs.opaque'
tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.user.fuseoverlayfs.opaque'
tar: Pattern matching characters used in file names
tar: Use --wildcards to enable pattern matching, or --no-wildcards to suppress this warning
tar: dev/*: Not found in archive
tar: Pattern matching characters used in file names
tar: Use --wildcards to enable pattern matching, or --no-wildcards to suppress this warning
tar: tmp/*: Not found in archive
tar: Pattern matching characters used in file names
tar: Use --wildcards to enable pattern matching, or --no-wildcards to suppress this warning
tar: home/mslinn/.atom/*: Not found in archive
tar: Pattern matching characters used in file names
tar: Use --wildcards to enable pattern matching, or --no-wildcards to suppress this warning
tar: var/lib/docker/*: Not found in archive
tar: Pattern matching characters used in file names
tar: Use --wildcards to enable pattern matching, or --no-wildcards to suppress this warning
tar: var/sitesUbuntu/*: Not found in archive
tar: Pattern matching characters used in file names
tar: Use --wildcards to enable pattern matching, or --no-wildcards to suppress this warning
tar: var/work/*: Not found in archive
tar: Pattern matching characters used in file names
tar: Use --wildcards to enable pattern matching, or --no-wildcards to suppress this warning
tar: var/tmp/*: Not found in archive
tar: Exiting with failure status due to previous errors </span></pre>
</div>
<!-- endregion -->
<p>
I think the error messages just indicate that the tar was made by <a href='https://www.freebsd.org/cgi/man.cgi?tar(1)' target='_blank' rel='nofollow'>BSD-TAR</a>,
while Ubuntu uses <a href='https://www.gnu.org/software/tar/' target='_blank' rel='nofollow'>GNU-TAR</a>.
I installed <code>bsdtar</code> like this:
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id169819f53d75'><button class='copyBtn' data-clipboard-target='#id169819f53d75' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt-get install <a href='https://packages.ubuntu.com/hirsute/libarchive-tools' target='_blank' rel='nofollow'>libarchive-tools</a>
<span class='unselectable'>Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following NEW packages will be installed:
libarchive-tools
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 57.1 kB of archives.
After this operation, 207 kB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu hirsute/universe amd64 libarchive-tools amd64 3.4.3-2 [57.1 kB]
Fetched 57.1 kB in 0s (167 kB/s)
Selecting previously unselected package libarchive-tools.
(Reading database ... 244498 files and directories currently installed.)
Preparing to unpack .../libarchive-tools_3.4.3-2_amd64.deb ...
Unpacking libarchive-tools (3.4.3-2) ...
Setting up libarchive-tools (3.4.3-2) ...
Processing triggers for man-db (2.9.4-2) ... </span></pre>
</div>
<!-- endregion -->
<p>
I <a href='https://stackoverflow.com/a/56031400/553865' target='_blank' rel='nofollow'>tried again</a>
using <code>bsdtar</code>,
which does not support GNU tar's <code>--delete</code> option.
Unfortunately, I only got a 1KB file out, no matter what I did.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id0ed73d45c305'><button class='copyBtn' data-clipboard-target='#id0ed73d45c305' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>bsdtar -cvf ubuntuBearPruned2_2021-01-10.tar \
--exclude 'dev/*' \
--exclude 'tmp/*' \
--exclude 'home/mslinn/.atom/*' \
--exclude 'var/lib/docker/*' \
--exclude 'var/sitesUbuntu/*' \
--exclude 'var/work/*' \
--exclude 'var/tmp/*' \
@ubuntuBear_2021-01-10.tar</pre>
</div>
<p>
Maybe there is a bug.
Maybe there is bad documentation.
I do not think I made an error.
Life is short.
Bugs are rampant.
<a href='https://en.wikipedia.org/wiki/Illegitimi_non_carborundum' target='_blank' rel='nofollow'>Illegitimi non carborundum!</a>
</p>
<p>
I give up on this direction.
Let's try to win some other way!
</p>
<!-- endregion -->
<!-- #region -->
<h3 id="duplicate">Success Duplicating the Ubuntu Instance</h3>
<p>
Let’s try duplicating the Ubuntu instance with <code>LxRunOffline</code>.
First the <code>Ubuntu</code> VM must be terminated.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide6b24e8a44b1'><button class='copyBtn' data-clipboard-target='#ide6b24e8a44b1' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl -t Ubuntu
<span class='unselectable'>C:\Users\Mike Slinn> </span>LxRunOffline duplicate -n Ubuntu -N UbuntuWsl2 -d f:\UbuntuWsl2</pre>
</div>
<p>
The above ran for a couple of hours and concluded without error.
<code>LxRunOffline duplicate</code> automatically registers the new instance.
</p>
<p>
The <code>F:\UbuntuWsl2</code>directory was 239 GB.
That's a fair-sized VM.
The directory only contains one file, called <code>ext4.vhdx</code>.
</p>
<p>
Let's see the Ubuntu instances that are currently set:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1442ef7ae94e'><button class='copyBtn' data-clipboard-target='#id1442ef7ae94e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --list
<span class='unselectable'>Windows Subsystem for Linux Distributions:
Ubuntu (Default)
UbuntuWsl2 </span></pre>
</div>
<div class="right" style="font-size: 3em;">😁</div>
<p>
That is what I wanted to see!
I started the new <code>UbuntuWsl2</code> Ubuntu instance like this:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id71390605001a'><button class='copyBtn' data-clipboard-target='#id71390605001a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl -d UbuntuWsl2</pre>
</div>
<p>
To set the default Ubuntu instance I typed:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc3f8f939959c'><button class='copyBtn' data-clipboard-target='#idc3f8f939959c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --setdefault UbuntuWsl2</pre>
</div>
<p>
Let's verify that worked:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id19269d075059'><button class='copyBtn' data-clipboard-target='#id19269d075059' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --list
<span class='unselectable'>Windows Subsystem for Linux Distributions:
UbuntuWsl2 (Default)
Ubuntu </span></pre>
</div>
<p>
I then deleted the huge tar files that I had created in earlier steps.
I never needed them because I used <code>LxRunOffline duplicate</code>.
</p>
<!-- endregion -->
<!-- #region -->
<h2 id="usb3">Ubuntu on 500GB USB3 SSD</h2>
<div class='imgWrapper imgFlex right' style=' '>
<a href='https://www.amazon.com/gp/product/B08GTXVG9P' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/wslBackup/sandiskExtreme.webp" type="image/webp">
<source srcset="/blog/images/wslBackup/sandiskExtreme.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/wslBackup/sandiskExtreme.png"
style='width: 100%; '
/>
</picture>
</a>
</div>
<p>
I have a <a href='https://www.amazon.com/gp/product/B08GTXVG9P' target='_blank' rel='nofollow'>Sandisk Extreme 500GB USB3 SSD drive</a>.
This tiny, light, and very portable storage device should be perfect for holding my Ubuntu development system.
How cool it would be to be able to drop it into a small pocket!
</p>
<p>
Better yet, the
<a href='https://www.quora.com/Can-a-USB-3-0-external-SSD-be-faster-than-an-internal-laptop80%99s-HDD' target='_blank' rel='nofollow'>performance of portable USB3 SSD drives</a>
blows away traditional hard drives.
</p>
<p>
For example, assume that your SSD drive appears as drive <code>X</code>.
Also assume that <code>LxRunOffline.exe</code> and <code>LxRunOffline.dll</code> have been copied to the root directory of the SSD drive.
To register your portable Ubuntu you would simply type:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9b98cfaabfdd'><button class='copyBtn' data-clipboard-target='#id9b98cfaabfdd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\> </span>X:LxRunOffline register X:\MyUbuntu -n UbuntuMSlinn</pre>
</div>
<div class="pullQuote">
You can walk up to any recently updated Windows 10 machine,
plug your tiny USB3 SSD drive into a USB3 or USB3.1 port,
run the registration command, and boom! you are productive with great storage performance.
</div>
<div class="right" style="font-size: 3em;">😁</div>
<!-- endregion -->
<!-- #region -->
<h2 id="duplicate">Duplicating to SSD</h2>
<p>
Again, the <code>Ubuntu</code> VM must be terminated.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide85dbd70ea97'><button class='copyBtn' data-clipboard-target='#ide85dbd70ea97' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl -t Ubuntu
<span class='unselectable'>C:\Users\Mike Slinn> </span>LxRunOffline duplicate -n Ubuntu -N UbuntuWsl2Extreme -d I:\UbuntuWsl2Extreme</pre>
</div>
<p>
The above ran for a couple of hours and concluded without error.
<code>LxRunOffline duplicate</code> automatically registers the new instance.
</p>
<p>
Let's see the Ubuntu instances that are currently set:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='iddc1507c251fb'><button class='copyBtn' data-clipboard-target='#iddc1507c251fb' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\Users\Mike Slinn> </span>wsl --list
<span class='unselectable'>Windows Subsystem for Linux Distributions:
Ubuntu (Default)
UbuntuWsl2
UbuntuWsl2Extreme </span></pre>
</div>
<div class="right" style="font-size: 3em;">😁</div>
<!-- endregion -->
<!-- #region -->
<h2 id="LxRunOfflineUpdate"><span class="code">LxRunOffline Update 2023-04-25</span></h2>
<p>
<a href='https://github.com/armanexplorer' target='_blank' rel='nofollow'>Arman Mazloumzadeh</a>
posted the
<a href='https://github.com/microsoft/WSL/issues/4735#issuecomment-1522509689' target='_blank' rel='nofollow'>following</a>:
</p>
<blockquote>
<p>
I finally resolved this amiss during the following steps (Windows 10.0.19045 Build 19045):
</p>
<!-- #region -->
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id300e259480e9'><button class='copyBtn' data-clipboard-target='#id300e259480e9' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>Running wsl.exe --import Ubuntu A:\wsl\Ubuntu A:\wsl\Ubuntu.tar = > Unspecified error
Running LxRunOffline.exe install -n Ubuntu -d A:\wsl\Ubuntu -f A:\wsl\Ubuntu.tar => [ERROR] Couldn't get the value "DistributionName" of the registry key "Software\Microsoft\Windows\CurrentVersion\Lxss\AppxInstallerCache"
Removing the Software\Microsoft\Windows\CurrentVersion\Lxss\AppxInstallerCache registry key
Running LxRunOffline.exe install -n Ubuntu -d A:\wsl\Ubuntu -f A:\wsl\Ubuntu.tar => [ERROR] Couldn't get the value "DistributionName" of the registry key "Software\Microsoft\Windows\CurrentVersion\Lxss\TryStoreWSL"
Removing the Software\Microsoft\Windows\CurrentVersion\Lxss\TryStoreWSL registry key
Running LxRunOffline.exe install -n Ubuntu -d A:\wsl\Ubuntu -f A:\wsl\Ubuntu.tar => ✔
Running ubuntu.exe config --default-user arman => ✔
done 🎉</pre>
</div>
<!-- endregion -->
</blockquote>
<!-- endregion -->
<!-- #region -->
<h2 id="startStop">Starting and Stopping Portable VMs</h2>
<p>
You can start the WSL VM called <code>UbuntuMSlinn</code> by using <code>wsl -d</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide40c83c57525'><button class='copyBtn' data-clipboard-target='#ide40c83c57525' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\> </span>wsl -d UbuntuMSlinn</pre>
</div>
<p>
You can stop it by using <code>wsl -t</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>cmd.exe</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5322c0a234cd'><button class='copyBtn' data-clipboard-target='#id5322c0a234cd' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>C:\> </span>wsl -t UbuntuMSlinn</pre>
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="issues">Microsoft WSL Issue Reporting</h2>
<p>
In the course of writing this blog post I found the
<a href='https://github.com/microsoft/WSL/blob/master/CONTRIBUTING.md#8-collect-wsl-logs' target='_blank' rel='nofollow'>web page</a>
for reporting a WSL issue, so that Microsoft can look at it.
<!-- endregion -->
AI / ML System Behavior Reflects the Society That Produced It2021-12-26T00:00:00-05:00https://mslinn.github.io/blog/2021/12/26/ai-society<p>
Artificial intelligence systems, including machine learning systems, are primarily software-driven.
Like all software, AI and ML systems reflect the societies that produced them.
<a href='https://www.thoughtworks.com/insights/articles/demystifying-conways-law' target='_blank' rel='nofollow'>Conway’s Law</a>
explains how the internal organization of software reflects the organization that created it.
</p>
<p>
I postulate that the behavior of AI & ML systems reflect the society that the systems are embedded in.
Do you find the AI systems are
<a href='https://www.bruegel.org/blog-post/dark-side-artificial-intelligence-manipulation-human-behaviour' target='_blank' rel='nofollow'>instrusive or exploitive</a>?
Look in the collective mirror for the society it was created by.
</p>
<p class="alert rounded shadow">
If the populace is viewed as merely something for corporations to exploit,
and this view is accepted at enough levels in society,
that becomes the status quo.
In my opinion, the USA in 2022 has definitely reached that point.
</p>
<div class="quoteCite shadow rounded">
<h2 style="margin-top: 0">Conway’s Law</h2>
Any organization that designs a system (defined broadly)
will produce a design whose structure is a copy of the organization's communication structure.
<br><br>
– Melvin E. Conway, 1967
</div>
<div class="quoteCite shadow rounded">
<h2 style="margin-top: 0">Allan Kelly’s Corollary</h2>
Organisational design is system design.
<br><br>
– Allan Kelly, 2005
</div>
<p>
The <a href='https://www.hbs.edu/ris/Publication%20Files/16-124_7ae90679-0ce6-4d72-9e9d-828872c7af49.pdf' target='_blank' rel='nofollow'>The Mirroring Hypothesis: Theory, Evidence and Exceptions</a>
by Lyra J. Colfer and Carliss Y. Baldwin was published by the Harvard Business School in 2016.
</p>
<div class="quoteCite shadow rounded">
A large-scale system is one that stretches across time and space.
A large organization similarly stretches across time and space.
Conway’s Law ties the two.
How we organize defines how we think collectively, and thus what we make collectively...<br><br>
... To build software, first build a community...<br><br>
Software is essentially about people, and the larger-scale we make it, the more that truth becomes visible. One program reflects how one person thinks. A large-scale application reflects how many people think together.
<br><br>
– Pieter Hintjes, <a href='http://hintjens.com/blog:73' target='_blank' rel='nofollow'>Sex in Title, and Other Stories</a>, Dec 16, 2013
</div>
<p>
Do you even care?
If so, please be the change you want to see in the world.
</p>
<div class='quote'>
<div class='imgWrapper imgFlex right quartersize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/gandhi.webp" type="image/webp">
<source srcset="/blog/images/gandhi.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/gandhi.png"
style='width: 100%; '
/>
</picture>
</div>
We but mirror the world.
All the tendencies present in the outer world are to be found in the world of our body.
If we could change ourselves, the tendencies in the world would also change.
As a man changes his own nature, so does the attitude of the world change towards him.
This is the divine mystery supreme.
A wonderful thing it is and the source of our happiness.
We need not wait to see what others do.
<span class='quoteAttribution'> – From <a href='https://commonground.ca/be-the-change-you-want-to-see-in-the-world/' rel='nofollow' target='_blank'>Mahatma Gandhi</a></span>
</div>
Spring-Breezifier: Solving COVID-19 With HVAC2021-12-22T00:00:00-05:00https://mslinn.github.io/blog/2021/12/22/covid-hvac<!-- #region intro -->
<p>
When the COVID-19 pandemic began, the world was unprepared and no one knew how the virus was transmitted.
As the medical profession lurched awkwardly into action, it gave advice that later turned out to be incorrect.
As time went by, important details about how the virus is transmitted were revealed,
and the previous medical advice had become politicized.
The new advice has not yet been generally adopted as policy and is currently unknown to most people.
</p>
<div class='quote'>
<div class='quoteText clearfix'>
<div class='imgWrapper imgFlex right quartersize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/covidHvac/mencken.webp" type="image/webp">
<source srcset="/blog/images/covidHvac/mencken.png" type="image/png">
<img
class="imgImg rounded"
src="/blog/images/covidHvac/mencken.png"
style='width: 100%; '
/>
</picture>
</div>
For every complex problem there is an answer that is clear, simple, and wrong.
<br><br><br><br><br><br>
</div><div class='quoteAttribution'> – <a href='https://www.britannica.com/biography/H-L-Mencken' rel='nofollow' target='_blank'>H. L. Mencken 1880-1956.</a></div>
</div>
<!-- endregion -->
<!-- #region about -->
<h2 id="about">About This Article</h2>
<p>
This article's intent is to show how the COVID-19 pandemic could be dealt with simply, cheaply,
and with a minimum of aggravation by raising awareness of the potential role of
<a href='https://www.cdc.gov/infectioncontrol/guidelines/environmental/background/air.html#c3' target='_blank' rel='nofollow'>HVAC</a>
(heating, ventilation and air conditioning) equipment.
</p>
<p>
The author is an electrical engineer with no health-related or HVAC-related qualifications.
However, he can read and write, ideas often come to mind when presented with new information.
</p>
<div class="formalNotice rounded shadow">
After initially publishing this blog post, an old friend showed me a YouTube
video made by a consulting civil engineer who specializes in this topic.
<a href="#youtube2">You can skip to it if you are impatient</a>
and want to know in-depth technical specifics.
</div>
<p>
All the new information referenced in this article is generally available, from qualified and reputable sources,
and this article just disseminates it.
</p>
<p>
This article concludes with actionable suggestions.
</p>
<!-- endregion -->
<!-- #region update -->
<h2 id="update">Update 2023-06-08</h2>
<div class='quote'>
<p>
[The] federal agency has set a target - five air changes per hour -
for how much rooms and buildings should be ventilated...<br><br>
it’s easy to see the guidance only in the context of Covid-19,
it will help with many other airborne hazards like wildfire smoke,
allergens and other infectious diseases, such as the flu...<br><br>
“If they had broadcast and implemented these changes at the beginning, there never would have been a pandemic”,
said Kimberly Prather,
an atmospheric chemist at the University of California at San Diego and the Scripps Institution of Oceanography.
</p>
<span class='quoteAttribution'> – From <a href='https://www.cnn.com/2023/05/12/health/cdc-new-ventilation-target/index.html' rel='nofollow' target='_blank'>CDC sets first target for indoor air ventilation to prevent spread of Covid-19</a></span>
</div>
<!-- endregion -->
<!-- #region aerosols -->
<h2 id="trans">COVID-19 Is Primarily Transmitted Via Aerosols</h2>
<p>
The most significant bit of information about COVID-19,
which was not generally accepted at the beginning of the pandemic,
is that it is <a href='https://www.nature.com/articles/d41586-021-00251-4' target='_blank' rel='nofollow'>mostly transmitted via aerosols</a>;
that is, via small airborne droplets only a few microns in diameter.
(One micron is a millionth of a meter, or one twenty-five thousandth of an inch).
Aerosol particles can hang in the air for hours and travel hundreds of feet.
</p>
<p>
The Centers for Disease Control and Prevention (CDC)
officially recently officially recognized that SARS-CoV-2 (the virus that causes COVID-19) is airborne,
meaning it is highly transmissible through the air.
</p>
<div class="quoteCite shadow rounded">
<div class='imgWrapper imgFlex right' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/covidHvac/cdc.webp" type="image/webp">
<source srcset="/blog/images/covidHvac/cdc.png" type="image/png">
<img
class="imgImg "
src="/blog/images/covidHvac/cdc.png"
style='width: 100%; maxwidth; 25%; width: 130px; height: auto;'
/>
</picture>
</div>
Exposure occurs in three principal ways:
<ol>
<li>inhalation of very fine respiratory droplets and aerosol particles</li>
<li>
deposition of respiratory droplets and particles on exposed mucous membranes in the mouth,
nose, or eye by direct splashes and sprays
</li>
<li>
touching mucous membranes with hands that have been soiled either directly by virus-containing respiratory fluids
or indirectly by touching surfaces with virus on them.
</li>
</ol>
<p>
People release respiratory fluids during exhalation
(e.g., quiet breathing, speaking, singing, exercise, coughing, sneezing)
in the form of droplets across a spectrum of sizes.
These droplets carry virus and transmit infection.
To stay healthy, avoid these droplets.
</p>
<p>
The largest droplets settle out of the air rapidly, within seconds to minutes.
The smallest very fine droplets, and aerosol particles formed when these fine droplets rapidly dry,
are small enough that they can remain suspended in the air for minutes to hours.
</p>
–From the CDC <a href='https://www.cdc.gov/coronavirus/2019-ncov/science/science-briefs/sars-cov-2-transmission.html' target='_blank' rel='nofollow'>Scientific Brief: SARS-CoV-2 Transmission</a>, Updated May 7, 2021.
</div>
<p>
The idea that physical distancing of 6 feet (2 meters) might help keep people healthy is
an example of bad science from a study
<a href='https://www.businessinsider.com/6-foot-distancing-rule-is-outdated-oxford-mit-new-system-2020-8' target='_blank' rel='nofollow'>80 years ago</a>
that was not debunked until recently.
</p>
<div class="quoteCite shadow rounded">
<div class='imgWrapper imgFlex right quartersize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/covidHvac/epaLogo.webp" type="image/webp">
<source srcset="/blog/images/covidHvac/epaLogo.png" type="image/png">
<img
class="imgImg "
src="/blog/images/covidHvac/epaLogo.png"
style='width: 100%; '
/>
</picture>
</div>
Transmission of COVID-19 from inhalation of virus in the air can occur at distances greater than six feet.
Particles from an infected person can move throughout an entire room or indoor space.
The particles can also linger in the air after a person has left the room – they can remain airborne for hours in some cases.
<br><br>
–From the US Environmental Protection Agency (EPA):
<a href='https://www.epa.gov/coronavirus/indoor-air-and-coronavirus-covid-19' target='_blank' rel='nofollow'>Indoor Air and Coronavirus (COVID-19)</a>, web page was updated December 15, 2021.
</div>
<p>
Distance between people is not a significant transmissibility factor.
Imagine two people, back to back, one (downwind) very sick with COVID-19, and the other (healthy) upwind, and the wind is blowing at 20 mph (32 km/h).
The sick person will not infect the healthy person, unless they exchange positions.
Just control the air that is breathed, and all airborne viruses such as COVID-19 will be controled.
</p>
<div class="quoteCite shadow rounded">
<div class='imgWrapper imgFlex right' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/covidHvac/ucsdMedicine.webp" type="image/webp">
<source srcset="/blog/images/covidHvac/ucsdMedicine.png" type="image/png">
<img
class="imgImg "
src="/blog/images/covidHvac/ucsdMedicine.png"
style='width: 100%; maxwidth; 25%; width: 130px; height: auto;'
/>
</picture>
</div>
What we learned during the pandemic is that aerosols were one of the main drivers in spreading the COVID-19 virus and that
their importance in the transmission of many other respiratory pathogens has been systematically underappreciated.
<br><br>
– Dr. Robert Schooley, Professor in the Department of Medicine at the University of California San Diego (November 22, 2021),
quoted from
<a href='https://ucsdnews.ucsd.edu/pressrelease/covid-gets-airborne' target='_blank' rel='nofollow'>COVID Gets Airborne – UC San Diego develops computer model to aid understanding of how viruses travel through the air</a>
</div>
<!-- endregion -->
<!-- #region controlling virus infiltration -->
<h2 id="virus">Controlling Virus Infiltration</h2>
<p>
We would do well to remember that the COVID-19 virus causes sickness.
Without exposure to the virus people would not get sick.
The risk that a person might catch the disease is directly related to the number of viruses inhaled per unit of time.
For example, the more viruses someone inhales in an hour, the more likely they will get sick.
</p>
<p>
<a href='https://www.statnews.com/2020/04/14/how-much-of-the-coronavirus-does-it-take-to-make-you-sick/' target='_blank' rel='nofollow'>The amount of virus necessary to make a person sick is called the infectious dose.</a>
All the countermeasures that have been used (hand washing, physical distancing, masks, curfews, etc.)
are intended to reduce the number of viruses people are exposed to per unit time.
Pandemic policy treats countermeasures as proxies for virus transmission vectors.
By not updating pandemic policy as new information becomes available, we loose the ability to control the spread of the virus.
</p>
<!-- endregion -->
<!-- #region hepa -->
<h2 id="hepa">HEPA Filters</h2>
<p>
HEPA filters on HVAC equipment are designed to remove these aerosols.
They come in a huge variety of sizes, shapes and air flow capacities.
If you could just breathe the pure air emitted from a fan that had a HEPA filter,
you would not get sick from any airborne virus.
Any number of people could gather safely,
if each of them received an adequate supply of pure air in their face at all times.
</p>
<div class="quoteCite shadow rounded">
<div class='imgWrapper imgFlex right quartersize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/covidHvac/epaLogo.webp" type="image/webp">
<source srcset="/blog/images/covidHvac/epaLogo.png" type="image/png">
<img
class="imgImg "
src="/blog/images/covidHvac/epaLogo.png"
style='width: 100%; '
/>
</picture>
</div>
HEPA is a type of pleated mechanical air filter.
It is an acronym for “high efficiency particulate air filter” (as officially defined by the U.S. Dept. of Energy).
This type of air filter can theoretically remove at least 99.97% of dust, pollen, mold, bacteria,
and any airborne particles with a size of 0.3 microns (µm).
<br><br>
–<a href='https://www.epa.gov/indoor-air-quality-iaq/what-hepa-filter-1' target='_blank' rel='nofollow'>US Environment Protection Agency</a>
</div>
<p>
HEPA filters are inexpensive and are readily available.
They are not new.
In fact,
<a href='https://www.apcfilters.com/the-history-of-hepa-filters/' target='_blank' rel='nofollow'>HEPA filter technology was created in the 1940s</a>
by the US Army Chemical Corps and National Defense Research Committee as part of the Manhattan Project.
HEPA technology was declassified after World War II and became available for commercial and personal use.
HEPA filters play a key role in the research and development of modern pharmaceuticals,
aerospace engineering, and computer chip manufacturing.
</p>
<div class="quoteCite shadow rounded">
HEPA air cleaners, which remain little-used in Canadian hospitals,
are a cheap and easy way to reduce risk from airborne pathogens.
<br><br>
–From Nature Magazine, October 6, 2021:
<a href='https://www.nature.com/articles/d41586-021-02669-2' target='_blank' rel='nofollow'>Real-world data show that filters clean COVID-causing virus from air – An inexpensive type of portable filter efficiently screened SARS-CoV-2 and other disease-causing organisms from hospital air.</a>
</div>
<p>
It is easy to attach a HEPA filter to a fan, or to replace an existing HVAC filter with a HEPA filter.
Here is a very <a href='https://www.jefftk.com/p/ceiling-air-purifier' target='_blank' rel='nofollow'>scientific approach to a home-made solution</a>.
</p>
<div class='imgWrapper imgBlock inline fullsize' style=' '>
<figure>
<a href='https://youtu.be/kH5APw_SLUU?t=106' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/covidHvac/hepaFan.webp" type="image/webp">
<source srcset="/blog/images/covidHvac/hepaFan.png" type="image/png">
<img alt='Dr. Jeffrey E. Terrell, director of the Michigan Sinus Center,
demonstrates how to build an air purifier with a HEPA filter for about $25 with parts from your local hardware store.'
class="imgImg rounded shadow"
src="/blog/images/covidHvac/hepaFan.png"
style='width: 100%; '
title='Dr. Jeffrey E. Terrell, director of the Michigan Sinus Center,
demonstrates how to build an air purifier with a HEPA filter for about $25 with parts from your local hardware store.'
/>
</picture>
</a>
<figcaption class='imgFigCaption fullsize'>
<a href="https://youtu.be/kH5APw_SLUU?t=106" target='_blank' >
Dr. Jeffrey E. Terrell, director of the Michigan Sinus Center,
demonstrates how to build an air purifier with a HEPA filter for about $25 with parts from your local hardware store.
</a>
</figcaption>
</figure>
</div>
<p>
One of the comments on the above video, from Rick Rude in 2014, was:
</p>
<div class="quoteCite shadow rounded">
Better to pull air thru the filter,
if you push air through the filter dust that builds up will get blown off and go back into the room.
Also, if the fan is pulling air through, the filter will stick to the fan and won't need as much tape to hold it on.
Always handle used filters with care because handling them rough will release concentrated dust back into your air.
If possible, always take air cleaners or vacuum cleaners outside to change the filters or bags.
</div>
<!-- endregion -->
<!-- #region merv filters -->
<h2 id="merv">MERV Filters</h2>
<p>
MERV is a graduated standard; MERV 7 is the minimum standard for furnaces,
while MERV 13 is the highest.
</p>
<p>
MERV 13 filters are more efficient at removing large particles from the air,
while HEPA filters are more efficient at removing small particles from the air.
MERV 13 filters can remove up to 99.97% of particles from the air,
while HEPA filters can remove up to 99.99% of particles from the air.
</p>
<p>
<a href='https://engineering.ucdavis.edu/news/science-action-how-build-corsi-rosenthal-box' target='_blank' rel='nofollow'>How to Build a Corsi-Rosenthal Box</a>.
</p>
<!-- endregion -->
<!-- #region youtube2 -->
<h2 id="youtube2">How to Identify and Rectify Poorly Ventilated Indoor Spaces Using Engineering Controls</h2>
<p>
<a href='https://www.linkedin.com/in/elfstrom/' target='_blank' rel='nofollow'>David Elfstrom, P. Eng.</a>,
is a independent civil engineer based in Simcoe, Ontario who consults
on the efficient use of energy in buildings.
During the pandemic he has been drawing attention to the importance of ventilation,
filtration, and overall indoor environmental quality.
Earlier this year Mr. Elfstrom completed a ventilation and air pathways assessment
of an apartment building in outbreak for a public health unit.
This presentation was part of Passive Buildings Canada's 2021 Annual General Meeting.
</p>
<style>.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; } .embed-container iframe, .embed-container object, .embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }</style><div class='embed-container'> <iframe title="YouTube video player" width="640" height="390" src="//www.youtube.com/embed/9J96F32tv1s" frameborder="0" allowfullscreen></iframe></div>
<p style="margin-top: 1em;">
Mr. Elfstrom also co-authored the
<a href='https://docs.google.com/document/d/17tKk8Da8tnchtnp9ZRe7fPazGAmXtvoA-n4GZcY0_fQ/edit' target='_blank' rel='nofollow'>Masks4Canada Room Ventilation/Filtration Guide and Tip Sheet</a>.
</p>
<!-- endregion -->
<!-- #region N95 -->
<h2 id="hvac">N95, KF94, FFP2, and 9152 Masks and Their Cousins</h2>
<p>
If you need to move about in a crowd, or enter a space that contained people a few hours ago,
you need to wear a properly fitting face mask that filters out the tiny aerosol particles that contain the COVID-19 virus.
<a href='https://www.travelawaits.com/2559161/n95-vs-kn95-vs-kf94-masks-for-travel/' target='_blank' rel='nofollow'>N95 masks and their cousins</a>
(KN95, KF94, etc.) do the job because they filter particles as small as 0.3 microns.
This is similar to the particle size filtered by HEPA filters.
Caution: KN95 is a self-reported test standard, and lacks strict government regulation by China,
resulting in many underperforming and often flat-out fake masks.
</p>
<p>
The US National Personal Protective Technology Laboratory (NPPTL),
which is part of the US National Institute for Occupational Safety and Health (NIOSH),
which itself is part of the CDC,
evaluated various masks and published the results as
<a href='https://www.cdc.gov/niosh/npptl/respirators/testing/NonNIOSHresults.html' target='_blank' rel='nofollow'>NPPTL Respirator Assessments to Support the COVID-19 Response</a>.
</p>
<p>
The following types of masks do <b>not</b> reliably filter tiny aerosols,
so they do not adequately protect you from COVID-19 variants such as Omicron:
</p>
<ol>
<li>
<a href='https://www.theguardian.com/commentisfree/2021/dec/27/best-masks-covid-tests-cloth-surgical-respirators' target='_blank' rel='nofollow'>Surgical masks</a>
(what most people wear these days)
</li>
<li>Masks with activated charcoal</li>
<li>Vented masks</li>
<li>Cloth masks</li>
<li>Gaiters</li>
<li>Ill-fitting masks that do not cover and seal the mouth and nose</li>
</ol>
<iframe width="690" height="388" src="https://www.youtube.com/embed/WE5Uo3F2TdU"
frameborder="0" allowfullscreen class="rounded shadow liImg"></iframe>
<!-- endregion -->
<!-- #region inoculations and pills -->
<h2 id="other">Inoculations and Pills</h2>
<div class='imgWrapper imgFlex right quartersize' style=' '>
<a href='https://www.reuters.com/business/healthcare-pharmaceuticals/us-fda-set-authorize-pfizer-merck-covid-19-pills-this-week-bloomberg-news-2021-12-21/' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/covidHvac/covidPills.webp" type="image/webp">
<source srcset="/blog/images/covidHvac/covidPills.png" type="image/png">
<img alt='COVID-19 pills'
class="imgImg rounded shadow"
src="/blog/images/covidHvac/covidPills.png"
style='width: 100%; '
title='COVID-19 pills'
/>
</picture>
</a>
</div>
<p>
Inoculations, including boosters, have proven to be very helpful.
Pills from Pfizer Inc. and Merck & Co. for people sick with COVID-19 will also be very helpful once they become available.
However, pills will only help sick people get better, they will not prevent getting sick.
</p>
<p>
Until everyone in the entire world has somehow acquired an effective level of antibodies,
COVID-19 will continue to mutate.
</p>
<!-- endregion -->
<!-- #region the only solution -->
<h2 id="go">The Only Solution Available Today</h2>
<p>
Inoculations and masks are good, but they are not perfect preventative measures against COVID-19.
Protection against infection is not 100%.
At present, the only preventative measure against COVID-19 that would allow people to safely mingle in person
would be to guarantee continuous streams of pure air directed individually at each person's face.
</p>
<p>
This measure would also prevent the spread of
<a href='https://bmcinfectdis.biomedcentral.com/articles/10.1186/s12879-019-3707-y' target='_blank' rel='nofollow'>all other diseases and their variants that are primarily spread via aerosols</a>,
including coronoviruses, colds, flus, tuberculosis, MERS-CoV, measles, ebola and chickenpox.
Pollen, dust and other particulates would also be removed, so athsma sufffers would feel relief from extended periods breathing pure air.
</p>
<p>
The solution to living with COVID-19 is simple, safe, inexpensive and is not disruptive:
</p>
<div class="formalNotice rounded shadow">
<h2 class="centered" style="margin: 0; padding: 0">Live your social life with a pure breeze in your face.</h2>
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/covidHvac/breeze-dandelion.webp" type="image/webp">
<source srcset="/blog/images/covidHvac/breeze-dandelion.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/covidHvac/breeze-dandelion.png"
style='width: 100%; '
/>
</picture>
</div>
<div style="clear:both; font-size: 0"></div>
</div>
<p>
Life as we knew it before the pandemic started 2 years ago could resume, mostly.
</p>
<!-- endregion -->
<!-- #region hvac upgrades -->
<h2 id="hvac">HVAC Upgrades Are Key</h2>
<p>
The engineering group that encompasses HVAC is ASHRAE.
ASHRAE was formed as the American Society of Heating, Refrigerating and Air-Conditioning Engineers
by the merger in 1959 of American Society of Heating and Air-Conditioning Engineers (ASHAE), founded in 1894 and
The American Society of Refrigerating Engineers (ASRE), founded in 1904.
</p>
<p>
ASHRAE offers <a href='https://www.ashrae.org/file%20library/technical%20resources/covid-19/core-recommendations-for-reducing-airborne-infectious-aerosol-exposure.pdf' target='_blank' rel='nofollow'>Core Recommendations for Reducing Airborne Infectious Aerosol Exposure</a>.
<a href='https://www.iso-aire.com/blog/what-are-the-differences-between-a-merv-13-and-a-hepa-filter' target='_blank' rel='nofollow'>MERV 13 or better</a>
levels of performance are recommended, and HEPA filters surpass that specifation.
In fact, all HEPA filters have a rating of a MERV 17 or higher.
</p>
<!-- endregion -->
<!-- #region entrepreneurs -->
<h2 id="entre">Entrepreneurs</h2>
<p>
This represents an opportunity for entrepreneurs.
</p>
<!-- #region restaurants -->
<h3 id="restos">Restaurants</h2>
<p>
Imagine a restaurant that stays open with near-normal seating capacity
because each table is equipped with a fan and ducting that blows a gentle breeze of pure air directly into the face of every patron.
The air would recirculate within the restaurant, so there would be no need to upgrade the existing HVAC system
because the HEPA filters located at each table would continuously clean the air within the restaurant.
Those filters would need to be cleaned and disinfected daily.
The wait staff would all wear N95 or similar masks, however each cashier could instead enjoy their own gentle breeeze of pure air.
</p>
<p>
Cost to equip each seat in the restaurant could be less than $50.
For do-it-yourself owners, the cost could approach $10 per seat.
For example, a 100-seat restaurant might be able to retrofit 75 of those seats at a cost of less than $3750.
They would never have to close again due to any pandemic caused by airborne aerosols...
provided, of course, that local regulations recognized this approach.
</p>
<!-- endregion -->
<!-- #region retail -->
<h3 id="stores">Retail Stores and Office Buildings</h2>
<p>
Enclosing sales clerks in stores behind clear plastic walls is wrong because it decreases air circulation.
Instead, each those staff members should have a dedicated ducted fan that blows a gentle breeze of pure air into their face at all times.
</p>
<p>
Similarly, queues of people awaiting their turn at checking out should have fans blowing pure air at their heads.
The air would recirculate within the store, there would be no need to upgrade the existing HVAC system
because the HEPA filters would continuously clean the air within the building.
</p>
<!-- endregion -->
<!-- #region spring-breezifier -->
<h2 id="domain">Spring-Breezifier Is Offered Into the Public Domain</h2>
<p>
I offer this idea to the world; I am an idea machine so I cannot act on most of them.
Just for fun, let's call this idea the <i>Spring-Breezifier</i>.
</p>
<p>
Designing and building Spring-Breezifiers seems like it might be a good high school or youth group project.
HVAC installers in particular should be able to make short work of this idea;
go ahead and make lots of money providing pure airflows by building custom Spring-Breezifiers for your customers,
we all thank you!
</p>
<p>
If anyone would like to talk to me about their Spring-Breezifier project, I would be happy to speak with them.
</p>
<!-- endregion -->
<!-- #region next steps -->
<h2 id="next">Next Steps</h2>
<ol>
<li>
Medical HVAC specialists could suggest the necessary cubic feet per minute of air required per person
sufficient to guarantee a continuous stream of pure air,
and provide guidelines for designing ductwork to direct that air effectively.
</li>
<li>
Build and test prototypes.
Most of the effort will be in designing and building the ducting and/or the scaffolding.
Adding inner baffles would raise the cost and weight, lower the noise and make the airflow less turbulent.
Perhaps Spring-Breezifiers could be built entirely from off-the-shelf parts for some or even most installations.
Large-format 3D printers for special circumstances might be helpful.
</li>
<li>Propose the inclusion of pure air streams into public spaces as a matter of public health policy.</li>
</ol>
<!-- endregion -->
<!-- endregion -->
Disappointing Scala 3 Installation Experience2021-05-19T00:00:00-04:00https://mslinn.github.io/blog/2021/05/19/installing-scala-3.0<p>
I run <a href='https://scalacourses.com' target='_blank' rel='nofollow'>ScalaCourses.com</a>, an online Scala training web site.
After years of hype, Scala 3 is now available.
</p>
<h2 id="prime">Scala 3: Not Yet Ready for Production</h2>
<p>
<a href='https://www.infoq.com/news/2021/03/scala3/' target='_blank' rel='nofollow'>Scala 3</a> was eight years in the making.
You would never know that from the horrible installation process and the disappointing installation instructions.
</p>
<p>
It is going to take quite a while before <a href='https://scalatimes.com/d374aea433' target='_blank' rel='nofollow'>Scala 3</a>,
which was known as Dotty before it was released, can be trusted in production.
According to the <a href='https://github.com/lampepfl/dotty' target='_blank' rel='nofollow'>Dotty GitHub project</a>,
the only published future milestone is <a href='https://github.com/lampepfl/dotty/milestones' target='_blank' rel='nofollow'>v3.1.0, which has no due date</a>.
Given that Scala 3 uses an entirely new build process, and an entirely new (and nonstandard) installation process,
and that the internals of the Scala compiler were almost completely replaced,
I doubt that version will be stable enough for use on production projects.
</p>
<h2 id="choices">Installation Choices</h2>
<p>
Installation cholices include:
</p>
<ul>
<li>Command-line Scala has limited use cases, but is nice to have around for occassional experimentation.</li>
<li>
<a href='https://www.scala-lang.org/blog/2021/04/08/scala-3-in-sbt.html' target='_blank' rel='nofollow'>SBT</a>
(which features an enhanced Scala REPL) is very helpful for interactively developing code, as well as for building and testing.
</li>
<li>
<a href='https://www.jetbrains.com/help/idea/discover-intellij-idea-for-scala.html' target='_blank' rel='nofollow'>IntelliJ</a>
provides the best Scala coding productivity and code quality.
</li>
<li>
<a href='https://shunsvineyard.info/2020/11/20/setting-up-vs-code-for-scala-development-on-wsl/' target='_blank' rel='nofollow'>VSCode</a>
has been playing catch-up but is not full-featured yet.
</li>
</ul>
<h2 id="install">Installation Transcript</h2>
<p>
The following is the transcript of how I installed command-line Scala on Ubuntu 20.10 running under WSL2.
</p>
<h3 class="numbered" id="remove_scala2">Remove Scala 2</h3>
<p>
This step is not required.
Scala 2 and Scala 3 can easily co-exist on the same system because their names are different.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id03ad0128cb1e'><button class='copyBtn' data-clipboard-target='#id03ad0128cb1e' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo apt remove scala
<span class='unselectable'>Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages will be REMOVED:
scala
0 upgraded, 0 newly installed, 1 to remove and 0 not upgraded.
After this operation, 666 MB disk space will be freed.
Do you want to continue? [Y/n]
(Reading database ... 236024 files and directories currently installed.)
Removing scala (2.13.4-400) ...
Processing triggers for man-db (2.9.3-2) ... </span></pre>
</div>
<h3 class="numbered" id="cs">Install Coursier</h3>
<p>
Coursier is a multithreaded downloader for project dependencies, and it now also downloads Scala 3.
SBT uses coursier internally.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idca95b68982c4'><button class='copyBtn' data-clipboard-target='#idca95b68982c4' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl -fLo cs https://git.io/coursier-cli-"$(uname | tr LD ld)"
<span class='unselectable'> % Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
100 144 100 144 0 0 285 0 --:--:-- --:--:-- --:--:-- 6000
100 57.1M 100 57.1M 0 0 3656k 0 0:00:15 0:00:15 --:--:-- 4092k </span>
<span class='unselectable'>$ </span>mv cs ~/.local/bin/
<span class='unselectable'>$ </span>chmod a+x ~/.local/bin/cs
<span class='unselectable'>$ </span>cs install cs
<span class='unselectable'>https://repo1.maven.org/maven2/io/get-coursier/apps/maven-metadata.xml
100.0% [##########] 1.8 KiB (8.5 KiB / s)
https://repo1.maven.org/maven2/io/get-coursier/coursier-cli_2.12/maven-metadata.xml
No new update since 2021-03-23 14:35:16
Wrote cs
Warning: /home/mslinn/.local/share/coursier/bin is not in your PATH
To fix that, add the following line to ~/.bashrc
export PATH="$PATH:/home/mslinn/.local/share/coursier/bin" </span>
<span class='unselectable'>$ </span> export PATH="$PATH:/home/mslinn/.local/share/coursier/bin"</pre>
</div>
<p>
Now let's test Coursier:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc20ff41ff3a2'><button class='copyBtn' data-clipboard-target='#idc20ff41ff3a2' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cs
<span class='unselectable'>Coursier 2.0.16
Usage: cs [options] [command] [command-options]
Available commands: bootstrap, channel, complete, fetch, get, install, java, java-home, launch, list, publish, resolve, setup, uninstall, update, search
Type cs command --help for help on an individual command </span></pre>
</div>
<p>
This is the Coursier help message for the <code>install</code> subcommand:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb627bfd12fea'><button class='copyBtn' data-clipboard-target='#idb627bfd12fea' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cs install --help
<span class='unselectable'>Command: install
Usage: cs install
--cache <string?>
Cache directory (defaults to environment variable COURSIER_CACHE, or ~/.cache/coursier/v1 on Linux and ~/Library/Caches/Coursier/v1 on Mac)
--mode | -m <offline|update-changing|update|missing|force>
Download mode (default: missing, that is fetch things missing from cache)
--ttl | -l <duration>
TTL duration (e.g. "24 hours")
--parallel | -n <int>
Maximum number of parallel downloads (default: 6)
--checksum <checksum1,checksum2,...>
Checksum types to check - end with none to allow for no checksum validation if no checksum is available, example: SHA-256,SHA-1,none
--retry-count <int>
Retry limit for Checksum error when fetching a file
--cache-file-artifacts | --cfa <bool>
Flag that specifies if a local artifact should be cached.
--follow-http-to-https-redirect <bool>
Whether to follow http to https redirections
--credentials <host(realm) user:pass|host user:pass>
Credentials to be used when fetching metadata or artifacts. Specify multiple times to pass multiple credentials. Alternatively, use the COURSIER_CREDENTIALS environment variable
--credential-file <string*>
Path to credential files to read credentials from
--use-env-credentials <bool>
Whether to read credentials from COURSIER_CREDENTIALS (env) or coursier.credentials (Java property), along those passed with --credentials and --credential-file
--quiet | -q <counter>
Quiet output
--verbose | -v <counter>
Increase verbosity (specify several times to increase more)
--progress | -P <bool>
Force display of progress bars
--log-changing <bool>
Log changing artifacts
--log-channel-version | --log-index-version | --log-jvm-index-version <bool>
Log app channel or JVM index version
--graalvm-home <string?>
--graalvm-option <string*>
--graalvm-default-version <string?>
--install-dir | --dir <string?>
--install-platform <string?>
Platform for prebuilt binaries (e.g. "x86_64-pc-linux", "x86_64-apple-darwin", "x86_64-pc-win32")
--install-prefer-prebuilt <bool>
--only-prebuilt <bool>
Require prebuilt artifacts for native applications, don't try to build native executable ourselves
--repository | -r <maven|sonatype:$repo|ivy2local|bintray:$org/$repo|bintray-ivy:$org/$repo|typesafe:ivy-$repo|typesafe:$repo|sbt-plugin:$repo|ivy:$pattern>
Repository - for multiple repositories, separate with comma and/or add this option multiple times (e.g. -r central,ivy2local -r sonatype:snapshots, or equivalently -r central,ivy2local,sonatype:snapshots)
--default-repositories <bool>
--proguarded <bool?>
--channel <org:name>
Channel for apps
--default-channels <bool>
Add default channels
--contrib <bool>
Add contrib channel
--file-channels <bool>
Add channels read from the configuration directory
--jvm <string?>
--jvm-dir <string?>
--system-jvm <bool?>
--local-only <bool>
--update <bool>
--jvm-index <string?>
--repository | -r <maven|sonatype:$repo|ivy2local|bintray:$org/$repo|bintray-ivy:$org/$repo|typesafe:ivy-$repo|typesafe:$repo|sbt-plugin:$repo|scala-integration|scala-nightlies|ivy:$pattern|jitpack|clojars|jcenter|apache:$repo>
Repository - for multiple repositories, separate with comma and/or add this option multiple times (e.g. -r central,ivy2local -r sonatype:snapshots, or equivalently -r central,ivy2local,sonatype:snapshots)
--no-default <bool>
Do not add default repositories (~/.ivy2/local, and Central)
--sbt-plugin-hack <bool>
Modify names in Maven repository paths for sbt plugins
--drop-info-attr <bool>
Drop module attributes starting with 'info.' - these are sometimes used by projects built with sbt
--channel <org:name>
Channel for apps
--default-channels <bool>
Add default channels
--contrib <bool>
Add contrib channel
--file-channels <bool>
Add channels read from the configuration directory
--repository | -r <maven|sonatype:$repo|ivy2local|bintray:$org/$repo|bintray-ivy:$org/$repo|typesafe:ivy-$repo|typesafe:$repo|sbt-plugin:$repo|scala-integration|scala-nightlies|ivy:$pattern|jitpack|clojars|jcenter|apache:$repo>
Repository - for multiple repositories, separate with comma and/or add this option multiple times (e.g. -r central,ivy2local -r sonatype:snapshots, or equivalently -r central,ivy2local,sonatype:snapshots)
--no-default <bool>
Do not add default repositories (~/.ivy2/local, and Central)
--sbt-plugin-hack <bool>
Modify names in Maven repository paths for sbt plugins
--drop-info-attr <bool>
Drop module attributes starting with 'info.' - these are sometimes used by projects built with sbt
--channel <org:name>
Channel for apps
--default-channels <bool>
Add default channels
--contrib <bool>
Add contrib channel
--file-channels <bool>
Add channels read from the configuration directory
--env <bool>
--disable-env | --disable <bool>
--setup <bool>
--user-home <string?>
--add-channel <string*>
(deprecated)
--force | -f <bool> </span></pre>
</div>
<h3 class="numbered" id="s3">Install Scala 3</h3>
<p>
The Scala 3 compiler and REPL are separate programs:
<code>scala3-compiler</code> and <code>scala3-repl</code>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id499a7e0d8290'><button class='copyBtn' data-clipboard-target='#id499a7e0d8290' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>cs install scala3-compiler
<span class='unselectable'>https://repo1.maven.org/maven2/org/scala-lang/scala3-compiler_3/3.0.0/scala3-compiler_3-3.0.0.pom
100.0% [##########] 4.8 KiB (79.7 KiB / s)
https://repo1.maven.org/maven2/org/scala-lang/tasty-core_3/3.0.0/tasty-core_3-3.0.0.pom
100.0% [##########] 3.5 KiB (69.5 KiB / s)
https://repo1.maven.org/maven2/org/scala-lang/scala3-library_3/3.0.0/scala3-library_3-3.0.0.pom
100.0% [##########] 3.6 KiB (53.9 KiB / s)
https://repo1.maven.org/maven2/org/scala-lang/scala3-interfaces/3.0.0/scala3-interfaces-3.0.0.pom
100.0% [##########] 3.4 KiB (65.9 KiB / s)
https://repo1.maven.org/maven2/org/scala-lang/scala3-interfaces/3.0.0/scala3-interfaces-3.0.0.jar
100.0% [##########] 3.4 KiB (113.9 KiB / s)
https://repo1.maven.org/maven2/org/scala-lang/tasty-core_3/3.0.0/tasty-core_3-3.0.0.jar
100.0% [##########] 71.9 KiB (192.7 KiB / s)
https://repo1.maven.org/maven2/org/scala-lang/scala3-library_3/3.0.0/scala3-library_3-3.0.0.jar
100.0% [##########] 1.1 MiB (1.8 MiB / s)
https://repo1.maven.org/maven2/org/scala-lang/scala3-compiler_3/3.0.0/scala3-compiler_3-3.0.0.jar
100.0% [##########] 14.7 MiB (3.7 MiB / s)
Wrote scala3-compiler </span>
<span class='unselectable'>$ </span>cs install scala3-repl
<span class='unselectable'>https://repo1.maven.org/maven2/io/get-coursier/apps/maven-metadata.xml
No new update since 2021-05-14 04:42:19
Wrote scala3-repl </span></pre>
</div>
<h3 class="numbered" id="repl3">Run Scala 3 REPL</h3>
<p>
The part is easy!
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id749fecb6e316'><button class='copyBtn' data-clipboard-target='#id749fecb6e316' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>scala3-repl --version
<span class='unselectable'>Scala code runner version 3.0.0 -- Copyright 2002-2021, LAMP/EPFL </span>
<span class='unselectable'>$ </span>scala3-repl
<span class='unselectable'>scala> </span></pre>
</div>
<h2 id="sbt">Easily Run Scala REPL With SBT</h2>
<p>
If you do not mind directories called <code>project/</code> and <code>target/</code> being created in your current directory,
and you have already <a href='https://www.scala-sbt.org/download.html' target='_blank' rel='nofollow'>installed sbt</a>,
you can get a REPL powered by Scala 3 like this:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8e1b574817e0'><button class='copyBtn' data-clipboard-target='#id8e1b574817e0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sbt "-Dsbt.version=1.5.2" ++3.0.0! console
<span class='unselectable'>[info] welcome to sbt 1.5.2 (Ubuntu Java 11.0.11)
[info] loading global plugins from /home/mslinn/.sbt/1.0/plugins
[info] loading project definition from /var/work/ancientWarmth/ancientWarmth/project
[info] set current project to ancientwarmth (in build file:/var/work/ancientWarmth/ancientWarmth/)
[info] Forcing Scala version to 3.0.0 on all projects.
[info] Reapplying settings...
[info] set current project to ancientwarmth (in build file:/var/work/ancientWarmth/ancientWarmth/)
[info] Updating
[info] Resolved dependencies
[info] Updating
https://repo1.maven.org/maven2/org/scala-lang/scaladoc_3/3.0.0/scaladoc_3-3.0.0.pom
100.0% [##########] 6.1 KiB (82.6 KiB / s)
https://repo1.maven.org/maven2/org/scala-lang/scala3-tasty-inspector_3/3.0.0/scala3-tasty-inspector_3-3.0.0.pom
100.0% [##########] 3.6 KiB (80.8 KiB / s)
[info] Resolved dependencies
[info] Fetching artifacts of
[info] Fetched artifacts of
[info] Fetching artifacts of
https://repo1.maven.org/maven2/org/scala-lang/scala3-tasty-inspector_3/3.0.0/scala3-tasty-inspector_3-3.0.0.jar
100.0% [##########] 16.6 KiB (338.1 KiB / s)
https://repo1.maven.org/maven2/org/scala-lang/scaladoc_3/3.0.0/scaladoc_3-3.0.0.jar
100.0% [##########] 1.5 MiB (3.1 MiB / s)
[info] Fetched artifacts of
scala> </span></pre>
</div>
<p>
Thanks to <a href='https://twitter.com/renghenKornel/status/1395684928440791040' target='_blank' rel='nofollow'>@renghen</a> for this tip.
</p>
<h2 id="sc">ScalaCourses</h2>
<p>
If you want to learn how to work effectively with Scala for functional and object-oriented programming,
<a href='https://scalacourses.com' target='_blank' rel='nofollow'>ScalaCourses.com</a> is your best option.
The course material is suitable for Scala 2 and Scala 3.
Visit ScalaCourses.com to learn how to become a proficient Scala programmer.
</p>OCI / Docker / AWS Lambda / Django / Buildah / podman2021-04-29T00:00:00-04:00https://mslinn.github.io/blog/2021/04/29/buildah-podman-python-lambda<style>
body {
counter-reset: pcounter;
}
p.count:before {
counter-increment: pcounter;
content: counter(pcounter) ")\A0";
}
</style>
<!-- #region -->
<p>
This blog post is a work in progress.
Some of it may be incorrect, and some thoughts might lead nowhere.
I am publicly posting it in this state so I can discuss it with others.
This post will be improved as information becomes available.
</p>
<h2 id="goal">Goal</h2>
<div class='imgWrapper imgFlex right' style=' '>
<a href='https://podman.io' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/buildahPodman/podman-logo-crop.webp" type="image/webp">
<source srcset="/blog/images/buildahPodman/podman-logo-crop.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/buildahPodman/podman-logo-crop.png"
style='width: 100%; padding: 1em; height: 191px; width: auto;'
/>
</picture>
</a>
</div>
<div class='imgWrapper imgFlex right' style=' '>
<a href='https://buildah.io/' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/buildahPodman/buildah-logo-crop.webp" type="image/webp">
<source srcset="/blog/images/buildahPodman/buildah-logo-crop.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/buildahPodman/buildah-logo-crop.png"
style='width: 100%; padding: 0.73em; height: 191px; width: auto;'
/>
</picture>
</a>
</div>
<p>
<a href='/blog/2021/04/28/buildah-podman.html'>As previously discussed</a>,
Buildah is a drop-in replacement for using <code>docker build</code> and a <code>Dockerfile</code>.
Buildah’s <code>build-using-dockerfile</code>, or <code>bud</code> argument makes it behave just like <code>docker build</code> does.
</p>
<p>
The goal of this blog post is to use Buildah / <code>podman</code> to create an Open Container Initiative (OCI) container image with a Django app,
including the Python 3.8 runtime installed.
The Django app will start when the container is created.
The code for the Django app will be stored on the local machine where its source code can be edited,
and it will be mapped into the container from the host system.
Changes made to the code from the host system will be immediately visible inside the container.
</p>
<h2 id="todo">TODO</h2>
<p class="count">
Background: AWS publishes <a href='https://docs.aws.amazon.com/lambda/latest/dg/python-image.html' target='_blank' rel='nofollow'>Deploying Python with an AWS base image</a>,
but that does not discuss running or testing.
<a href='https://docs.aws.amazon.com/lambda/latest/dg/getting-started-create-function.html' target='_blank' rel='nofollow'>Create a Lambda function with the console</a>
is a more complete article, but is focused on using the web browser console, using Docker, and Node.js.
So many differences from the desired goal make the articles difficult to translate to AWS CLI, Buildah / <code>podman</code> and Python.
</p>
<p class="count">
Talk about the <a href='https://github.com/aws/aws-lambda-runtime-interface-emulator' target='_blank' rel='nofollow'>AWS Lambda Runtime Interface Emulator</a>,
compare and contrast with the
<a href='https://pypi.org/project/awslambdaric/' target='_blank' rel='nofollow'>AWS Lambda Python Runtime Interface Client</a>.
</p>
<p class="count">
Compare these <a href='https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html' target='_blank' rel='nofollow'>AWS Lambda Runtimes</a> with other, equivalant runtimes.
</p>
<p class="count">
OCI images are swapped in when AWS Lambda is invoked.
Do larger images cost more to use?
If so, discuss.
</p>
<!-- endregion -->
<!-- #region -->
<h2 id="main">Deploy Python Lambda function with Container Image</h2>
<p>
Consider this <code>Dockerfile</code>, which launches a Python 3.8 command-line application in a manner compatible with AWS Lambda:
</p>
<div class="codeLabel"><a href='data:text/plain;charset=UTF-8,Dockerfile' download='Dockerfile'
title='Click on the file name to download the file'>Dockerfile</a>
</div>
<pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id7c2e5ea7feb8">FROM public.ecr.aws/lambda/python:3.8
COPY app.py ./
CMD ["app.handler"]
</pre>
<p>
Following is a small Python app called <code>app.py</code>, which will be launched by the <code>Dockerfile</code>.
The Python app can be run as an AWS Lambda program because it implements the <code>handler</code> entry point.
</p>
<div class="codeLabel"><a href='data:text/plain;charset=UTF-8,app.py' download='app.py'
title='Click on the file name to download the file'>app.py</a>
</div>
<pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id6999955ae653">import sys
def handler(event, context):
return f"Hello from AWS Lambda using Python {sys.version}!"
</pre>
<!-- endregion -->
<!-- #region -->
<h2 id="build">Build image</h2>
<p>
Buildah builds the image, just the same way that Docker would:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id51426743124d'><button class='copyBtn' data-clipboard-target='#id51426743124d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah bud -t hello .
<span class='unselectable'>STEP 1: FROM public.ecr.aws/lambda/python:3.8
Getting image source signatures
Copying blob 03ac043af787 skipped: already exists
Copying blob 420e64b38334 done
Copying blob ff259f25b075 done
Copying blob 3ff716981d54 done
Copying blob 6b6e623a48a8 done
Copying blob 9aa8f1e66d54 done
Copying config 67dc3a2a54 done
Writing manifest to image destination
Storing signatures
STEP 2: COPY app.py ./
STEP 3: CMD ["app.handler"]
STEP 4: COMMIT hello
Getting image source signatures
Copying blob 683073d39306 skipped: already exists
Copying blob 658871a69e1f skipped: already exists
Copying blob 6fa16f35d11e skipped: already exists
Copying blob d6fa53d6caa6 skipped: already exists
Copying blob 61c062506436 skipped: already exists
Copying blob 1c1d66a5fd95 skipped: already exists
Copying blob 33af9dc6463a done
Copying config 98862dfd20 done
Writing manifest to image destination
Storing signatures
--> 98862dfd208
98862dfd2087152ee821553d6cb1c033e735af06e5f11c814bcc9300fb65584e </span></pre>
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="deploy_local">Test Lambda function Locally</h2>
<p>
Before calling the Lambda API from a local container, first run the container.
Containers default to running in the foreground, but the <code>-d</code> option causes a container to be run as a background process.
This container is given the name <code>hello</code>,
the external HTTP endpoint at 9000 is mapped to internal port 8080,
and the latest version of the <code>hello</code> lambda function is run in the container.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id01e4113a8555'><button class='copyBtn' data-clipboard-target='#id01e4113a8555' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman run \
-d \
--name hello \
-p 9000:8080 \
hello:latest
<span class='unselectable'>d4d296e4c91d01c98d312e3f79599dca53990d95218e94bbdfbbac6a43cde9e8 </span></pre>
</div>
<p>
Call the local version of the Lambda API:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id093c68d6aa85'><button class='copyBtn' data-clipboard-target='#id093c68d6aa85' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>curl \
-XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" \
-d '{}'
<span class='unselectable'>"Hello from AWS Lambda using Python 3.8.9 (default, Apr 20 2021, 13:58:54) \n[GCC 7.3.1 20180712 (Red Hat 7.3.1-12)]!" </span></pre>
</div>
<p>
Stop the container called <code>hello</code>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id242021565e07'><button class='copyBtn' data-clipboard-target='#id242021565e07' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman stop hello
<span class='unselectable'>96cc1b1ed92368a1165d6a6ad0b1e5544d4ac751b64e94df33bf2322e6d7b30c </span></pre>
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="podman_tag">Create AWS ECR Repository</h2>
<p>
AWS provides a registry for OCI-compatible image repositories called the
<a href='https://aws.amazon.com/ecr/' target='_blank' rel='nofollow'>AWS Elastic Container Registry (ECR)</a>.
</p>
<!-- #region -->
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6dcf27be9ef8'><button class='copyBtn' data-clipboard-target='#id6dcf27be9ef8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ecr create-repository help
CREATE-REPOSITORY() CREATE-REPOSITORY()<br/>
NAME
create-repository -<br/>
DESCRIPTION
Creates a repository. For more information, see Amazon ECR Repositories
in the Amazon Elastic Container Registry User Guide .<br/>
See also: AWS API Documentation<br/>
See 'aws help' for descriptions of global parameters.<br/>
SYNOPSIS
create-repository
--repository-name <value>
[--tags <value>]
[--image-tag-mutability <value>]
[--image-scanning-configuration <value>]
[--cli-input-json <value>]
[--generate-cli-skeleton <value>]<br/>
OPTIONS
--repository-name (string)
The name to use for the repository. The repository name may be spec-
ified on its own (such as nginx-web-app ) or it can be prepended
with a namespace to group the repository into a category (such as
project-a/nginx-web-app ).<br/>
--tags (list)
The metadata that you apply to the repository to help you categorize
and organize them. Each tag consists of a key and an optional value,
both of which you define. Tag keys can have a maximum character
length of 128 characters, and tag values can have a maximum length
of 256 characters.<br/>
(structure)
The metadata that you apply to a resource to help you categorize
and organize them. Each tag consists of a key and an optional
value, both of which you define. Tag keys can have a maximum
character length of 128 characters, and tag values can have a
maximum length of 256 characters.<br/>
Key -> (string)
One part of a key-value pair that make up a tag. A key is a
general label that acts like a category for more specific tag
values.<br/>
Value -> (string)
The optional part of a key-value pair that make up a tag. A
value acts as a descriptor within a tag category (key).<br/>
Shorthand Syntax:<br/>
Key=string,Value=string ...<br/>
JSON Syntax:<br/>
[
{
"Key": "string",
"Value": "string"
}
...
]<br/>
--image-tag-mutability (string)
The tag mutability setting for the repository. If this parameter is
omitted, the default setting of MUTABLE will be used which will al-
low image tags to be overwritten. If IMMUTABLE is specified, all im-
age tags within the repository will be immutable which will prevent
them from being overwritten.<br/>
Possible values:<br/>
o MUTABLE<br/>
o IMMUTABLE<br/>
--image-scanning-configuration (structure)
The image scanning configuration for the repository. This setting
determines whether images are scanned for known vulnerabilities af-
ter being pushed to the repository.<br/>
scanOnPush -> (boolean)
The setting that determines whether images are scanned after be-
ing pushed to a repository. If set to true , images will be
scanned after being pushed. If this parameter is not specified,
it will default to false and images will not be scanned unless a
scan is manually started with the StartImageScan API.<br/>
Shorthand Syntax:<br/>
scanOnPush=boolean<br/>
JSON Syntax:<br/>
{
"scanOnPush": true|false
}<br/>
--cli-input-json (string) Performs service operation based on the JSON
string provided. The JSON string follows the format provided by --gen-
erate-cli-skeleton. If other arguments are provided on the command
line, the CLI values will override the JSON-provided values. It is not
possible to pass arbitrary binary values using a JSON-provided value as
the string will be taken literally.<br/>
--generate-cli-skeleton (string) Prints a JSON skeleton to standard
output without sending an API request. If provided with no value or the
value input, prints a sample input JSON that can be used as an argument
for --cli-input-json. If provided with the value output, it validates
the command inputs and returns a sample output JSON for that command.<br/>
See 'aws help' for descriptions of global parameters.<br/>
EXAMPLES
Example 1: To create a repository<br/>
The following create-repository example creates a repository inside the
specified namespace in the default registry for an account.<br/>
aws ecr create-repository \
--repository-name project-a/nginx-web-app<br/>
Output:<br/>
{
"repository": {
"registryId": "123456789012",
"repositoryName": "sample-repo",
"repositoryArn": "arn:aws:ecr:us-west-2:123456789012:repository/project-a/nginx-web-app"
}
}<br/>
For more information, see Creating a Repository in the Amazon ECR User
Guide.<br/>
Example 2: To create a repository configured with image tag immutabil-
ity<br/>
The following create-repository example creates a repository configured
for tag immutability in the default registry for an account.<br/>
aws ecr create-repository \
--repository-name sample-repo \
--image-tag-mutability IMMUTABLE<br/>
Output:<br/>
{
"repository": {
"registryId": "123456789012",
"repositoryName": "sample-repo",
"repositoryArn": "arn:aws:ecr:us-west-2:123456789012:repository/sample-repo",
"imageTagMutability": "IMMUTABLE"
}
}<br/>
For more information, see Image Tag Mutability in the Amazon ECR User
Guide.<br/>
Example 3: To create a repository configured with a scanning configura-
tion<br/>
The following create-repository example creates a repository configured
to perform a vulnerability scan on image push in the default registry
for an account.<br/>
aws ecr create-repository \
--repository-name sample-repo \
--image-scanning-configuration scanOnPush=true<br/>
Output:<br/>
{
"repository": {
"registryId": "123456789012",
"repositoryName": "sample-repo",
"repositoryArn": "arn:aws:ecr:us-west-2:123456789012:repository/sample-repo",
"imageScanningConfiguration": {
"scanOnPush": true
}
}
}<br/>
For more information, see Image Scanning in the Amazon ECR User Guide.<br/>
OUTPUT
repository -> (structure)
The repository that was created.<br/>
repositoryArn -> (string)
The Amazon Resource Name (ARN) that identifies the repository.
The ARN contains the arn:aws:ecr namespace, followed by the re-
gion of the repository, AWS account ID of the repository owner,
repository namespace, and repository name. For example,
arn:aws:ecr:region:012345678910:repository/test .<br/>
registryId -> (string)
The AWS account ID associated with the registry that contains
the repository.<br/>
repositoryName -> (string)
The name of the repository.<br/>
repositoryUri -> (string)
The URI for the repository. You can use this URI for Docker push
or pull operations.<br/>
createdAt -> (timestamp)
The date and time, in JavaScript date format, when the reposi-
tory was created.<br/>
imageTagMutability -> (string)
The tag mutability setting for the repository.<br/>
imageScanningConfiguration -> (structure)
The image scanning configuration for a repository.<br/>
scanOnPush -> (boolean)
The setting that determines whether images are scanned after
being pushed to a repository. If set to true , images will be
scanned after being pushed. If this parameter is not speci-
fied, it will default to false and images will not be scanned
unless a scan is manually started with the StartImageScan
API.<br/>
<br/>
CREATE-REPOSITORY()</pre>
</div>
<!-- endregion -->
<p>
The following creates an AWS ECR image repository in
called <code>hello</code> within the <code>test</code> namespace.
<a href='https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning.html' target='_blank' rel='nofollow'>Images are scanned</a>
for known vulnerabilities after they are pushed to the repository.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ideda6dfae692d'><button class='copyBtn' data-clipboard-target='#ideda6dfae692d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ecr create-repository \
--repository-name test/hello \
--image-scanning-configuration scanOnPush=true
<span class='unselectable'>{
"repository": {
"repositoryArn": "arn:aws:ecr:us-east-1:031372724784:repository/test/hello",
"registryId": "031372724784",
"repositoryName": "test/hello",
"repositoryUri": "031372724784.dkr.ecr.us-east-1.amazonaws.com/test/hello",
"createdAt": 1620232146.0,
"imageTagMutability": "MUTABLE",
"imageScanningConfiguration": {
"scanOnPush": true
}
}
} </span></pre>
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="podman_tag">Tag Image</h2>
<p class="quote">
<b><code>podman tag</code></b> –
Assigns a new image name to an existing image.
A full name refers to the entire image name, including the optional tag after the <code>:</code>.
If there is no tag provided, then podman will default to latest for both the image and the target-name.
– From <code>man podman-tag</code>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb57306e97d20'><button class='copyBtn' data-clipboard-target='#idb57306e97d20' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>IMAGE_NAME=hello
<span class='unselectable'>$ </span>IMAGE_VERSION=0.1
<span class='unselectable'>$ </span>podman tag $IMAGE_NAME:$IMAGE_VERSION \
$REGISTRY/$IMAGE_NAME:$IMAGE_VERSION
<span class='unselectable'>$ </span>podman images
<span class='unselectable'>REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/hello 0.1 98862dfd2087 39 minutes ago 622 MB
752246127823.dkr.ecr.us-east-1.amazonaws.com/hello latest 98862dfd2087 39 minutes ago 622 MB
public.ecr.aws/lambda/python 3.8 67dc3a2a54fb 25 hours ago 622 MB
752246127823.dkr.ecr.us-east-1.amazonaws.com/ancientwarmth latest 5d18ea34fc30 28 hours ago 2.03 GB
localhost/ancientwarmth latest 5d18ea34fc30 28 hours ago 2.03 GB
<none> <none> 40ef32b39cf4 5 days ago 622 MB
docker.io/library/amazonlinux latest 53ef897d731f 5 days ago 170 MB
docker.io/amazon/aws-lambda-python 3.8 e12ea62c5582 9 days ago 622 MB
docker.io/library/alpine latest 6dbb9cc54074 2 weeks ago 5.88 MB
docker.io/lambci/lambda build-python3.8 714c659c9f6f 3 months ago 2.03 GB </span></pre>
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="push">Push Image to ECR</h2>
<p>
<code>Podman</code> will use the IAM credentials for the <code>dev</code>
<a href='https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html' target='_blank' rel='nofollow'>profile</a>
in <code>~/.aws/credentials</code> to log into that AWS account:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>~/.aws/credentials</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id8762e1b641f8'><button class='copyBtn' data-clipboard-target='#id8762e1b641f8' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>[default]
aws_access_key_id = ********************
aws_secret_access_key = ****************************************
region = us-east-1<br>
[dev]
aws_access_key_id = ********************
aws_secret_access_key = ****************************************
region = us-east-1</pre>
</div>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id51400ebb4ec5'><button class='copyBtn' data-clipboard-target='#id51400ebb4ec5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>export AWS_PROFILE=dev
<span class='unselectable'>$ </span>AWS_ACCOUNT="$( aws sts get-caller-identity \
--query Account \
--output text
)"
<span class='unselectable'>$ </span>AWS_REGION="$( aws configure get region )"
<span class='unselectable'>$ </span>REGISTRY="$AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com"
<span class='unselectable'>$ </span>aws ecr get-login-password \
--region "$AWS_REGION" | \
podman login \
--password-stdin \
--username AWS \
"$REGISTRY"
<span class='unselectable'>Login Succeeded! </span></pre>
</div>
<p>
Now that <code>podman</code> is logged into AWS, use <code>podman</code> push the image to AWS ECR:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idc4cf51b77d45'><button class='copyBtn' data-clipboard-target='#idc4cf51b77d45' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman push test/$IMAGE_NAME \
$REGISTRY/$IMAGE_NAME:$IMAGE_VERSION
<span class='unselectable'>Getting image source signatures
Copying blob 692590faf2d1 [--------------------------------------] 8.0b / 8.2MiB
Copying blob 397718cff58d [--------------------------------------] 8.0b / 206.2MiB
Copying blob 9ca787b1c91c [--------------------------------------] 8.0b / 93.1MiB
Copying blob ef26f5221b79 [--------------------------------------] 8.0b / 196.7MiB
Copying blob 0a3f69c27a89 [--------------------------------------] 8.0b / 316.4MiB
Copying blob 5b3cbb76df75 [--------------------------------------] 8.0b / 1.1GiB
Copying blob e9cad39831b0 [--------------------------------------] 8.0b / 3.5KiB
Error: Error copying image to the remote destination:
Error writing blob: Error initiating layer upload to /v2/ancientwarmth/blobs/uploads/ in
752246127823.dkr.ecr.us-east-1.amazonaws.com:
name unknown: The repository with name 'hello' does not exist in the registry with id '752246127823' </span></pre>
</div>
<p>
The results of an
<a href='https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ecr/describe-image-scan-findings.html' target='_blank' rel='nofollow'>image scan</a>
for the new repository can be retrieved as follows:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id07cf7543e256'><button class='copyBtn' data-clipboard-target='#id07cf7543e256' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>aws ecr describe-image-scan-findings \
--repository-name test/hello \
--image-id imageTag=tag_name</pre>
</div>
<!-- endregion -->
<!-- #region -->
<!-- endregion -->
<!-- #region -->
<h2 id="buildah_python">Deploy Python Lambda function with Container Image</h2>
<p>
<code>Podman</code> can invoke the app using an OCI container with Amazon Linux 2 and Python 3.8:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idcdba0b9c019c'><button class='copyBtn' data-clipboard-target='#idcdba0b9c019c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman container run -ti \
public.ecr.aws/lambda/python:3.8 \
blog/docker/podman/app.py
<span class='unselectable'>Trying to pull public.ecr.aws/lambda/python:3.8...
Getting image source signatures
Copying blob 1de4740de1c2 done
Copying blob 03ac043af787 done
Copying blob 2e2bb77ae2dc done
Copying blob 842c9dce67e8 done
Copying blob df513d38f4d9 done
Copying blob 031c6369fb2b done
Copying config e12ea62c55 done
Writing manifest to image destination
Storing signatures
time="2021-05-02T23:38:30.971" level=info msg="exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)" </span></pre>
</div>
<!-- endregion -->
Docker, OCI Images, Buildah and podman2021-04-28T00:00:00-04:00https://mslinn.github.io/blog/2021/04/28/buildah-podman<!-- #region -->
<p>
There are many ways to create and run Docker-compatible images.
Docker is probably the worst option, mostly because it runs as a daemon, and all *nix daemons run with <code>root</code> privileges.
Also, the <code>docker-ce</code> package lists <code>iptables</code> as a dependency, which needs <code>systemd</code> to be running normally,
and WSL2 only partially supports <code>systemd</code>.
</p>
<p>
<a href='https://www.capitalone.com/tech/cloud/container-runtime/' target='_blank' rel='nofollow'>A Comprehensive Container Runtime Comparison</a>
provides helpful background information and an interesting historical viewpoint.
</p>
<h2 id="oci">Open Container Initiative (OCI)</h2>
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<a href='https://opencontainers.org/' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/buildahPodman/oci_logo.webp" type="image/webp">
<source srcset="/blog/images/buildahPodman/oci_logo.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/buildahPodman/oci_logo.png"
style='width: 100%; '
/>
</picture>
</a>
</div>
<p>
The latest evolution of Docker-compatible images, <a href='https://github.com/opencontainers/image-spec' target='_blank' rel='nofollow'>OCI image format</a>
(not to be confused with <a href='https://www.oracle.com/ca-en/cloud/' target='_blank' rel='nofollow'>Oracle Cloud Infrastructure</a>),
is compatible with:
</p>
<ul style="column-count: 2;">
<li><a href='https://aws.amazon.com/lambda/' target='_blank' rel='nofollow'>AWS Lambda</a></li>
<li><a href='https://azure.microsoft.com/en-us/services/functions/' target='_blank' rel='nofollow'>Azure Functions</a></li>
<li><a href='https://azure.microsoft.com/en-us/services/kubernetes-service/' target='_blank' rel='nofollow'>Azure Kubernetes Service</a></li>
<li><a href='https://buildah.io/' target='_blank' rel='nofollow'>Buildah</a></li>
<li><a href='https://buildpacks.io/' target='_blank' rel='nofollow'>Cloud Native Buildpacks</a></li>
<li><a href='https://circleci.com/' target='_blank' rel='nofollow'>CircleCI</a></li>
<li><a href='https://www.docker.com/' target='_blank' rel='nofollow'>Docker</a></li>
<li><a href='https://dokku.com/' target='_blank' rel='nofollow'>Dokku</a></li>
<li><a href='https://gitlab.com' target='_blank' rel='nofollow'>GitLab</a></li>
<li><a href='https://cloud.google.com/container-registry/docs/image-formats' target='_blank' rel='nofollow'>Google Cloud</a></li>
<li><a href='https://heroku.com' target='_blank' rel='nofollow'>Heroku</a></li>
<li><a href='https://containerjournal.com/topics/container-management/what-is-knative-and-what-can-it-do-for-you/' target='_blank' rel='nofollow'>Knative</a></li>
<li><a href='https://kubernetes.io/' target='_blank' rel='nofollow'>Kubernetes</a></li>
<li><a href='https://podman.io/' target='_blank' rel='nofollow'><code>podman</code></a></li>
<li><a href='https://github.com/containers/skopeo' target='_blank' rel='nofollow'><code>skopeo</code></a></li>
<li><a href='https://spring.io/guides/topicals/spring-boot-docker/' target='_blank' rel='nofollow'>Spring Boot</a></li>
<li><a href='https://cloud.google.com/tekton' target='_blank' rel='nofollow'>Tekton</a></li>
</ul>
<p>
Supported OCI formats include:
</p>
<ul style="column-count: 2;">
<li>Docker containers schema 1</li>
<li>Docker containers schema 2</li>
<li>Pods (groups of containers)</li>
<li>Images</li>
<li>Volumes</li>
</ul>
<h2 id="three">Buildah, podman and skopeo</h2>
<p>
This blog post discusses 3 related open source projects from RedHat / IBM that provide an alternative to Docker:
Buildah, <code>podman</code> and <code>skopeo</code>.
These 3 projects share a common source code base, and are
daemonless tools for managing Open Container Initiative (OCI) images.
</p>
<p>
Paraphrasing the reasons expressed in
<a href='https://developers.redhat.com/blog/2019/02/21/podman-and-buildah-for-docker-users/' target='_blank' rel='nofollow'>Podman and Buildah for Docker Users</a>
for using <code>podman</code> instead of Docker,
wherever <code>podman</code> is mentioned, read “<code>podman</code>, Buildah and <code>skopeo</code>”:
</p>
<p class="quoteCite" cite="From “Podman and Buildah for Docker Users”">
The Podman approach is simply to directly interact with the image registry,
with the container and image storage,
and with the Linux kernel through the <code>runC</code> container runtime process (not a daemon).<br><br>
Running Podman as a normal user means that Podman will,
by default, store images and containers in the user’s home directory.
Podman uses a repository in the user’s home directory:
<code>~/.local/share/containers</code> (instead of <code>/var/lib/docker</code>).<br><br>
Despite the new locations for the local repositories, the images created by Docker and Podman are compatible with the OCI standard.
Podman can push to and pull from popular container registries like Quay.io and Docker hub, as well as private registries.
</p>
<h2 id="buildah_vs_podman">Buildah vs. podman</h2>
<p>
<code>Podman</code> can build OCI containers interactively or in batch mode.
You can either build using a <code>Dockerfile</code> using <code>podman build</code> (batch mode),
or you can interactively run a container,
make changes to the running image, and then <code>podman commit</code> those changes to a new image tag.
</p>
<p>
Buildah was written before <code>podman</code>.
Some of Buildah's source code for creating and managing container images was ported to <code>podman</code>.
The <code>podman build</code> command is a subset of Buildah’s functionality.
</p>
<p>
<p>
However, apparently the differences between the two programs are important:
</p>
<p class="quote">
Buildah builds OCI images.
Confusingly, <code>podman build</code> can also be used to build Docker images also,
but it’s incredibly slow and used up a lot of disk space by using the <code>vfs</code> storage driver by default.
<code>buildah bud</code> (‘build using Dockerfile’) was much faster for me, and uses the overlay storage driver.
<br><br>
– From <a href='https://zwischenzugs.com/page/3/' target='_blank' rel='nofollow'>Goodbye Docker: Purging is Such Sweet Sorrow</a> by Ian Miell.
</p>
<!-- endregion -->
<!-- #region -->
<h2 id="podman">podman</h2>
<div class='imgWrapper imgFlex inline' style=' '>
<a href='https://podman.io' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/buildahPodman/podman-logo.webp" type="image/webp">
<source srcset="/blog/images/buildahPodman/podman-logo.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/buildahPodman/podman-logo.png"
style='width: 100%; padding: 1em'
/>
</picture>
</a>
</div>
<p>
<code>Podman</code> supports developing, managing, and running OCI Containers on Linux systems, including WSL, without requiring <code>root</code> privilege.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>shell Installation on Ubuntu</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id306f1850d9b0'><button class='copyBtn' data-clipboard-target='#id306f1850d9b0' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install buildah podman skopeo</pre>
</div>
<div class="pullQuote">
Podman commands are very nearly the same as Docker’s.
</div>
<p>
Because <code>podman</code> is a drop-in replacement for <code>docker</code>, the following alias enables the <code>docker</code> command to invoke <code>podman</code>:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>~/.bash_aliases</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id161920233b19'><button class='copyBtn' data-clipboard-target='#id161920233b19' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>alias docker=podman</pre>
</div>
<p>
As described in <a href='https://www.vultr.com/docs/how-to-install-and-use-podman-on-ubuntu-20-04' target='_blank' rel='nofollow'>How to Install and Use Podman on Ubuntu 20.04</a>,
I added <code>'registry.access.redhat.com'</code> to the list of
<code>registries</code> in <code>/etc/containers/registries.conf</code>.
I also added <a href='https://gallery.ecr.aws/' target='_blank' rel='nofollow'><code>gallery.ecr.aws</code></a> and
<a href='https://cloud.google.com/container-registry/docs/pushing-and-pulling#add-registry' target='_blank' rel='nofollow'><code>gcr.io</code></a>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>/etc/containers/registries.conf</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd4be6b63093d'><button class='copyBtn' data-clipboard-target='#idd4be6b63093d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>[registries.search]
registries = ['docker.io', 'gallery.ecr.aws', 'gcr.io', 'quay.io', 'registry.access.redhat.com']</pre>
</div>
<!-- endregion -->
<!-- #region -->
<h3 id="podmanHelp"><span class="code">podman</span> Help</h3>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idbc16de91f299'><button class='copyBtn' data-clipboard-target='#idbc16de91f299' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman --help
<span class='unselectable'>Manage pods, containers and images<br/>
Usage:
podman [flags]
podman [command]<br/>
Available Commands:
attach Attach to a running container
auto-update Auto update containers according to their auto-update policy
build Build an image using instructions from Containerfiles
commit Create new image based on the changed container
container Manage containers
cp Copy files/folders between a container and the local filesystem
create Create but do not start a container
diff Display the changes to the object's file system
events Show podman events
exec Run a process in a running container
export Export container's filesystem contents as a tar archive
generate Generate structured data based on containers and pods.
healthcheck Manage health checks on containers
help Help about any command
history Show history of a specified image
image Manage images
images List images in local storage
import Import a tarball to create a filesystem image
info Display podman system information
init Initialize one or more containers
inspect Display the configuration of object denoted by ID
kill Kill one or more running containers with a specific signal
load Load an image from container archive
login Login to a container registry
logout Logout of a container registry
logs Fetch the logs of one or more containers
manifest Manipulate manifest lists and image indexes
mount Mount a working container's root filesystem
network Manage networks
pause Pause all the processes in one or more containers
play Play a pod and its containers from a structured file.
pod Manage pods
port List port mappings or a specific mapping for the container
ps List containers
pull Pull an image from a registry
push Push an image to a specified destination
restart Restart one or more containers
rm Remove one or more containers
rmi Removes one or more images from local storage
run Run a command in a new container
save Save image to an archive
search Search registry for image
start Start one or more containers
stats Display a live stream of container resource usage statistics
stop Stop one or more containers
system Manage podman
tag Add an additional name to a local image
top Display the running processes of a container
unmount Unmounts working container's root filesystem
unpause Unpause the processes in one or more containers
unshare Run a command in a modified user namespace
untag Remove a name from a local image
version Display the Podman Version Information
volume Manage volumes
wait Block on one or more containers<br/>
Flags:
--cgroup-manager string Cgroup manager to use ("cgroupfs"|"systemd") (default "cgroupfs")
--cni-config-dir string Path of the configuration directory for CNI networks
--conmon string Path of the conmon binary
-c, --connection string Connection to use for remote Podman service
--events-backend string Events backend to use ("file"|"journald"|"none") (default "file")
--help Help for podman
--hooks-dir strings Set the OCI hooks directory path (may be set multiple times) (default [/usr/share/containers/oci/hooks.d])
--identity string path to SSH identity file, (CONTAINER_SSHKEY)
--log-level string Log messages above specified level (debug, info, warn, error, fatal, panic) (default "error")
--namespace string Set the libpod namespace, used to create separate views of the containers and pods on the system
--network-cmd-path string Path to the command for configuring the network
-r, --remote Access remote Podman service (default false)
--root string Path to the root directory in which data, including images, is stored
--runroot string Path to the 'run directory' where all state information is stored
--runtime string Path to the OCI-compatible binary used to run containers, default is /usr/bin/runc
--storage-driver string Select which storage driver is used to manage storage of images and containers (default is overlay)
--storage-opt stringArray Used to pass an option to the storage driver
--syslog Output logging information to syslog as well as the console (default false)
--tmpdir string Path to the tmp directory for libpod state content.
Note: use the environment variable 'TMPDIR' to change the temporary storage location for container images, '/var/tmp'.
--url string URL to access Podman service (CONTAINER_HOST) (default "unix:/home/mslinn/.docker/run/podman/podman.sock")
-v, --version Version of Podman<br/>
Use "podman [command] --help" for more information about a command. </span></pre>
</div>
<!-- endregion -->
<!-- #region -->
<h3 id="padman_info"><span class="code">podman info</span></h3>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id5ce8b14aefb5'><button class='copyBtn' data-clipboard-target='#id5ce8b14aefb5' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman info
<span class='unselectable'>host:
arch: amd64
buildahVersion: 1.15.2
cgroupVersion: v1
conmon:
package: 'conmon: /usr/libexec/podman/conmon'
path: /usr/libexec/podman/conmon
version: 'conmon version 2.0.20, commit: unknown'
cpus: 8
distribution:
distribution: ubuntu
version: "20.10"
eventLogger: file
hostname: Bear
idMappings:
gidmap:
- container_id: 0
host_id: 1000
size: 1
- container_id: 1
host_id: 100000
size: 65536
uidmap:
- container_id: 0
host_id: 1000
size: 1
- container_id: 1
host_id: 100000
size: 65536
kernel: 5.4.72-microsoft-standard-WSL2
linkmode: dynamic
memFree: 897724416
memTotal: 6231638016
ociRuntime:
name: runc
package: 'containerd.io: /usr/bin/runc'
path: /usr/bin/runc
version: |-
runc version 1.0.0-rc93
commit: 12644e614e25b05da6fd08a38ffa0cfe1903fdec
spec: 1.0.2-dev
go: go1.13.15
libseccomp: 2.5.1
os: linux
remoteSocket:
path: /home/mslinn/.docker/run/podman/podman.sock
rootless: true
slirp4netns:
executable: /bin/slirp4netns
package: Unknown
version: |-
slirp4netns version 1.0.1
commit: 6a7b16babc95b6a3056b33fb45b74a6f62262dd4
libslirp: 4.3.1
swapFree: 0
swapTotal: 0
uptime: 306h 23m 6.37s (Approximately 12.75 days)
registries:
search:
- quay.io
- docker.io
- gallery.ecr.aws
- registry.access.redhat.com
store:
configFile: /home/mslinn/.config/containers/storage.conf
containerStore:
number: 8
paused: 0
running: 0
stopped: 8
graphDriverName: overlay
graphOptions:
overlay.mount_program:
Executable: /bin/fuse-overlayfs
Package: Unknown
Version: |-
fusermount3 version: 3.9.3
fuse-overlayfs: version 1.0.0
FUSE library version 3.9.3
using FUSE kernel interface version 7.31
graphRoot: /home/mslinn/.local/share/containers/storage
graphStatus:
Backing Filesystem: extfs
Native Overlay Diff: "false"
Supports d_type: "true"
Using metacopy: "false"
imageStore:
number: 4
runRoot: /home/mslinn/.docker/run/containers
volumePath: /home/mslinn/.local/share/containers/storage/volumes
version:
APIVersion: 1
Built: 0
BuiltTime: Wed Dec 31 19:00:00 1969
GitCommit: ""
GoVersion: go1.14.7
OsArch: linux/amd64
Version: 2.0.6 </span></pre>
</div>
<!-- endregion -->
<!-- #region -->
<h3 id="padman_container_help"><span class="code">podman container</span> Help</h3>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id29052548935c'><button class='copyBtn' data-clipboard-target='#id29052548935c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>man podman-container
<span class='unselectable'>podman-container(1) General Commands Manual podman-container(1)<br/>
NAME
podman-container - Manage containers<br/>
SYNOPSIS
podman container subcommand<br/>
DESCRIPTION
The container command allows you to manage containers<br/>
COMMANDS
┌───────────┬────────────────────────────────┬───────────────────────────────────┐
│Command │ Man Page │ Description │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│attach │ podman-attach(1) │ Attach to a running container. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│checkpoint │ podman-container-checkpoint(1) │ Checkpoints one or more running │
│ │ │ containers. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│cleanup │ podman-container-cleanup(1) │ Cleanup the container's network │
│ │ │ and mountpoints. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│commit │ podman-commit(1) │ Create new image based on the │
│ │ │ changed container. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│cp │ podman-cp(1) │ Copy files/folders between a │
│ │ │ container and the local │
│ │ │ filesystem. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│create │ podman-create(1) │ Create a new container. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│diff │ podman-diff(1) │ Inspect changes on a container or │
│ │ │ image's filesystem. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│exec │ podman-exec(1) │ Execute a command in a running │
│ │ │ container. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│exists │ podman-container-exists(1) │ Check if a container exists in │
│ │ │ local storage │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│export │ podman-export(1) │ Export a container's filesystem │
│ │ │ contents as a tar archive. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│init │ podman-init(1) │ Initialize a container │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│inspect │ podman-inspect(1) │ Display a container or image's │
│ │ │ configuration. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│kill │ podman-kill(1) │ Kill the main process in one or │
│ │ │ more containers. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│list │ podman-ps(1) │ List the containers on the │
│ │ │ system.(alias ls) │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│logs │ podman-logs(1) │ Display the logs of a container. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│mount │ podman-mount(1) │ Mount a working container's root │
│ │ │ filesystem. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│pause │ podman-pause(1) │ Pause one or more containers. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│port │ podman-port(1) │ List port mappings for the │
│ │ │ container. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│prune │ podman-container-prune(1) │ Remove all stopped containers │
│ │ │ from local storage. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│restart │ podman-restart(1) │ Restart one or more containers. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│restore │ podman-container-restore(1) │ Restores one or more containers │
│ │ │ from a checkpoint. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│rm │ podman-rm(1) │ Remove one or more containers. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│run │ podman-run(1) │ Run a command in a container. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│runlabel │ podman-container-runlabel(1) │ Executes a command as described │
│ │ │ by a container image label. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│start │ podman-start(1) │ Starts one or more containers. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│stats │ podman-stats(1) │ Display a live stream of one or │
│ │ │ more container's resource usage │
│ │ │ statistics. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│stop │ podman-stop(1) │ Stop one or more running │
│ │ │ containers. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│top │ podman-top(1) │ Display the running processes of │
│ │ │ a container. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│unmount │ podman-unmount(1) │ Unmount a working container's │
│ │ │ root filesystem.(Alias unmount) │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│unpause │ podman-unpause(1) │ Unpause one or more containers. │
├───────────┼────────────────────────────────┼───────────────────────────────────┤
│wait │ podman-wait(1) │ Wait on one or more containers to │
│ │ │ stop and print their exit codes. │
└───────────┴────────────────────────────────┴───────────────────────────────────┘<br/>
SEE ALSO
podman, podman-exec, podman-run<br/>
podman-container(1) </span></pre>
</div>
<!-- endregion -->
<!-- #region -->
<h3 id="podman_help">Podman Run Help</h3>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb21877d8591b'><button class='copyBtn' data-clipboard-target='#idb21877d8591b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman container run --help
<span class='unselectable'>Run a command in a new container<br/>
Description:
Runs a command in a new container from the given image<br/>
Usage:
podman container run [flags] IMAGE [COMMAND [ARG...]]<br/>
Examples:
podman container run imageID ls -alF /etc
podman container run --network=host imageID dnf -y install java
podman container run --volume /var/hostdir:/var/ctrdir -i -t fedora /bin/bash<br/>
Flags:
--add-host strings Add a custom host-to-IP mapping (host:ip) (default [])
--annotation strings Add annotations to container (key:value)
-a, --attach strings Attach to STDIN, STDOUT or STDERR
--authfile string Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override
--blkio-weight string Block IO weight (relative weight) accepts a weight value between 10 and 1000.
--blkio-weight-device DEVICE_NAME:WEIGHT Block IO weight (relative device weight, format: DEVICE_NAME:WEIGHT)
--cap-add strings Add capabilities to the container
--cap-drop strings Drop capabilities from the container
--cgroup-parent string Optional parent cgroup for the container
--cgroupns string cgroup namespace to use
--cgroups string control container cgroup configuration ("enabled"|"disabled"|"no-conmon") (default "enabled")
--cidfile string Write the container ID to the file
--conmon-pidfile string Path to the file that will receive the PID of conmon
--cpu-period uint Limit the CPU CFS (Completely Fair Scheduler) period
--cpu-quota int Limit the CPU CFS (Completely Fair Scheduler) quota
--cpu-rt-period uint Limit the CPU real-time period in microseconds
--cpu-rt-runtime int Limit the CPU real-time runtime in microseconds
--cpu-shares uint CPU shares (relative weight)
--cpus float Number of CPUs. The default is 0.000 which means no limit
--cpuset-cpus string CPUs in which to allow execution (0-3, 0,1)
--cpuset-mems string Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems.
-d, --detach Run container in background and print container ID
--detach-keys [a-Z] Override the key sequence for detaching a container. Format is a single character [a-Z] or a comma separated sequence of `ctrl-<value>`, where `<value>` is one of: `a-cf`, `@`, `^`, `[`, `\`, `]`, `^` or `_` (default "ctrl-p,ctrl-q")
--device strings Add a host device to the container
--device-cgroup-rule strings Add a rule to the cgroup allowed devices list
--device-read-bps strings Limit read rate (bytes per second) from a device (e.g. --device-read-bps=/dev/sda:1mb)
--device-read-iops strings Limit read rate (IO per second) from a device (e.g. --device-read-iops=/dev/sda:1000)
--device-write-bps strings Limit write rate (bytes per second) to a device (e.g. --device-write-bps=/dev/sda:1mb)
--device-write-iops strings Limit write rate (IO per second) to a device (e.g. --device-write-iops=/dev/sda:1000)
--disable-content-trust This is a Docker specific option and is a NOOP
--dns strings Set custom DNS servers
--dns-opt strings Set custom DNS options
--dns-search strings Set custom DNS search domains
--entrypoint string Overwrite the default ENTRYPOINT of the image
-e, --env stringArray Set environment variables in container (default [PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,TERM=xterm])
--env-file strings Read in a file of environment variables
--env-host Use all current host environment variables in container
--expose strings Expose a port or a range of ports
--gidmap strings GID map to use for the user namespace
--group-add strings Add additional groups to join
--health-cmd string set a healthcheck command for the container ('none' disables the existing healthcheck)
--health-interval string set an interval for the healthchecks (a value of disable results in no automatic timer setup) (default "30s")
--health-retries uint the number of retries allowed before a healthcheck is considered to be unhealthy (default 3)
--health-start-period string the initialization time needed for a container to bootstrap (default "0s")
--health-timeout string the maximum time allowed to complete the healthcheck before an interval is considered failed (default "30s")
-h, --hostname string Set container hostname
--http-proxy Set proxy environment variables in the container based on the host proxy vars (default true)
--image-volume string Tells podman how to handle the builtin image volumes ("bind"|"tmpfs"|"ignore") (default "bind")
--init Run an init binary inside the container that forwards signals and reaps processes
--init-path string Path to the container-init binary
-i, --interactive Keep STDIN open even if not attached
--ip string Specify a static IPv4 address for the container
--ipc string IPC namespace to use
--kernel-memory <number>[<unit>] Kernel memory limit (format: <number>[<unit>], where unit = b (bytes), k (kilobytes), m (megabytes), or g (gigabytes))
-l, --label stringArray Set metadata on container
--label-file strings Read in a line delimited file of labels
--log-driver string Logging driver for the container
--log-opt strings Logging driver options
--mac-address string Container MAC address (e.g. 92:d0:c6:0a:29:33)
-m, --memory <number>[<unit>] Memory limit (format: <number>[<unit>], where unit = b (bytes), k (kilobytes), m (megabytes), or g (gigabytes))
--memory-reservation <number>[<unit>] Memory soft limit (format: <number>[<unit>], where unit = b (bytes), k (kilobytes), m (megabytes), or g (gigabytes))
--memory-swap string Swap limit equal to memory plus swap: '-1' to enable unlimited swap
--memory-swappiness int Tune container memory swappiness (0 to 100, or -1 for system default) (default -1)
--mount stringArray Attach a filesystem mount to the container
--name string Assign a name to the container
--network string Connect a container to a network (default "slirp4netns")
--no-healthcheck Disable healthchecks on container
--no-hosts Do not create /etc/hosts within the container, instead use the version from the image
--oom-kill-disable Disable OOM Killer
--oom-score-adj int Tune the host's OOM preferences (-1000 to 1000)
--pid string PID namespace to use
--pids-limit int Tune container pids limit (set 0 for unlimited, -1 for server defaults)
--pod string Run container in an existing pod
--pod-id-file string Read the pod ID from the file
--privileged Give extended privileges to container
-p, --publish strings Publish a container's port, or a range of ports, to the host (default [])
-P, --publish-all Publish all exposed ports to random ports on the host interface
--pull string Pull image before creating ("always"|"missing"|"never") (default "missing")
-q, --quiet Suppress output information when pulling images
--read-only Make containers root filesystem read-only
--read-only-tmpfs When running containers in read-only mode mount a read-write tmpfs on /run, /tmp and /var/tmp (default true)
--replace If a container with the same name exists, replace it
--restart string Restart policy to apply when a container exits ("always"|"no"|"on-failure")
--rm Remove container (and pod if created) after exit
--rmi Remove container image unless used by other containers
--rootfs The first argument is not an image but the rootfs to the exploded container
--seccomp-policy string Policy for selecting a seccomp profile (experimental) (default "default")
--security-opt stringArray Security Options
--shm-size <number>[<unit>] Size of /dev/shm (format: <number>[<unit>], where unit = b (bytes), k (kilobytes), m (megabytes), or g (gigabytes)) (default "65536k")
--sig-proxy Proxy received signals to the process (default true)
--stop-signal string Signal to stop a container. Default is SIGTERM
--stop-timeout uint Timeout (in seconds) to stop a container. Default is 10 (default 10)
--subgidname string Name of range listed in /etc/subgid for use in user namespace
--subuidname string Name of range listed in /etc/subuid for use in user namespace
--sysctl strings Sysctl options
--systemd string Run container in systemd mode ("true"|"false"|"always") (default "true")
--tmpfs tmpfs Mount a temporary filesystem (tmpfs) into a container
-t, --tty Allocate a pseudo-TTY for container
--uidmap strings UID map to use for the user namespace
--ulimit strings Ulimit options
-u, --user string Username or UID (format: <name|uid>[:<group|gid>])
--userns string User namespace to use
--uts string UTS namespace to use
-v, --volume stringArray Bind mount a volume into the container
--volumes-from strings Mount volumes from the specified container(s)
-w, --workdir string Working directory inside the container </span></pre>
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="podman_run">podman run</h2>
<p>
From <a href='https://chariotsolutions.com/blog/post/building-and-deploying-lambdas-from-a-docker-container/' target='_blank' rel='nofollow'>Building and Deploying Lambdas from a Docker Container</a> by Keith Gregory:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id081ec470c9df'><button class='copyBtn' data-clipboard-target='#id081ec470c9df' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman run \
-it \
--entrypoint /bin/bash \
--rm \
-v /tmp:/mnt \
amazon/aws-lambda-python:3.8
<span class='unselectable'>Trying to pull quay.io/amazon/aws-lambda-python:3.8...
Requesting bear token: invalid status code from registry 405 (Method Not Allowed)
Trying to pull docker.io/amazon/aws-lambda-python:3.8...
Getting image source signatures
Copying blob df513d38f4d9 skipped: already exists
Copying blob 2e2bb77ae2dc skipped: already exists
Copying blob 031c6369fb2b skipped: already exists
Copying blob 03ac043af787 skipped: already exists
Copying blob 842c9dce67e8 skipped: already exists
Copying blob 1de4740de1c2 [--------------------------------------] 0.0b / 0.0b
Copying config e12ea62c55 done
Writing manifest to image destination
Storing signatures
bash-4.2# </span>pwd
<span class='unselectable'>/var/task </span></pre>
</div>
<h2 id="cleanup">Cleaning Up a Container</h2>
<p>
<code>podman container cleanup</code> is a good command to know about.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id02939467c643'><button class='copyBtn' data-clipboard-target='#id02939467c643' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>podman container cleanup --help
<span class='unselectable'>Cleanup network and mountpoints of one or more containers
Description:
podman container cleanup
Cleans up mount points and network stacks on one or more containers
from the host. The container name or ID can be used. This command is
used internally when running containers, but can also be used if
container cleanup has failed when a container exits.
Usage:
podman container cleanup [options] CONTAINER [CONTAINER...]
Examples:
podman container cleanup --latest
podman container cleanup ctrID1 ctrID2 ctrID3
podman container cleanup --all
Options:
-a, --all Cleans up all containers
--exec string Clean up the given exec session instead of the container
-l, --latest Act on the latest container podman is aware of
Not supported with the "--remote" flag
--rm After cleanup, remove the container entirely
--rmi After cleanup, remove the image entirely </span></pre>
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="builah">Buildah</h2>
<div class='imgWrapper imgFlex inline' style=' '>
<a href='https://buildah.io/' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/buildahPodman/buildah-logo.webp" type="image/webp">
<source srcset="/blog/images/buildahPodman/buildah-logo.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/buildahPodman/buildah-logo.png"
style='width: 100%; padding: 1em'
/>
</picture>
</a>
</div>
<p>
<a href='https://buildah.io/' target='_blank' rel='nofollow'>Buildah</a> is a drop-in replacement for using <code>docker build</code> and a <code>Dockerfile</code>.
</p>
<div class="quote">
Where Buildah really shines is in its native commands,
which you can use to interact with container builds.
Rather than using <code>build-using-dockerfile/bud</code> for each build,
Buildah has commands to actually interact with the temporary container created during the build process.
(Docker uses temporary, or intermediate containers, too, but you don’t really interact with them while the image is being built.)
<br><br>
Unlike <code>docker build</code>, Buildah doesn’t commit changes to a layer automatically for every instruction in the <code>Dockerfile</code>
– it builds everything from top to bottom, every time.
On the positive side, this means non-cached builds (for example, those you would do with automation or build pipelines)
end up being somewhat faster than their Docker build counterparts, especially if there are many instructions.
<br><br>
– From <a href='https://opensource.com/article/18/6/getting-started-buildah' target='_blank' rel='nofollow'>Getting started with Buildah.</a>, published by <code>opensource.com</code>
</div>
<p>
Some key Buildah subcommands:
</p>
<dl>
<dt class="code">buildah bud</dt>
<dd>Buildah’s <code>build-using-dockerfile</code>, or <code>bud</code> argument makes it behave just like <code>docker build</code> does.</dd>
<dt class="code">buildah from</dt>
<dd>Build up a container root filesystem from an image or from scratch.</dd>
<dt class="code">buildah config</dt>
<dd>Adjust defaults in the image's configuration blob.</dd>
<dt class="code">buildah run</dt>
<dd>
<code>buildah run</code> is for running commands that build a container image.
This is similar to <code>RUN</code> in a <code>Dockerfile</code>, and unlike <code>docker run</code>.
</dd>
<dt class="code">buildah commit</dt>
<dd>Commit changes to the container to a new image.</dd>
<dt class="code">buildah push</dt>
<dd>Push images to registries (such a Quay) or a local <code>dockerd</code> instance.</dd>
<dt class="code"></dt>
<dd></dd>
<dt class="code"></dt>
<dd></dd>
<dt class="code"></dt>
<dd></dd>
</dl>
<h3 id="buildahHelp">Buildah Help</h3>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf5f43e3cbcc7'><button class='copyBtn' data-clipboard-target='#idf5f43e3cbcc7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah -h
<span class='unselectable'>A tool that facilitates building OCI images<br/>
Usage:
buildah [flags]
buildah [command]<br/>
Available Commands:
add Add content to the container
build-using-dockerfile Build an image using instructions in a Dockerfile
commit Create an image from a working container
config Update image configuration settings
containers List working containers and their base images
copy Copy content into the container
from Create a working container based on an image
help Help about any command
images List images in local storage
info Display Buildah system information
inspect Inspect the configuration of a container or image
login Login to a container registry
logout Logout of a container registry
manifest Manipulate manifest lists and image indexes
mount Mount a working container's root filesystem
pull Pull an image from the specified location
push Push an image to a specified destination
rename Rename a container
rm Remove one or more working containers
rmi Remove one or more images from local storage
run Run a command inside of the container
tag Add an additional name to a local image
umount Unmount the root file system of the specified working containers
unshare Run a command in a modified user namespace
version Display the Buildah version information<br/>
Flags:
-h, --help help for buildah
--log-level string The log level to be used. Either "debug", "info", "warn" or "error". (default "error")
--registries-conf string path to registries.conf file (not usually used)
--registries-conf-dir string path to registries.conf.d directory (not usually used)
--root string storage root dir (default "/var/lib/containers/storage")
--runroot string storage state dir (default "/var/run/containers/storage")
--storage-driver string storage-driver
--storage-opt strings storage driver option
--userns-gid-map ctrID:hostID:length default ctrID:hostID:length GID mapping to use
--userns-uid-map ctrID:hostID:length default ctrID:hostID:length UID mapping to use
-v, --version version for buildah<br/>
Use "buildah [command] --help" for more information about a command. </span></pre>
</div>
<!-- endregion -->
<!-- #region -->
<h3 id="buildahUse">Buildah / <span class="code">Dockerfile</span> Compatibility</h3>
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/buildahPodman/whales.webp" type="image/webp">
<source srcset="/blog/images/buildahPodman/whales.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/buildahPodman/whales.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
<a href='https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/building_running_and_managing_containers/building-container-images-with-buildah_porting-containers-to-systemd-using-podman' target='_blank' rel='nofollow'>Buildah</a>
can create an image from a Dockerfile by typing:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idad0925bbc86b'><button class='copyBtn' data-clipboard-target='#idad0925bbc86b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah bud -t hello .</pre>
</div>
<p>
…instead of:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3c07241fcbab'><button class='copyBtn' data-clipboard-target='#id3c07241fcbab' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span><span class="bg_yellow">sudo</span> docker build -t hello .</pre>
</div>
<p>
Buildah can create an image called <code>hello</code> from the <code>Dockerfile</code> and the Python app by typing:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id895c9d864ba7'><button class='copyBtn' data-clipboard-target='#id895c9d864ba7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah bud -t hello .
<span class='unselectable'>STEP 1: FROM public.ecr.aws/lambda/python:3.8
Getting image source signatures
Copying blob 1de4740de1c2 done
Copying blob 2e2bb77ae2dc done
Copying blob df513d38f4d9 done
Copying blob 03ac043af787 done
Copying blob 031c6369fb2b done
Copying blob 842c9dce67e8 done
Copying config e12ea62c55 done
Writing manifest to image destination
Storing signatures
STEP 2: COPY app.py ./
STEP 3: CMD ["app.handler"]
STEP 4: COMMIT hello
Getting image source signatures
Copying blob 109f575f8e6a skipped: already exists
Copying blob ff64b4f854ad skipped: already exists
Copying blob dd66ad8702f4 skipped: already exists
Copying blob d6fa53d6caa6 skipped: already exists
Copying blob 80166c3283e5 skipped: already exists
Copying blob 61f74564c3aa skipped: already exists
Copying blob d95ebdc79761 done
Copying config 40ef32b39c done
Writing manifest to image destination
Storing signatures
--> 40ef32b39cf
40ef32b39cf4ffd3d2e4e3426bec4a5ea168524f7f3fcfe863a378abd9794270 </span></pre>
</div>
<p>
Once the build is complete, the new image can be displayed with the <code>buildah images</code> command:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id34949982a73d'><button class='copyBtn' data-clipboard-target='#id34949982a73d' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah images
<span class='unselectable'>REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/hello latest 40ef32b39cf4 56 seconds ago 622 MB </span></pre>
</div>
<p>
The new image, tagged <code>hello:latest</code>, can be pushed to a remote image registry.
This is easily accomplished with the <code>buildah push</code> command.
</p>
<!-- endregion -->
<!-- #region -->
<h3 id="buildah_push"><span class="code">buildah push</span> Help</h3>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7708e8dc5f37'><button class='copyBtn' data-clipboard-target='#id7708e8dc5f37' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>man buildah-push
<span class='unselectable'>buildah-push(1) General Commands Manual buildah-push(1)<br/>
NAME
buildah-push - Push an image from local storage to elsewhere.<br/>
SYNOPSIS
buildah push [options] image [destination]<br/>
DESCRIPTION
Pushes an image from local storage to a specified destination, decompressing and recompessing layers as needed.<br/>
imageID
Image stored in local container/storage<br/>
DESTINATION
The DESTINATION is a location to store container images. If omitted, the source image parameter will be reused as
destination.<br/>
The Image "DESTINATION" uses a "transport":"details" format. Multiple transports are supported:<br/>
dir:path
An existing local directory path storing the manifest, layer tarballs and signatures as individual files. This is
a non-standardized format, primarily useful for debugging or noninvasive container inspection.<br/>
docker://docker-reference
An image in a registry implementing the "Docker Registry HTTP API V2". By default, uses the authorization state
in $XDG\_RUNTIME\_DIR/containers/auth.json, which is set using (buildah login). If the authorization state is not
found there, $HOME/.docker/config.json is checked, which is set using (docker login).
If docker-reference does not include a registry name, the image will be pushed to a registry running on local‐
host.<br/>
docker-archive:path[:docker-reference]
An image is stored in the docker save formatted file. docker-reference is only used when creating such a file,
and it must not contain a digest.<br/>
docker-daemon:docker-reference
An image _dockerreference stored in the docker daemon internal storage. If _dockerreference does not begin with a
valid registry name (a domain name containing "." or the reserved name "localhost") then the default registry name
"docker.io" will be prepended. _dockerreference must contain either a tag or a digest. Alternatively, when reading
images, the format can also be docker-daemon:algo:digest (an image ID).<br/>
oci:path:tag
An image tag in a directory compliant with "Open Container Image Layout Specification" at path.<br/>
oci-archive:path:tag
An image tag in a tar archive compliant with "Open Container Image Layout Specification" at path.<br/>
If the transport part of DESTINATION is omitted, "docker://" is assumed.<br/>
OPTIONS
--authfile path<br/>
Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json, which is set using buildah lo‐
gin. If the authorization state is not found there, $HOME/.docker/config.json is checked, which is set using
docker login.<br/>
--cert-dir path<br/>
Use certificates at path (*.crt, *.cert, *.key) to connect to the registry. Default certificates directory is
/etc/containers/certs.d.<br/>
--creds creds<br/>
The [username[:password]] to use to authenticate with the registry if required. If one or both values are not sup‐
plied, a command line prompt will appear and the value can be entered. The password is entered without echo.<br/>
--digestfile Digestfile<br/>
After copying the image, write the digest of the resulting image to the file.<br/>
--disable-compression, -D<br/>
Don't compress copies of filesystem layers which will be pushed.<br/>
--encryption-key key<br/>
The [protocol:keyfile] specifies the encryption protocol, which can be JWE (RFC7516), PGP (RFC4880), and PKCS7
(RFC2315) and the key material required for image encryption. For instance, jwe:/path/to/key.pem or pgp:admin@exam‐
ple.com or pkcs7:/path/to/x509-file.<br/>
--format, -f<br/>
Manifest Type (oci, v2s1, or v2s2) to use when saving image to directory using the 'dir:' transport (default is
manifest type of source)<br/>
--quiet, -q<br/>
When writing the output image, suppress progress output.<br/>
--remove-signatures<br/>
Don't copy signatures when pushing images.<br/>
--sign-by fingerprint<br/>
Sign the pushed image using the GPG key that matches the specified fingerprint.<br/>
--tls-verify bool-value<br/>
Require HTTPS and verify certificates when talking to container registries (defaults to true)<br/>
EXAMPLE
This example pushes the image specified by the imageID to a local directory in docker format.<br/>
# buildah push imageID dir:/path/to/image<br/>
This example pushes the image specified by the imageID to a local directory in oci format.<br/>
# buildah push imageID oci:/path/to/layout:image:tag<br/>
This example pushes the image specified by the imageID to a tar archive in oci format.<br/>
# buildah push imageID oci-archive:/path/to/archive:image:tag<br/>
This example pushes the image specified by the imageID to a container registry named registry.example.com.<br/>
# buildah push imageID docker://registry.example.com/repository:tag<br/>
This example pushes the image specified by the imageID to a container registry named registry.example.com and saves
the digest in the specified digestfile.<br/>
# buildah push --digestfile=/tmp/mydigest imageID docker://registry.example.com/repository:tag<br/>
This example works like docker push, assuming registry.example.com/my_image is a local image.<br/>
# buildah push registry.example.com/my_image<br/>
This example pushes the image specified by the imageID to a private container registry named registry.example.com
with authentication from /tmp/auths/myauths.json.<br/>
# buildah push --authfile /tmp/auths/myauths.json imageID docker://registry.example.com/repository:tag<br/>
This example pushes the image specified by the imageID and puts into the local docker container store.<br/>
# buildah push imageID docker-daemon:image:tag<br/>
This example pushes the image specified by the imageID and puts it into the registry on the localhost while turning
off tls verification.
# buildah push --tls-verify=false imageID docker://localhost:5000/my-imageID<br/>
This example pushes the image specified by the imageID and puts it into the registry on the localhost using creden‐
tials and certificates for authentication.
# buildah push --cert-dir /auth --tls-verify=true --creds=username:password imageID docker://local‐
host:5000/my-imageID<br/>
ENVIRONMENT
BUILD_REGISTRY_SOURCES<br/>
BUILD_REGISTRY_SOURCES, if set, is treated as a JSON object which contains lists of registry names under the keys
insecureRegistries, blockedRegistries, and allowedRegistries.<br/>
When pushing an image to a registry, if the portion of the destination image name that corresponds to a registry is
compared to the items in the blockedRegistries list, and if it matches any of them, the push attempt is denied. If
there are registries in the allowedRegistries list, and the portion of the name that corresponds to the registry is
not in the list, the push attempt is denied.<br/>
TMPDIR The TMPDIR environment variable allows the user to specify where temporary files are stored while pulling
and pushing images. Defaults to '/var/tmp'.<br/>
FILES
registries.conf (/etc/containers/registries.conf)<br/>
registries.conf is the configuration file which specifies which container registries should be consulted when com‐
pleting image names which do not include a registry or domain portion.<br/>
policy.json (/etc/containers/policy.json)<br/>
Signature policy file. This defines the trust policy for container images. Controls which container registries
can be used for image, and whether or not the tool should trust the images.<br/>
SEE ALSO
buildah(1), buildah-login(1), containers-policy.json(5), docker-login(1), containers-registries.conf(5)<br/>
buildah June 2017 buildah-push(1) </span></pre>
</div>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd25517095810'><button class='copyBtn' data-clipboard-target='#idd25517095810' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah run \
--entrypoint /var/lang/bin/pip \
--rm \
--user "$(id -u):$(id -g)" \
-v "$(pwd):/mnt" \
amazon/aws-lambda-python:3.8 \
install --target /mnt/build --upgrade psycopg2-binary</pre>
</div>
<!-- endregion -->
<!-- #region -->
<div class='imgWrapper imgFlex right' style=' '>
<a href='https://www.scholastic.ca/books/view/how-to-speak-dolphin' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/buildahPodman/howToSpeakDolphin.webp" type="image/webp">
<source srcset="/blog/images/buildahPodman/howToSpeakDolphin.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/buildahPodman/howToSpeakDolphin.png"
style='width: 100%; width: 25%; height: auto;'
/>
</picture>
</a>
</div>
<h2 id="howto">How To</h2>
<p>
The following was inspired by
<a href='https://github.com/groda/big_data/blob/master/docker_for_beginners.md#recap-images-and-containers' target='_blank' rel='nofollow'>Recap: images and containers</a>
from <b>Docker for beginners</b>.
The equivalent commands for Docker alternatives are shown.
</p>
<h3 class="clear" id="ver">Check software version</h3>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id908457ee393b'><span class='unselectable'>$ </span>docker -v<br><span class='unselectable'>Docker version 20.10.2, build 20.10.2-0ubuntu1~20.10.1 </span></pre>
</div>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id974af49264a3'><span class='unselectable'>$ </span>buildah -v<br><span class='unselectable'>buildah version 1.15.2 (image-spec 1.0.1, runtime-spec 1.0.2-dev) </span></pre>
</div>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id1d89b7ea7381'><span class='unselectable'>$ </span>podman -v<br><span class='unselectable'>podman version 2.0.6 </span></pre>
</div>
<div class='imgWrapper imgFlex right' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/buildahPodman/aws_linux.webp" type="image/webp">
<source srcset="/blog/images/buildahPodman/aws_linux.png" type="image/png">
<img
class="imgImg "
src="/blog/images/buildahPodman/aws_linux.png"
style='width: 100%; width: 25%; height: auto; margin-bottom: 1em;'
/>
</picture>
</div>
<h3 id="dlimg">Download the Amazon Linux 2 image</h3>
<p>
AWS Lambda functions run under Amazon Linux.
</p>
<p>
Each of these 3 commands does a very similar task, downloading a specific image.
Docker uses different subdirectories for images than Buildah and <code>podman</code> do.
</p>
<div class="clear">
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id790368c5c197'><span class='unselectable'>$ </span><span class="bg_yellow">sudo</span> docker pull amazonlinux
<span class='unselectable'>Using default tag: latest
latest: Pulling from library/amazonlinux
3c2c91c7c431: Pull complete
Digest: sha256:06b9e2433e4e563e1d75bc8c71d32b76dc49a2841e9253746eefc8ca40b80b5e
Status: Downloaded newer image for amazonlinux:latest
docker.io/library/amazonlinux:latest </span></pre>
</div>
</div>
<p>
Buildah works without complaint.
</p>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6c5221aabd4b'><span class='unselectable'>$ </span>buildah pull amazonlinux
<span class='unselectable'>53ef897d731f9a5673c083d0e86d7911f85d6e63bb2be2346b17bdbacdc58637 </span></pre>
</div>
<p>
<code>podman</code> seems to hiccup and then complete successfully.
</p>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3b3536bfefd3'><span class='unselectable'>$ </span>podman pull amazonlinux
<span class='unselectable'>Trying to pull quay.io/amazonlinux...
error parsing HTTP 404 response body: invalid character '<' looking for beginning of value: "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n<title>404 Not Found</title>\n<h1>Not Found</h1>\n<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>\n"
Trying to pull docker.io/library/amazonlinux...
Getting image source signatures
Copying blob 3c2c91c7c431 [--------------------------------------] 0.0b / 0.0b
Copying config 53ef897d73 done
Writing manifest to image destination
Storing signatures
53ef897d731f9a5673c083d0e86d7911f85d6e63bb2be2346b17bdbacdc58637
</span></pre>
</div>
<div class='imgWrapper imgFlex right' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/buildahPodman/bash-logo.webp" type="image/webp">
<source srcset="/blog/images/buildahPodman/bash-logo.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/buildahPodman/bash-logo.png"
style='width: 100%; width: 35%; height: auto; margin-bottom: 1em;'
/>
</picture>
</div>
<h3 id="echo">Run a Bash Command in an OCI Container</h3>
<p>
Again, <code>Docker</code> must be run as root for this operation, this represents an unnecessary security risk.
</p>
<div class="clear">
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide63004af06ff'><span class='unselectable'>$ </span><span class="bg_yellow">sudo</span> docker container run amazonlinux echo 'Hello World!'
<span class='unselectable'>Hello World! </span></pre>
</div>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id3d0e75dea39a'><span class='unselectable'>$ </span>podman container run amazonlinux <a href='https://opensource.com/article/18/6/linux-version#how-to-find-the-linux-kernel-version' target='_blank' rel='nofollow'>cat /etc/os-release</a>
<span class='unselectable'>VERSION="2"
ID="amzn"
ID_LIKE="centos rhel fedora"
VERSION_ID="2"
PRETTY_NAME="Amazon Linux 2"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2"
HOME_URL="https://amazonlinux.com/" </span></pre>
</div>
</div>
<div class='imgWrapper imgFlex right' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/buildahPodman/kodak-carousel-projector.webp" type="image/webp">
<source srcset="/blog/images/buildahPodman/kodak-carousel-projector.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/buildahPodman/kodak-carousel-projector.png"
style='width: 100%; width: 35%; height: auto; margin-bottom: 1em;'
/>
</picture>
</div>
<h3 id="localImages">Show All Locally Available Images</h3>
<p>
Again, <code>Docker</code> must be run as root for this operation, this represents an unnecessary security risk.
</p>
<div class="clear"><div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd7d19abd1057'><span class='unselectable'>$ </span><span class="bg_yellow">sudo</span> docker images
<span class='unselectable'>REPOSITORY TAG IMAGE ID CREATED SIZE
amazonlinux latest 53ef897d731f 21 hours ago 163MB </span></pre>
</div>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idd819d1a65c22'><span class='unselectable'>$ </span>podman images
<span class='unselectable'>REPOSITORY TAG IMAGE ID CREATED SIZE
localhost/hello latest 40ef32b39cf4 5 hours ago 622 MB
docker.io/library/amazonlinux latest 53ef897d731f 21 hours ago 170 MB </span></pre>
</div>
</div>
<div class='imgWrapper imgFlex right' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/buildahPodman/containerShip.webp" type="image/webp">
<source srcset="/blog/images/buildahPodman/containerShip.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/buildahPodman/containerShip.png"
style='width: 100%; width: 35%; height: auto; margin-bottom: 1em;'
/>
</picture>
</div>
<h3 id="listCont">List OCI Containers</h3>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida6129e334a00'><span class='unselectable'>$ </span><span class="bg_yellow">sudo</span> docker container ls<br><span class='unselectable'>CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES </span></pre>
</div>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9dccc466975c'><span class='unselectable'>$ </span>podman container ls<br><span class='unselectable'>CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES </span></pre>
</div>
<h3 id="listcont4">View All OCI Containers (Running or Not)</h3>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id52a99018ced9'><span class='unselectable'>$ </span><span class="bg_yellow">sudo</span> docker container ls -a
<span class='unselectable'>CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
250f56d9aced amazonlinux "echo 'Hello World!'" 14 minutes ago Exited (0) 14 minutes ago competent_einstein </span></pre>
</div>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id629a2180d0f2'><span class='unselectable'>$ </span>podman container ls -a
<span class='unselectable'>CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0f8203e9d3b8 docker.io/library/amazonlinux:latest echo Hello world! 36 minutes ago Exited (0) 36 minutes ago
beautiful_mestorf
14282ace8978 docker.io/library/amazonlinux:latest echo Hello world! 36 minutes ago Exited (0) 36 minutes ago
beautiful_goldwasser
1b9a8db52fb9 docker.io/library/alpine:latest echo Hello World! About an hour ago Exited (0) About an hour ago zealous_easley
6444ee144488 docker.io/library/amazonlinux:latest echo Hello World! 12 minutes ago Exited (0) 12 minutes ago
frosty_ritchie
7444122cbc59 docker.io/library/alpine:latest cat /etc/motd About an hour ago Exited (0) About an hour ago elated_sammet
aef84973d6ad docker.io/library/amazonlinux:latest echo Hello world! About an hour ago Exited (0) About an hour ago lucid_sinoussi
e210f74bc209 docker.io/library/amazonlinux:latest cat /etc/motd About an hour ago Exited (0) About an hour ago jovial_borg </span></pre>
</div>
<h3 id="listCont3">List Running OCI containers</h3>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id70ca4cd09e13'><span class='unselectable'>$ </span><span class="bg_yellow">sudo</span> docker container ps -a
<span class='unselectable'>CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
250f56d9aced amazonlinux "echo 'Hello World!'" 5 minutes ago Exited (0) 5 minutes ago competent_einstein </span></pre>
</div>
<p>
<code>podman</code> has a problem with the <code>container ps</code> sub-subcommand.
</p>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idf0cd3c20dacd'><span class='unselectable'>$ </span>podman container ps -a
<span class='unselectable'>Error: unrecognized command `podman container ps`
Try 'podman container --help' for more information. </span></pre>
</div>
<h3 id="buildah_push_use"><span class="code">buildah push</span> to Docker Daemon</h3>
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/buildahPodman/containerShipTugboat.webp" type="image/webp">
<source srcset="/blog/images/buildahPodman/containerShipTugboat.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/buildahPodman/containerShipTugboat.png"
style='width: 100%; '
/>
</picture>
</div>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id6e35eb618951'><button class='copyBtn' data-clipboard-target='#id6e35eb618951' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah push hello:latest docker-daemon:hello:latest
<span class='unselectable'>Getting image source signatures
Copying blob sha256:72fcdba8cff9f105a61370d930d7f184702eeea634ac986da0105d8422a17028
247.02 MiB / 247.02 MiB [==================================================] 2s
Copying blob sha256:e567905cf805891b514af250400cc75db3cb47d61219750e0db047c5308bd916
144.75 MiB / 144.75 MiB [==================================================] 1s
Copying config sha256:6d54bef73e638f2e2dd8b7bf1c4dfa26e7ed1188f1113ee787893e23151ff3ff
1.59 KiB / 1.59 KiB [======================================================] 0s
Writing manifest to image destination
Storing signatures </span>
<span class='unselectable'>$ </span>buildah images | head -n2
<span class='unselectable'>REPOSITORY TAG IMAGE ID CREATED SIZE
docker.io/hello latest 6d54bef73e63 2 minutes ago 398 MB </span>
<span class='unselectable'>$ </span>buildah run -t hello:latest
<span class='unselectable'>Hello, world! </span></pre>
</div>
<h3 id="buildah_rmi">Delete an OCI Image</h3>
<div class='imgWrapper imgFlex inline fullsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/buildahPodman/containerSky.webp" type="image/webp">
<source srcset="/blog/images/buildahPodman/containerSky.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/buildahPodman/containerSky.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
Delete an OCI image in Buildah's <code>~/.local/share/container</code> directory with the <code>rmi</code> subcommand:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb7ac063b185b'><button class='copyBtn' data-clipboard-target='#idb7ac063b185b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>buildah rmi e12ea62c5582
<span class='unselectable'>e12ea62c5582f91a2228e3e284ea957f2df4f1cdb150fd2c189ef8f11d7633ce </span></pre>
</div>
<!-- endregion -->
Stack Overflow Culture: Zero-Sum, Authoritarian and Hormonally Imbalanced2021-04-18T00:00:00-04:00https://mslinn.github.io/blog/2021/04/18/so-culture<p>
This blog post describes some problems that significantly impact
<a href='https://www.crunchbase.com/organization/stack-overflow' target='_blank' rel='nofollow'>Stack Overflow</a>
users, and offers suggestions for improvement.
If you are unfamiliar with Stack Overflow, or would like to read a summary of what this blog post is based on,
Kevin Workman wrote a terrific summary in March 2019 entitled
“<a href='https://happycoding.io/blog/stack-overflow-culture-wars' target='_blank' rel='nofollow'>The Stack Overflow Culture Wars</a>”.
Very little has changed since then.
</p>
<div class='imgWrapper imgFlex center halfsize' style=' '>
<a href='https://www.stackoverflow.com' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/stackOverflow/stackoverflowLogo.webp" type="image/webp">
<source srcset="/blog/images/stackOverflow/stackoverflowLogo.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/stackOverflow/stackoverflowLogo.png"
style='width: 100%; padding: 20px;'
/>
</picture>
</a>
</div>
<fold-article intro>
<h2 id="intro">Introduction</h2>
<p>
<a href='https://www.stackoverflow.com' target='_blank' rel='nofollow'>Stack Overflow</a> is the premier website world-wide for programmers
to help each other by asking and answering questions.
It has a defined protocol for this type of interaction, however new user on-boarding is often ineffective,
so newcomers are not properly informed,
and old-timers often do not exhibit appropriate people skills.
The <a href='https://games.greggman.com/game/done-with-stackoverflow/' target='_blank' rel='nofollow'>protocol is somewhat misguided</a>
and appropriate tools for better interaction are not provided.
</p>
<p>
As a result, this website has developed a well-documented reputation for being
“<a href='https://codeblog.jonskeet.uk/2018/03/17/stack-overflow-culture/' target='_blank' rel='nofollow'>a valuable resource, but a scary place to contribute due to potential hostility.</a>”
</p>
<div class="pullQuote">
Sometimes, loving something means caring enough to admit that it has a problem.<br><br>
– <a href='https://stackoverflow.blog/2018/04/26/stack-overflow-isnt-very-welcoming-its-time-for-that-to-change/' target='_blank' rel='nofollow'>Jay Hanlon</a>,
writing about Stack Overflow when he was EVP of Culture and Experience.
</div>
</fold-article>
<fold-article problem>
<div class="clear quote">
Stack Overflow suffers from militant moderators who close and delete reasonable submissions and answers due to Draconian rules.<br><br>
– <code>sleavey</code> commenting on a Hacker News thread entitled
<a href='https://news.ycombinator.com/item?id=16610353' target='_blank' rel='nofollow'>Stack Overflow Culture</a>.
</div>
</fold-article>
<fold-article top>
<h2 id="topdown">Change Starts At the Top</h2>
<h3 id="Chandrasekar">Prashanth Chandrasekar, CEO</h3>
<p>
Prashanth Chandrasekar became CEO of Stack Overflow in September, 2019.
<a href='https://www.intercom.com/blog/podcasts/prashanth-chandrasekar-on-writing-the-script-of-the-future/' target='_blank' rel='nofollow'>Inside Intercom interviewed Mr. Chandrasekar</a>
in August 2020.
This puff piece made no mention of any cultural problems.
The focus was on the brilliance of Stack Overflow’s technology.
If Mr. Chandrasekar has a vision for how to guide social change, he did not mention it.
Instead, in the article and his publications since then he only speaks publicly about
<a href='https://stackoverflow.blog/author/pchandrasekar/' target='_blank' rel='nofollow'>transitioning Stack Overflow to a product-led SaaS company</a>.
</p>
<p>
Shortly after becoming CEO, Mr. Chandrasekar published
“<a href='https://stackoverflow.blog/2020/01/21/scripting-the-future-of-stack-2020-plans-vision/' target='_blank' rel='nofollow'>Scripting the Future of Stack Overflow</a>,
in which he wrote:
</p>
<p class="quote">
We learned that we needed much better channels to listen to our moderators and community members.
We have not evolved the existing channels of engagement for power users in our community, like Meta,
or articulated how we intended to make improvements going forward.
This has caused friction as our user base and business have rapidly grown.
We acknowledge these issues, apologize for our mistakes, and have plans for improving in the future.
</p>
<p>
Later in the article, he mentioned improvements to the code of conduct, a survey and establishing a moderator council.
More than 2 years later, none of this has made the slightest difference in Stack Overflow's culture.
</p>
<h3 id="pathak">Mihir Pathak — EVP, Strategy & Transformation</h3>
<p>
Prior to his employment at Stack Overflow,
Mr. Pathak was a derivatives strategist McKinsey & Company,
a <a href='https://www.mckinsey.com/business-functions/organization/our-insights/the-four-building-blocks--of-change#' target='_blank' rel='nofollow'>world-renouned change management company</a>.
That is to say, although Mr. Pathak worked at McKinsey, he was not there to assist other companies make structural changes;
instead, he was responsible for pricing methodologies and hedging techniques underlying financial derivative products
and options trading strategies – a heads-down money manager.
</p>
<p>
<a href='https://stackoverflow.com/company/leadership/mihir-pathak' target='_blank' rel='nofollow'>Mr. Pathak’s page at StackOverflow</a> is no longer available.
<a href='https://webcache.googleusercontent.com/search?q=cache:1eKtOpLW2VoJ:https://stackoverflow.com/company/leadership/mihir-pathak' target='_blank' rel='nofollow'>Google cached it</a>.
No announcement has been made about his departure or if there will be a replacement.
</p>
<p>
Mr. Pathak's job description is still visible at <a href='https://www.themuse.com/profiles/stackoverflow' target='_blank' rel='nofollow'><code>themuse.com</code></a>:
</p>
<p class="quote">
Mihir is responsible for the long-term business strategy of Stack Overflow,
which includes forming partnerships with like-minded organizations and understanding how to best serve the needs of future developers.
</p>
<p>
Mr. Pathak was employed from September 2016 at Stack Overflow as a business development executive, not a change management champion.
</p>
<h3 id="dietrich">Teresa Dietrich, Chief Product and Technology Officer</h3>
<p>
In January 2020 <a href='https://www.crunchbase.com/person/teresa-dietrich' target='_blank' rel='nofollow'>Teresa Dietrich</a> was made Chief Product and Technology Officer.
She also came from McKinsey & Company, where she was Global Head of Product & Engineering.
Two months after she took the job she wrote
“<a href='https://stackoverflow.blog/2020/02/25/sharing-our-first-quarter-2020-community-roadmap/' target='_blank' rel='nofollow'>Sharing our first quarter 2020 community roadmap</a>”.
That rather bland article did not seem to indicate any recognition of serious problems within the Stack Overflow culture,
and I could not find any publicly available results of the studies that were mentioned.
</p>
<p>
16 months after Ms. Dietrich started her job, I do not see any cultural change.
Has Ms. Dietrich been tasked with leading such a change?
If so:
</p>
<ul>
<li>Does she have board-level support?</li>
<li>Does she have what she needs to make significant cultural changes?</li>
<li>
Why has she not made any public acknowledgement of a cultural problem?
One possible answer is that her boss, Mr. Chandrasekar, has not done so.
</li>
<li>
Is Ms. Dietrich the right person for the job?
This is not a purely technical problem, it is a social problem.
I believe that women in general possess more highly developed social skills,
but the skills necessary to climb to the top are not the skills required to make this type of cultural shift.
</li>
</ul>
<div class="pullQuote">
Are Mr. Chandrasekar and Ms. Dietrich part of the cultural problem, or part of the solution?
</div>
</fold-article>
<fold-article women>
</fold-article>
<fold-article me>
<h2 id="me">Where Am I Coming From?</h2>
<p>
I have no agenda, no investment in the status quo, nothing to prove, no contacts at the company,
I am not an undercover journalist,
and I am not a competitor or investor.
I am just Joe User... and I am not shy when I believe I have something that I think needs to be heard.
</p>
<p>
If I mispeak, please tell me.
If I missed something, again please tell me.
If there is a bigger picture I would like to learn about it.
</p>
<p>
I have used Stack Overflow and its <a href='https://stackexchange.com/sites' target='_blank' rel='nofollow'>sibling websites</a> for more than 10 years.
Until a few weeks ago, I contributed a few answers here and there, and otherwise did not spend much time on the sites.
All contributors are volunteers, so the only reasons to contribute are for prestige, social bonding and altruism.
</p>
<p>
For almost 30 days, in my spare time, I helped people who asked questions on Stack Overflow.
I am writing this blog post because although I enjoy helping others,
the enjoyment I experienced while doing so within the Stack Overflow environment was overshadowed by the regressive behavior
tolerated and even enforced by other contributors.
I should also say that I did encounter certain other contributors with whom interaction was very pleasant.
However, interactions with alpha contributors with the highest scores seemed more often than not to be quite unpleasant.
Since I first published this blog post, I have mostly not interacted on the site.
I remain open to contributing to improvements in the culture.
</p>
<p>
The next image is provided so readers know that I am able to effectively participate in the current Stack Overflow website.
</p>
<div class='imgWrapper imgBlock center halfsize' style=' '>
<figure>
<picture class='imgPicture'>
<source srcset="/blog/images/stackOverflow/stackOverflow.webp" type="image/webp">
<source srcset="/blog/images/stackOverflow/stackOverflow.png" type="image/png">
<img alt='The volume of accepted and upvoted answers put me in the top 0.14% of Stack Overflow answerers in under 30 days.'
class="imgImg rounded shadow"
src="/blog/images/stackOverflow/stackOverflow.png"
style='width: 100%; '
title='The volume of accepted and upvoted answers put me in the top 0.14% of Stack Overflow answerers in under 30 days.'
/>
</picture>
<figcaption class='imgFigCaption halfsize'>
The volume of accepted and upvoted answers put me in the top 0.14% of Stack Overflow answerers in under 30 days.
</figcaption>
</figure>
</div>
<p>
Over 100 of the answers I offered in a 30-day period were accepted as the preferred answer.
In fact, most of my answers were selected as the preferred answer.
That means many more alternative answers were not accepted.
</p>
<p>
After losing, sometimes a contributor will delete their post.
I have done it myself, when the winning post was significantly better by all measures.
</p>
<p>
Some of the other contributors who had provided alternative answers that were not selected clearly felt they had lost a competition.
This structure, and others that I describe below, define a system designed to generate hostility.
Stack Overflow, as currently implemented,
<a href='https://en.wikipedia.org/wiki/Dominance_hierarchy' target='_blank' rel='nofollow'>promotes dominance behavior</a>,
which for most primates (other than <a href='https://www.scientificamerican.com/article/bonobo-sex-and-society-2006-06/' target='_blank' rel='nofollow'>bonobos</a>) is patriarchal.
</p>
</fold-article>
<fold-article gamify>
<div class='imgWrapper imgFlex right' style=' '>
<a href='http://localhost:4001/blog/2021/04/18/so-culture.html' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/stackOverflow/codinghorror-app-icon.webp" type="image/webp">
<source srcset="/blog/images/stackOverflow/codinghorror-app-icon.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/stackOverflow/codinghorror-app-icon.png"
style='width: 100%; '
/>
</picture>
</a>
</div>
<h2 id="gamification">Gamification</h2>
<p>
Stack Overflow's success has be in part due to its successful <a href='https://en.wikipedia.org/wiki/Gamification' target='_blank' rel='nofollow'>gamification</a>
of the interaction between questioners and answerers.
Gamification is powerful and addictive.
Unfortunately, the model chosen resembles FPS (<a href='https://en.wikipedia.org/wiki/First-person_shooter' target='_blank' rel='nofollow'>first-person shooter</a>) games,
instead of co-operative games.
</p>
<p>
Jeff Atwood, one of the two original authors of Stack Overflow, wrote an article in 2011 entitled
<a href='https://blog.codinghorror.com/the-gamification/' target='_blank' rel='nofollow'>The Gamification</a>, in which he writes:
</p>
<div class="quote">
Gaming elements are not tacked on to the Stack Exchange Q&A engine, but a natural and essential element of the design.
</div>
<p>
Just below that sentence, Mr. Atwood shows a screenshot from an FPS video game:
</p>
<div class='imgWrapper imgBlock center halfsize' style=' '>
<figure>
<a href='https://blog.codinghorror.com/the-gamification/' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/stackOverflow/fps.webp" type="image/webp">
<source srcset="/blog/images/stackOverflow/fps.png" type="image/png">
<img alt='FPS Game screenshot from 'The Gamification'.'
class="imgImg rounded shadow"
src="/blog/images/stackOverflow/fps.png"
style='width: 100%; '
title='FPS Game screenshot from 'The Gamification'.'
/>
</picture>
</a>
<figcaption class='imgFigCaption halfsize'>
<a href="https://blog.codinghorror.com/the-gamification/" target='_blank' >
FPS Game screenshot from 'The Gamification'.
</a>
</figcaption>
</figure>
</div>
<div class="quote">
I haven't ever quite come out and said it this way, but … I played a lot of Counter-Strike from 1998 to 2001,
and Stack Overflow is in many ways my personal Counter-Strike.<br><br>
– Jeff Atwood, from “The Gamification”
</div>
<p>
FPS games are structured so the player only wins by killing others.
This is an entirely different motivational structure than a scenario where a person only wins if others succeed.
</p>
</fold-article>
<fold-article terms>
<h2 id="terms">Terminology</h2>
<p>
I use a few non-standard terms in this blog post:
</p>
<dl>
<dt>Questioner</dt>
<dd>One who provides a question</dd>
<dt>Answerer</dt>
<dd>One who provides an answer</dd>
<dt>Downvoter</dt>
<dd>One who downvotes another person's contribution</dd>
<dt>Downvotee</dt>
<dd>One who has their contribution downvoted</dd>
</dl>
</fold-article>
<fold-article downvote>
<h2 id="dialog">Dialog Improves Most Questions</h2>
<p>
A small percentage of questions asked on Stack Overflow are unambiguous, contain all the necessary information,
and are phrased well enough to be understood.
For these questions, answers can be posted without any interaction between questioner and potential answerers.
</p>
<p>
However, most questions involve a dialog between potential answerers and the questioner.
In the dialog, the question is refined, and the questioner's code and any other relevant data is elicited and provided.
The tools provided for such a dialog are unfortunately problematic.
</p>
<p>
The only two mechanisms for interaction between questioner and potential answerers are comments and answers.
Comments have severe limitations that greatly reduce their effectiveness for eliciting information from a questioner:
</p>
<ul>
<li>Comments must be very short</li>
<li>Comments cannot be formatted properly</li>
<li>Comments cannot be edited for more than a few minutes</li>
</ul>
<p>
This means that answerers who are trying to explain something to the questioner to elicit more information,
or guide the questioner towards understanding their problem better,
must resort to posting an incomplete answer.
Posting an incomplete answer is risky; other potential answerers can attack the answer by downvoting it.
</p>
<div class="pullQuote">
Downvotes typically last forever
</div>
</fold-article>
<fold-article incentives>
<h2 class="clear" id="broken">Some Incentives Promote Hostility</h2>
<p>
Many long-time users have completely objectified other users, and act as if Stack Overflow is a video game.
Points are accumulated, and at any given time there are a finite number of questions to answer.
A person's reputation on Stack Overflow is represented by a single number, which is the number of points they have accumulated.
This single number is the structural source of many problems.
A more nuanced reputation score would be a giant step forward.
</p>
<p>
Many of these long-term answerers have come to view their participation on Stack Overflow as a zero-sum competition;
they can only win (that is, have their answer accepted) if everyone else loses (that is, no-one else provides an answer that is accepted).
</p>
<p>
Some answerers employ intimidation order to suppress competition.
Downvotes are often used in the same way against other answerers as a
<a href='https://en.wikipedia.org/wiki/Brushback_pitch' target='_blank' rel='nofollow'>brush back pitch</a> in baseball.
</p>
<div class="pullQuote">
Downvoting has no negative consequences for the downvoter
</div>
<p>
Standard operating procedure for competitively-minded answerers is to downvote answers from others at every opportunity.
There is no risk in downvoting:
</p>
<ul>
<li>Downvotes are untraceable; there is no public record of who downvoted or when a downvote was cast.</li>
<li>Downvotes are free to downvoters; this encourages liberal downvoting.</li>
</ul>
<p>
This scenario incentivizes competitively-minded answerers to strafe the competition with downvotes at every opportunity.
</p>
<div class="pullQuote">
If downvotes cost the downvoter the same number of points as they penalize the downvotee, then downvotes would become much rarer.
</div>
<p>
Downvotes typically last forever.
Yes, a downvoter could theoretically reverse a downvote,
but it is awkward for them to find their old downvotes, and there is no incentive to do so.
</p>
<p>
Questions from newcomers are also frequently downvoted, without discussion, or along with hostile remarks.
That leaves a permanent impression, and tends to select for new members who are comfortable with dominance-based hostility.
This is a self-perpetuating, and highly toxic, social order.
</p>
<div class='imgWrapper imgBlock center halfsize' style=' '>
<figure>
<a href='https://www.focusforhealth.org/how-toxic-masculinity-harms-men-and-society-as-a-whole/' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/stackOverflow/toxicMasulinity.webp" type="image/webp">
<source srcset="/blog/images/stackOverflow/toxicMasulinity.png" type="image/png">
<img alt='Toxic Masculinity Harms Men and Society As A Whole, from Focus for Health'
class="imgImg rounded shadow"
src="/blog/images/stackOverflow/toxicMasulinity.png"
style='width: 100%; '
title='Toxic Masculinity Harms Men and Society As A Whole, from Focus for Health'
/>
</picture>
</a>
<figcaption class='imgFigCaption halfsize'>
<a href="https://www.focusforhealth.org/how-toxic-masculinity-harms-men-and-society-as-a-whole/" target='_blank' >
Toxic Masculinity Harms Men and Society As A Whole, from Focus for Health
</a>
</figcaption>
</figure>
</div>
</fold-article>
<fold-article suggest_onboard>
<div class='imgWrapper imgFlex right quartersize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/stackOverflow/stackOverflowHelp.webp" type="image/webp">
<source srcset="/blog/images/stackOverflow/stackOverflowHelp.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/stackOverflow/stackOverflowHelp.png"
style='width: 100%; '
/>
</picture>
</div>
<h2 id="onboard">Suggestion: Gamified Onboarding</h2>
<p>
The only information for new users that is directly accessible from the Stack Overflow front page, is the Help Center, under the question mark icon.
It is obvious from the often very polite and tentative inquiries made by new users when they ask their first question that they never noticed that information, or if they did it did not seem relevant.
... and then those new users are mercilessly slammed.
</p>
<p>
New users should be presented with a
<a href='https://www.appcues.com/blog/the-5-best-user-onboarding-experiences' target='_blank' rel='nofollow'>short instructional question and answer-style introduction</a>,
where information is provided on how to be a good Stack Overflow user.
This should happen before they get the opportunity to post their first question.
Different levels of users should be explained.
Although Stack Overflow is all-English, the onboarding should be multilingual.
</p>
</fold-article>
<fold-article suggest_multilingual>
<h2 id="multling">Suggestion: Multilingual Support</h2>
<p>
A high percentage of users do not speak English very well.
They really struggle, and tolerance is low on Stack Overflow for bad English.
Other sites, for example Facebook and LinkedIn, have a translation facility built right in.
I think Facebook did a particularly good job.
Why not do something similar on StackOverflow.com?
English would be the official language, but everyone world-wide would be able to interact much more effectively.
</p>
<div class="pullQuote">
This site is multilingual.<br/>
It is not that hard to do!
</div>
<p>
Machine translation is really quite good.
I have it on this site.
Want to view this site in one of over 100 languages?
Just <a href='#body'>go to the top of this page</a> and click on this pull-down menu labeled <b>Select Language</b>:
</p>
<div class='imgWrapper imgFlex center halfsize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/stackOverflow/selectLanguage.webp" type="image/webp">
<source srcset="/blog/images/stackOverflow/selectLanguage.png" type="image/png">
<img
class="imgImg "
src="/blog/images/stackOverflow/selectLanguage.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
You will then see the list of languages that you can view this website in:
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/stackOverflow/selectLanguages.webp" type="image/webp">
<source srcset="/blog/images/stackOverflow/selectLanguages.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/stackOverflow/selectLanguages.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
Morally speaking, not to provide a multilingual site discriminates against non-English-speaking people.
</p>
</fold-article>
<fold-article suggest_rep>
<h2 id="elicitation">Suggestion: More Nuanced Reputation Score</h2>
<p>
Instead of a single metric, answerers should be rated along several dimensions.
Economists and psychologists both use multidimensional diagrams.
The following diagram represents data in 4 dimensions.
More dimensions can easily be shown in this type of diagram.
</p>
<div class='imgWrapper imgBlock inline fullsize' style=' '>
<figure>
<picture class='imgPicture'>
<source srcset="/blog/images/stackOverflow/multiDimentionalPlot.webp" type="image/webp">
<source srcset="/blog/images/stackOverflow/multiDimentionalPlot.png" type="image/png">
<img alt='Multi-dimensional data can easily be visualized by outlined shapes'
class="imgImg rounded shadow"
src="/blog/images/stackOverflow/multiDimentionalPlot.png"
style='width: 100%; '
title='Multi-dimensional data can easily be visualized by outlined shapes'
/>
</picture>
<figcaption class='imgFigCaption fullsize'>
Multi-dimensional data can easily be visualized by outlined shapes
</figcaption>
</figure>
</div>
<p>
Instead of "bigger is better" (a single number indicating status, with the high score indicating alpha status),
more information would allow for more detail,
so a fuzzy diagram would show little interaction,
while a highly detailed and intricate design would indicate a lot of participation.
Some answerers might be stronger regarding some metrics, while being weaker on other metrics.
</p>
<p>
Instead of displaying a person's score, the shape of their participation would be shown.
Some people prefer to be seen as well-rounded, others prefer to be the best on selected aspects and ignore the others.
One size does not fit all.
</p>
<p>
People would start to give pet names for various shapes.
Jokes would be made about the shapes.
</p>
<p>
HR personnel would start to hire teams based on how well these shapes meshed together.
Money will be made by timely entrepreneurs because these shapes will quickly be adopted industry-wide.
Some will aspire to change their shape.
</p>
<p>
An entire industry will spring up servicing those who wish to modify their shape.
</p>
</fold-article>
<fold-article suggest_active>
<h2 id="restrict_down">Suggestion: Encourage and Highlight Elicitation</h2>
<p>
<a href='https://en.wikipedia.org/wiki/Elicitation_technique' target='_blank' rel='nofollow'>Elicitation</a> is a difficult skill to master.
The current high-scorers have no incentives to employ elicitation, and they act as if on a campaign to eradicate it.
</p>
<div class="pullQuote">
Introduce an Elicitation Phase
</div>
<p class="clear">
A button should be introduced that lets everyone who visits the question page that a potential answerer would like to elicit information.
While at least one such button is enabled, no downvotes are possible relating to the question,
and the question cannot be closed or moved to another forum by anyone, regardless of their privilege level.
</p>
<p>
The elicitation mode would time out, but not suddenly or unexpectedly.
Instead, it would back off, rather like the Ethernet back-off algorithm for collision resolution used in random access MAC protocols.
</p>
<p>
Both the potential answerer and the questioner would be given cues that they have a question or a response waiting,
and after a period of inactivity the special status ends.
I leave the details of the timing undefined for others to discuss.
</p>
</fold-article>
<fold-article suggest_crowd>
<h2 class="restrict_down" id="crowd">Suggestion: Restrict Downvoting</h2>
<p>
Downvoting needs to incorporate:
</p>
<ul>
<li>Accountability (no more anonymous downvoting)</li>
<li>Cost (no more drive-by shootings without consequences)</li>
<li>Time window (vote after the dust settles, not during the elicitation phase)</li>
<li>Public displays of user profiles should prominently display that user's downvotes and upvotes, with links</li>
<li>Aggregate statistics on user profiles of their percentage downvotes and upvotes, trends (absolute and relative), etc.</li>
</ul>
</fold-article>
<fold-article suggest_crowd>
</fold-article>
Serverless E-Commerce2021-04-14T00:00:00-04:00https://mslinn.github.io/blog/2021/04/14/serverless-ecommerce<style>
h2.numbered:before {
color: darkgreen;
content: "Option " counter(h2counter) ":\A0\A0\A0";
}
</style>
<!-- #region -->
<p>
As readers of this blog know, I have been
<a href='/django/index.html'>chronicling my adventure into Python-powered e-commerce</a>
for several months.
I have been focusing on Django in general, and <a href='https://django-oscar.readthedocs.io' target='_blank' rel='nofollow'>Django-Oscar</a> in particular.
Webapps made with this technology are almost exclusively run on dedicated real or virtual machines.
<a href='https://www.cloudflare.com/learning/serverless/what-is-serverless/' target='_blank' rel='nofollow'>Serverless</a>
computing is a method of providing backend services on an as-used basis.
AWS Lambda is the best-known example of serverless computing, and it combines nicely with a CDN like AWS CloudFront.
</p>
<p>
This blog post discusses 3 goals for an e-commerce system.
Two goals are provided by the technology behind
<a href='https://martinfowler.com/articles/serverless.html' target='_blank' rel='nofollow'>serverless webapps</a>:
</p>
<ol>
<li>Enormous and instantaneous scalability.</li>
<li>Pay-as-you-go without an up-front cost commitment.</li>
</ol>
<p>
I have one more goal: very low latency for online shoppers.
</p>
<!-- endregion -->
<!-- #region -->
<h2 id="big">The Big Picture</h2>
<p class="quote">
AWS Lambda consists of two main parts:
the Lambda service which manages the execution requests,
and the Amazon Linux micro virtual machines provisioned using AWS Firecracker,
which actually runs the code.
<br><br>
A Firecracker VM is started the first time a given Lambda function receives an execution request (the so-called “Cold Start”),
and as soon as the VM starts, it begins to poll the Lambda service for messages.
When the VM receives a message, it runs your function code handler, passing the received message JSON to the function as the event object.
<br><br>
Thus every time the Lambda service receives a Lambda execution request, it checks if there is a Firecracker microVM available to manage the execution request.
If so, it delivers the message to the VM to be executed.
<br><br>
In contrast, if no available Firecracker VM is found, it starts a new VM to manage the message.
Each VM executes one message at a time, so if a lot of concurrent requests are sent to the Lambda service,
for example due to a traffic spike received by an API gateway,
several new Firecracker VMs will be started to manage the requests and the average latency of the requests will be higher since each VM takes roughly a second to start.
<br><br>
– From <a href='https://www.proud2becloud.com/how-to-run-any-programming-language-on-aws-lambda-custom-runtimes/' target='_blank' rel='nofollow'>How to run any programming language on AWS Lambda: Custom Runtimes</a> by Matteo Moroni.
</p>
<!-- endregion -->
<!-- #region -->
<h2 id="lambdalimits">AWS Lambda Limits</h2>
<p>
AWS Lambda programs have access to considerable resources, enough for most e-commerce stores.
The AWS Lambda runtime environment has the following limitations, some of which can be improved upon with some work:
</p>
<ul>
<li>The disk space (ephemeral) is limited to 512 MB.</li>
<li>The default deployment package size is 50 MB.</li>
<li>The memory range is from 128 to 3008 MB.</li>
<li>The maximum execution timeout for a function is 15 minutes.</li>
<li>Request and response (synchronous calls) body payload size can be up to 6 MB.</li>
<li>Event request (asynchronous calls) body can be up to 128 KB.</li>
</ul>
<!-- endregion -->
<!-- #region -->
<div class='imgWrapper imgFlex right quartersize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/django/clouds.webp" type="image/webp">
<source srcset="/blog/images/django/clouds.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/django/clouds.png"
style='width: 100%; '
/>
</picture>
</div>
<h2 id="cf">CloudFront</h2>
<p>
<a href='https://forum.djangoproject.com/u/briancaffey/summary' target='_blank' rel='nofollow'>Brian Caffey</a> wrote
<a href='https://forum.djangoproject.com/t/building-a-django-application-on-aws-with-cloud-development-kit-cdk/2830' target='_blank' rel='nofollow'>Building a Django application on AWS with Cloud Development Kit (CDK)</a>.
The website Mr. Caffey's article discusses does not use Lambda, instead his website is always running.
So, this option is quite informative and well-thought-out,
but it is AWS-specific and does not discuss serverless architecture.
</p>
<p>
For me, the most interesting part about Mr. Caffey's article is it mentions using 3 origins with AWS CloudFront:
(1) an origin for ALB (for hosting the Django API),
(2) an origin for the S3 website (static Vue.js site),
and (3) an S3 origin for Django assets.
Mr. Caffey does not say why he used 3 origins, but feeding one CloudFront distribution from multiple origins would mean that all of their
content would appear on the same Internet subdomain.
</p>
<p>
This means that the
<a href='https://www.w3.org/TR/2020/SPSD-cors-20200602/#resource-preflight-requests' target='_blank' rel='nofollow'>extra HTTP handshaking required for certain CORS</a>
(cross-origin HTTP requests) requests between subdomains would be avoided; specifically, there would be no need for pre-flight requests.
This would make the website seem noticeably faster if users did lots of content editing and/or transactions with the website.
My own pet project has users creating and modifying content, and purchasing product,
so taking the requirement for the CORS handshakes away would be a win,
plus the end user's web browser could reuse the origin HTTP connection,
speeding up even non-cacheable requests.
</p>
<p>
Tamás Sallai wrote <a href='https://advancedweb.hu/how-to-route-to-multiple-origins-with-cloudfront/' target='_blank' rel='nofollow'>How to route to multiple origins with CloudFront – Set up path-based routing with Terraform</a>.
Mr. Sallai <a href='https://advancedweb.hu/' target='_blank' rel='nofollow'>is a prolific writer</a>!
</p>
<!-- endregion -->
<!-- #region -->
<div class='imgWrapper imgFlex right quartersize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/django/lifeOnTheEdge.webp" type="image/webp">
<source srcset="/blog/images/django/lifeOnTheEdge.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/django/lifeOnTheEdge.png"
style='width: 100%; '
/>
</picture>
</div>
<h2 id="edge">Edge Computing</h2>
<p>
Performing computations and serving assets from a <a href='https://aws.amazon.com/cloudfront/features/' target='_blank' rel='nofollow'>nearby point of presence</a> minimizes latency for end users.
E-commerce customers much prefer online stores that respond quickly.
Edge computing can deliver that experience world-wide, and developers can deploy their work from wherever they are.
</p>
<p>
<a href='https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html' target='_blank' rel='nofollow'>AWS Lambda@Edge</a>
(<a href='https://aws.amazon.com/lambda/edge/' target='_blank' rel='nofollow'>console</a>)
runs the Lambda computation in one of 13 regional AWS points of presence, one hop removed from the CloudFront edge locations,
or at least in the same availability zone at the CloudFront point of presence.
Distributed database issues would need to be addressed before significant
benefits would accrue from this implementing this decentralized architecture.
Unfortunately, Lambda@Edge has some significant restrictions that prevent it from running nontrivial Django apps.
</p>
<h3>Lambda@Edge Restrictions</h3>
<p>From <a href='https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html' target='_blank' rel='nofollow'>requirements and restrictions on using Lambda functions with CloudFront</a>,
it is apparent that it is not possible to run non-trivial Django apps securely at the edge with good performance.</p>
<ul>
<li>You can add triggers only for functions in the US East (N. Virginia) Region.</li>
<li>You can’t configure your Lambda function to access resources inside your VPC.</li>
<li>AWS Lambda environment variables are not supported.</li>
<li>Lambda functions with AWS Lambda layers are not supported.</li>
<li>Using AWS X-Ray is not supported.</li>
<li>AWS Lambda reserved concurrency and provisioned concurrency are not supported.</li>
<li>Lambda functions defined as container images are not supported.</li>
</ul>
<p>
Until such time as Lambda@Edge removes the above restrictions,
Django webapps will continue to be deployed as centralized webapps, which means that ultra-low latency is not possible world-wide.
</p>
<h3 id="cf_fns">CloudFront Functions</h3>
<p>
<a href='https://aws.amazon.com/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/' target='_blank' rel='nofollow'>CloudFront Functions</a>
are closer to the user, but have even more restrictions than Lambda@Edge.
Alas, CloudFront Functions do not seem likely to be able to support significant computation any time soon.
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<figure>
<a href='https://aws.amazon.com/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.webp" type="image/webp">
<source srcset="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.png" type="image/png">
<img alt='From “Introducing CloudFront Functions – Run Your Code at the Edge with Low Latency at Any Scale”'
class="imgImg rounded shadow"
src="/blog/images/serverlessEcommerce/cloudfront-functions-only-lambda-egde-1024x413.png"
style='width: 100%; '
title='From “Introducing CloudFront Functions – Run Your Code at the Edge with Low Latency at Any Scale”'
/>
</picture>
</a>
<figcaption class='imgFigCaption '>
<a href="https://aws.amazon.com/blogs/aws/introducing-cloudfront-functions-run-your-code-at-the-edge-with-low-latency-at-any-scale/" target='_blank' >
From “Introducing CloudFront Functions – Run Your Code at the Edge with Low Latency at Any Scale”
</a>
</figcaption>
</figure>
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="vendor">Infrastructure as Code (IaC)</h2>
<div class="quote">
For anything bigger than a toy cloud application, Infrastructure as Code (IaC) is table stakes.
You’d be hard-pressed to find someone managing anything of scale who thinks letting folks point and click in the console is the optimal route.
<br><br> – From <a href='https://acloudguru.com/blog/engineering/cloudformation-terraform-or-cdk-guide-to-iac-on-aws' target='_blank' rel='nofollow'>CloudFormation, Terraform, or CDK? A guide to IaC on AWS</a> by Jared Short, published by <code>acloudguru.com</code>.
</div>
<div class="quote">
<a href='https://www.hashicorp.com/products/terraform' target='_blank' rel='nofollow'>Terraform</a>, AWS CloudFormation, Packer, Pulumi, and GeoEngineer are the most popular tools in the category "Infrastructure Build Tools".
<br>
– <a href='https://stackshare.io/infrastructure-build-tools' target='_blank' rel='nofollow'>from Stackshare.io</a>
</div>
<!-- endregion -->
<!-- #region -->
<h2 id="infographic">Infographic: Lambda Framework Comparison</h2>
<p>
Yan Cui at Lumigo.io made <a href='https://lumigo.io/aws-lambda-deployment/' target='_blank' rel='nofollow'>this terrific infographic</a>,
which compares 9 serverless application frameworks and infrastructure management tools according to opinionatedness and customizability.
This article discusses some of those technologies.
</p>
<div class='imgWrapper imgBlock inline fullsize' style=' '>
<figure>
<a href='https://lumigo.io/aws-lambda-deployment/' target='_blank' class='imgImgUrl'><picture class='imgPicture'>
<source srcset="/blog/images/django/lumigoComparison.webp" type="image/webp">
<source srcset="/blog/images/django/lumigoComparison.png" type="image/png">
<img alt='From 'AWS Lambda Deployment Frameworks', by Yan Cui at lumigo.io'
class="imgImg rounded shadow"
src="/blog/images/django/lumigoComparison.png"
style='width: 100%; '
title='From 'AWS Lambda Deployment Frameworks', by Yan Cui at lumigo.io'
/>
</picture>
</a>
<figcaption class='imgFigCaption fullsize'>
<a href="https://lumigo.io/aws-lambda-deployment/" target='_blank' >
From 'AWS Lambda Deployment Frameworks', by Yan Cui at lumigo.io
</a>
</figcaption>
</figure>
</div>
<p>
The trade-off between customizability and opinionatedness is that
highly customizable frameworks require more code to do things that opinionated frameworks do more succinctly.
On the other hand, very opinionated frameworks are more limited in their abilities.
A classic example of an opinionated framework is Ruby on Rails, which is specifically designed for master/detail applications.
Other types of applications should use a different framework, or no framework at all.
</p>
<p>
Two of the technologies on the above infographic are Zappa and Terraform, both of which I discuss in this blog post.
Zappa is rather opinionated, while Terraform is very customizable.
</p>
<!-- endregion -->
<!-- #region -->
<h2 class="numbered" id="cdk">AWS Cloud Development Kit (CDK)</h2>
<p>
AWS CDK provides a programmatic interface for modeling and provisioning cloud resources.
Languages supported include Java, JavaScript, .NET, Node.js, Python and Typescript.
</p>
<p>
Even if AWS is not directly the service provider,
awareness of the <a href='https://aws.amazon.com/cdk/' target='_blank' rel='nofollow'>AWS CDK</a> is important because some other options,
for example the <a href='https://aws.amazon.com/blogs/developer/introducing-the-cloud-development-kit-for-terraform-preview/' target='_blank' rel='nofollow'>Cloud Development Kit for Terraform</a> (cdktf),
are based on <a href='https://github.com/aws/aws-cdk' target='_blank' rel='nofollow'>AWS CDK</a>.
</p>
<!-- endregion -->
<!-- #region -->
<h2 class="numbered" id="chalice">Chalice – Serverless Django on AWS</h2>
<p>
<a href='https://aws.github.io/chalice/' target='_blank' rel='nofollow'>Chalice</a> is an AWS open-source project that has good traction.
This Python serverless microframework for AWS allows applications that use Amazon API Gateway and AWS Lambda to be quickly created and deployed.
</p>
<p>
The name and logo of this project are suggestive of the Holy Grail.
I found the thinly veiled references to Christianity to be off-putting.
Religious references have no place in a professional environment.
Programmers who work with this project have religious icons, words and phrases continuously presented to them,
and they must write words that are strongly identified with Christian doctrine for them to write software.
This is forced <a href='https://www.vocabulary.com/dictionary/indoctrination' target='_blank' rel='nofollow'>indoctrination</a>.
</p>
<!-- endregion -->
<!-- #region -->
<h2 class="numbered" id="zappa">Django w/ Zappa & AWS Lambda</h2>
<div class='imgWrapper imgFlex right quartersize' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/django/zappa_400x400.webp" type="image/webp">
<source srcset="/blog/images/django/zappa_400x400.png" type="image/png">
<img alt='Don't eat the yellow snow'
class="imgImg rounded shadow"
src="/blog/images/django/zappa_400x400.png"
style='width: 100%; '
title='Don't eat the yellow snow'
/>
</picture>
</div>
<p>
<a href='https://github.com/zappa/Zappa' target='_blank' rel='nofollow'>Zappa</a> is a popular library for serverless web hosting of Python webapps.
Zappa allows Python WSGI webapps like Django to run on AWS Lambda instead of from within a container like AWS EC2.
I am particularly interested in using Zappa to package and run <code>django-oscar</code> for AWS Lambda and CloudFront.
</p>
<p>
Zappa can perform two primary functions:
</p>
<ol>
<li>
<b>Packaging</b> – Zappa can build a Django webapp into an AWS Lambda package.
The package can be delivered via other mechanisms, for example mechanisms that are not even Python aware.
</li>
<li>
<b>Deploying</b> – Zappa can deploy and Django webapp to AWS Lambda,
and configure several AWS services to feed events to the Django webapp.
</li>
</ol>
<div class="quote">
Zappa does not provide a means to define additional resources as part of the overall infrastructure.
It is also somewhat rigid in how it defines certain resources which can lead to friction when incorporating
Zappa within organizations with more rigid requirements on cloud resource management.
With Zappa, you are better off allowing it to manage all the pieces needed for your web application on
its own and manage other resources with a separate tool such as stacker or Terraform.
<br><br>
… or use Zappa's <code>package</code> command to create an archive that is ready for upload to
lambda and utilize the other helpful functions the project provides for use after code is deployed.
<br><br>
– from <a href='https://www.jbssolutions.com/resources/blog/evolution-maintainable-lambda-development-pt-2/' target='_blank' rel='nofollow'>The Evolution of Maintainable Lambda Development Pt 2</a> by JBS Custom Software Solutions.
</div>
<p>
The Zappa documentation is excellent.
The project has some rough edges, but the new regime coming on board seem competent and fired up.
They have some work ahead to set things straight, but the technical path seems clear.
</p>
<p>
I think this project deserves special attention.
Lots of moldy issues and PRs need to be processed, which a small team could get done fairly quickly.
The project might also benefit from someone to hone the messaging.
I opened an <a href='https://github.com/zappa/Zappa/issues/968' target='_blank' rel='nofollow'>issue on the Zappa GitHub microsite</a> to discuss this.
</p>
<p>
This seminal project has been around several years,
and other well-known projects that have been developed since Zappa was first released have acknowledged that Zappa provided inspiration.
Time to brush it up and set it straight again; its best days lie ahead!
</p>
<p>
Edgar Roman wrote this helpful document:
<a href='https://romandc.com/zappa-django-guide/' target='_blank' rel='nofollow'>Guide to using Django with Zappa</a>.
</p>
<p>
I've messing around with Zappa, will report back.
</p>
<h3 id="videos">Videos</h3>
<p>
<a href='https://www.google.com/search?client=firefox-b-d&q=aws+zappa+django+video' target='_blank' rel='nofollow'>Videos of Zappa exist</a>.
</p>
<ul>
<li>
This video has got all the right technologies mixed together for me:
<a href='https://www.youtube.com/watch?v=Gf0vpJQZeBI' target='_blank' rel='nofollow'>Serverless Deployment of a Django Project with AWS Lambda, Zappa, S3 and PostgreSQL</a>.
</li>
</ul>
<!-- endregion -->
<!-- #region -->
<h2 class="numbered" id="djambda">Djambda / AWS Lambda / Terraform</h2>
<p>
Terraform does not impose a runtime dependency unless the realtime orchestration features are used.
</p>
<p>
<a href='https://github.com/netsome/djambda' target='_blank' rel='nofollow'>Djambda</a>
is an example project setting up Django application in AWS Lambda managed by Terraform.
I intend to play with it and write up my experience right here Real Soon Now.
</p>
<p>
This project uses GitHub Actions to create environments for the master branch and pull requests.
I wonder if this project can be used without GitHub actions?
</p>
<div class="quote">
[Terraform] does not provide an abstraction layer for the AWS, Azure, or Google Cloud.
It does that deliberately, as you should embrace all aspects when using cloud -
not extract a common denominator from the services delivered by the cloud provider.
<br><br> – From <a href='https://awsmaniac.com/aws-cdk-why-not-terraform/' target='_blank' rel='nofollow'>AWS CDK? Why not Terraform?</a>
by Wojciech Gawroński.
</div>
<!-- endregion -->
<!-- #region -->
<h2 class="numbered" id="serverless">Serverless Framework with WSGI</h2>
<p>
The <a href='https://www.serverless.com/plugins/serverless-wsgi' target='_blank' rel='nofollow'>docs</a> describe Serverless WSGI as:
</p>
<div class="quote">
Serverless plugin to deploy WSGI applications (Flask/Django/Pyramid etc.) and bundle Python packages.
</div>
<p>
I am concerned that the Serverless architecture requires an
<a href='https://www.serverless.com/pricing/fair-use-policy/' target='_blank' rel='nofollow'>ongoing runtime dependency</a>
on the viability and good will of Serverless, Inc.
Any hiccup on their part will immediately be felt by all their users.
It would make me nervous to base daily operational infrastructure on this.
</p>
<!-- endregion -->
<!-- #region -->
<h3 id="bintray">Bintray and JCenter Went <i>Poof!</i></h3>
<p>
I do not want to rely upon online services from a software tool vendor to run my builds.
The Scala community is still recovering from Bintray and JCenter shutting down.
I had dozens of Scala libraries on Bintray.
I do not plan to migrate them, they are gone from public access.
</p>
<div class="quote">
On February 3, 2021, JFrog announced that they will be shutting down Bintray and JCenter.
A complete shutdown is planned for February 2022.
<br><br>
– <a href='https://blog.gradle.org/jcenter-shutdown' target='_blank' rel='nofollow'>JCenter shutdown impact on Gradle builds</a>
</div>
<h3 id="free">Trading Autonomy for Minimal Convenience is a Poor Trade</h3>
<p>
Remember that free products are usually subject to change or termination without notice.
Examples abound of many companies whose free (and non-free) products suddenly ceased.
There is no need to assume this type of vulnerability,
so I block my metaphoric ears to the siren sound that tempts trusting souls into assuming
unnecessary dependencies, and I chose tooling that is completely under my control.
</p>
<div class="quote">
<h2>What happens if I exceed the fair use policy?</h2>
<p>
<i>From the <a href='https://www.serverless.com/pricing/' target='_blank' rel='nofollow'>Serverless Pricing and Terms page</a>.</i>
</p>
We want to offer a lot of value for free so you can get your idea off the ground, before introducing any infrastructure cost.
The intent of the fair use policy is to ensure that we can provide a high quality of service without incurring significant infrastructure costs.
The great majority of users will fall well within range of the typical usage guidelines.
While we reserve the right to throttle services if usage exceeds the fair use policy,
we do not intend to do so as long as we can deliver a high quality of service without significant infrastructure costs.
<br><br>
If you anticipate your project will exceed these guidelines, please contact our support team.
We’ll work with you on a plan which scales well.
</div>
<!-- endregion -->
<h2 id="apprunner">AWS AppRunner</h2>
<p>
AWS just announced <a href='https://aws.amazon.com/apprunner/' target='_blank' rel='nofollow'>AppRunner</a>.
I wonder how suitable it is...
</p>
Merging a Remote File with a Local File2021-04-12T00:00:00-04:00https://mslinn.github.io/blog/2021/04/12/merging-remote-file<p>
Today I am once again re-installing WSL2 on one of my laptops.
Seems that a Windows 10 installation’s half-life is measured in months, after which time
a reset is required. The reset preserves data, but not installed programs and not the WSL setup.
</p>
<p>
When I set up an OS I often use a pre-existing system’s files as templates for the new system’s files.
</p>
<h2 id="meld">Meld</h2>
<p>
<a href='https://meldmerge.org/' target='_blank' rel='nofollow'>Meld</a> is a fantastic, F/OSS file and directory merge tool.
2-way and 3-way merges are supported.
Meld uses X Windows for its user interface.
<a href='https://opticos.github.io/gwsl/' target='_blank' rel='nofollow'>GWSL</a> makes it easy to run X apps on WSL and WSL2.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idabe20986ae4b'><button class='copyBtn' data-clipboard-target='#idabe20986ae4b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>sudo apt install meld</pre>
</div>
<p>
Merging a remote file with a local file using Meld is easy once you know how.
Unless the remote file system is mounted locally, Meld cannot be used to modify <i>remote</i> files and directories,
just <i>local</i> files and directories.
</p>
<p>
Following is the incantation I used to display my local <code>.profile</code> and interactively merge it with my profile
on an Ubuntu Linux machine called <code>gojira</code>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idfdb8b58b92b7'><button class='copyBtn' data-clipboard-target='#idfdb8b58b92b7' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>meld ~/.profile <(ssh mslinn@gojira cat .profile)&</pre>
</div>
<p>
The above runs <code>ssh</code> in a subshell, logs in as <code>mslinn</code>
to the machine called <code>gojira</code> and
then displays the contents of <code>.profile</code> on <code>gojira</code>.
Meld compares the output of <code>cat</code> with the local copy of <code>~/.profile</code>,
and displays the differences:
</p>
<div class='imgWrapper imgFlex inline' style=' '>
<picture class='imgPicture'>
<source srcset="/blog/images/mergeRemote/meld.webp" type="image/webp">
<source srcset="/blog/images/mergeRemote/meld.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/mergeRemote/meld.png"
style='width: 100%; '
/>
</picture>
</div>
<div class="right" style="font-size: 3em;">😁</div>
<p>
Meld makes it easy to reconcile file versions.
</p>
Visual Studio Code Workspace Settings2021-04-11T00:00:00-04:00https://mslinn.github.io/blog/2021/04/11/svcode-workspace-settings<p>
For me, the killer feature that Visual Studio Code is how it integrates the Windows user interface
with working on WSL and WSL2.
Programs residing on the active WSL OS image execute natively on that OS, while VSCode
continues to run as a native Windows application.
This is possible because VSCode installs a proxy on the target OS.
The proxy does the bidding of the Windows executable.
</p>
<p>
Getting a project to execute on the target OS instead of the host OS can be tricky.
I have found that using a workspace to hold a collection of VSCode projects
is very helpful, because the definition of the collection also defines how they are handled.
</p>
<p>
WSL projects have different types of VSCode workspace entries than Windows entries do.
They are easy to recognize and change once you know what to look for.
The two possibile types of VSCode workspace project entries in a <code>.workspace</code> file are:
</p>
<ul>
<li><b>WSL Project</b> – <code>"uri": "vscode-remote://wsl+ubuntu/path/to/vscode/project"</code></li>
<li><b>Windows Project</b> – <code>"path": "C:\\path\\to\\vscode\\project"</code></li>
</ul>
<p>
The following VSCode workspace file has both types of entries.
For me, this is an error; I only want WSL projects.
My task is to change the yellow highlighted Windows project and make it look like the other WSL projects.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>aw.workspace.code-workspace</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id786d2bde1342'>{
"folders": [
{
"uri": "vscode-remote://wsl+ubuntu/var/sitesUbuntu/www.ancientwarmth.com"
},
{
"uri": "vscode-remote://wsl+ubuntu/var/work/django/django"
},
{
"uri": "vscode-remote://wsl+ubuntu/var/work/django/oscar"
},
{
"uri": "vscode-remote://wsl+ubuntu/var/work/ancientWarmth/ancientWarmth"
},
{
<span class="bg_yellow">"path": "../../var/work/django/main"</span>
}
],
"remoteAuthority": "wsl+Ubuntu",
"settings": {
"liveServer.settings.multiRootWorkspaceName": "www.mslinn.com",
"python.pythonPath": "/var/work/django/oscar/bin/python",
"git.ignoreLimitWarning": true,
"sqltools.connections": [
{
"previewLimit": 50,
"server": "localhost",
"port": 5432,
"driver": "PostgreSQL",
"name": "Ancient Warmth on Camille",
"database": "ancient_warmth",
"username": "postgres",
"password": "hithere"
}
]
}
}</pre>
</div>
<p>
All I need to do is change this entry:
</p>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id2276d1b4e854'><span class="bg_yellow">"path": "../..</span>/var/work/django/main"</pre>
</div>
<p>To:</p>
<div class="jekyll_pre" >
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id36ea2a9973e7'><span class="bg_yellow">"uri": "vscode-remote://wsl+ubuntu</span>/var/var/work/django/main"</pre>
</div>
<div class="right" style="font-size: 3em;">😁</div>
<p>
The modified entry will cause VSCode to launch the project from WSL, instead of Windows.
</p>
A Python Virtual Environment For Every Project2021-04-09T00:00:00-04:00https://mslinn.github.io/blog/2021/04/09/python-venvs<!-- #region intro -->
<p>
Python virtual environments are cheap to make and use –
unless you are unfortunate enough to program in native Windows.
I have adopted the habit of making a Python virtual environment (<i>venv</i>)
for each significant Python project,
plus a default venv for trivial Python work.
</p>
<!-- endregion -->
<!-- #region why -->
<h2 id="why">Why Virtualize Python?</h2>
<div class='imgWrapper imgFlex right' style='width: 40%; '>
<picture class='imgPicture'>
<source srcset="/blog/images/node/pushback.webp" type="image/webp">
<source srcset="/blog/images/node/pushback.png" type="image/png">
<img
class="imgImg rounded shadow"
src="/blog/images/node/pushback.png"
style='width: 100%; '
/>
</picture>
</div>
<p>
It is better to use virtualized user- and project-specific Python instances,
instead of working with a system-wide installation of Python.
This allows you to install and upgrade Python packages without using supervisor privileges.
Also, virtualized instances allows you to work on many different independent Python projects
at the same time, without package version collisions.
</p>
<p>
<code>Docker</code> is over-sold.
It adds unnecessary complexity to software projects.
Instead of virtualizing the entire software environment,
as <code>docker</code> attempts to do,
virtualizing the Python programming environment as described in this blog post,
<a href='/ruby/1000-ruby-setup.html'>Ruby <code>rbenv</code></a>,
or <a href='/blog/2022/03/01/node-package-managers.html'><code>node.js nvm</code></a>
are much easier and more productive approaches.
</p>
<p>
I think <code>docker</code> has been pushed hard in the media because it is a gateway technology to
<a href='https://www.infoworld.com/article/3223434/what-is-paas-platform-as-a-service-a-simpler-way-to-build-software-applications.html' target='_blank' rel='nofollow'>PaSS</a>.
This is a trend that PaSS vendors like AWS and Azure want to encourage, but
<a href='https://www.datacenterdynamics.com/en/news/37signals-spent-more-than-3-million-on-the-cloud-in-2022-for-basecamp-and-hey' target='_blank' rel='nofollow'>customers are pushing back</a>.
</p>
<!-- endregion -->
<!-- #region deprecated1 -->
<h2 id="deprecated1">Python’s <span class="code">venv</span> Virtualization Module</h2>
<p>
<code>Venv</code> is a tool to create isolated virtual Python environments.
It has been included with Python since Python v3.3,
which was released 11 years ago.
</p>
<p>
<a href='https://peps.python.org/pep-0405/#motivation' target='_blank' rel='nofollow'>PEP 405</a> specifies Python’s
<a href='https://docs.python.org/3/library/venv.html' target='_blank' rel='nofollow'><code>venv</code></a>
virtualization module.
</p>
<!-- endregion -->
<!-- #region deprecated2 -->
<h2 id="deprecated2">Deprecated Virtualization Modules</h2>
<p>
Python 3.6 was released 6 years ago.
It deprecated the other virtualization modules,
<a href='https://github.com/pyenv/pyenv' target='_blank' rel='nofollow'><code>pyenv</code></a> and
<a href='https://virtualenv.pypa.io/en/latest/' target='_blank' rel='nofollow'><code>virtualenv</code></a>.
Instead, use <a href='https://docs.python.org/3/library/venv.html' target='_blank' rel='nofollow'><code>venv</code></a>, as described in this blog post.
</p>
<!-- endregion -->
<!-- #region installubuntu -->
<h2 id="installubuntu">Extra Installation Steps for Ubuntu</h2>
<p>
<code>Venv</code> was included with Python on Ubuntu until Ubuntu 23.04
It virtualizes all the common Python executables.
</p>
<p>
Debian <a href='https://www.omgubuntu.co.uk/2023/04/pip-install-error-externally-managed-environment-fix' target='_blank' rel='nofollow'>changed <code>pip</code>’s behavior</a>
as a result of
<a href='https://peps.python.org/pep-0668/#implementation-notes' target='_blank' rel='nofollow'>PEP 668 – Marking Python base environments as “externally managed”</a>.
This affects Ubuntu because it is <a href='https://en.wikipedia.org/wiki/Downstream_(software_development)' target='_blank' rel='nofollow'>downstream</a>.
Starting with Ubuntu 23.04, you need to install <code>venv</code> by typing <code>sudo apt install python3.xx-venv</code>,
where <code>xx</code> is the minor version number of Python that is installed.
For Python 3.11, the following command is required:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb3197c5ba37a'><button class='copyBtn' data-clipboard-target='#idb3197c5ba37a' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install python3.11-venv</pre>
</div>
<p>
Some software projects use <a href='https://help.ubuntu.com/community/MetaPackages' target='_blank' rel='nofollow'>meta-packages</a>
to specify version-agnostic dependencies.
Perhaps Python on Ubuntu will do so in the future.
</p>
<p>
In order to virtualize <code>pip</code>, <code>venv</code> needs to invoke <code>ensurepip</code>,
which is <a href='https://askubuntu.com/a/897004/58760' target='_blank' rel='nofollow'>not installed by default on Ubuntu</a>.
On Ubuntu systems, the <code>ensurepip</code> command is provided by a package called <code>python3-pip</code>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id7148c24220c6'><button class='copyBtn' data-clipboard-target='#id7148c24220c6' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install python3-pip</pre>
</div>
<p>
You could install both of the above packages together, of course:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id01b64684b032'><button class='copyBtn' data-clipboard-target='#id01b64684b032' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>yes | sudo apt install python3.11-venv python3-pip</pre>
</div>
<!-- endregion -->
<!-- #region manual -->
<h2 id="manual">Manually Creating a VEnv</h2>
<p>
The following demonstrates how to create a new virtual python environment in the <code>~/venv/default/</code> directory.
Intermediate directories, such as <code>venv</code> in this example, will be created as required.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id9c4b4a31afff'><button class='copyBtn' data-clipboard-target='#id9c4b4a31afff' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>python3 -m venv ~/venv/default</pre>
</div>
<p class="alert rounded shadow">
Re-running this command at a later date will update the version of Python in an existing <code>venv</code>.
Each time you run this command, all <code>pip</code> packages are removed.
</p>
<p>
At this point, the virtual environment just contained executable images for Python.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida83651158507'><span class='unselectable'>$ </span>ls ~/venv/default/**
<span class='unselectable'>/home/mslinn/venv/default/lib64@ /home/mslinn/venv/default/pyvenv.cfg
/home/mslinn/venv/default/bin:
Activate.ps1 activate activate.csh activate.fish pip* pip3* pip3.10* python@ python3@ python3.10@
/home/mslinn/venv/default/include:
/home/mslinn/venv/default/lib:
python3.10/ </span></pre>
</div>
<!-- endregion -->
<!-- #region free -->
<h2 id="free">VEnvs are Nearly Free</h2>
<p>
The cost of a venv is virtually free for all OSes except native Windows.
This is because on all OSes except native Windows,
the executable images are linked by default,
so they do not require much storage space.
</p>
<p>
The <code>ls</code> command below shows that the <code>python</code> program in the <code>default</code> venv
is linked to <code>/usr/bin/python3.10</code>.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='id99c38dca768b'><button class='copyBtn' data-clipboard-target='#id99c38dca768b' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>ls -go ~/venv/default/bin/python
<span class='unselectable'>lrwxrwxrwx 1 18 Apr 9 06:01 <span class="bg_yellow">/home/mslinn/venv/default/bin/python -> python3*</span> </span></pre>
</div>
<!-- endregion -->
<!-- #region standard -->
<h2 id="standard">Create a VEnv for Every Python Project</h2>
<p>
My projects are stored under the directory pointed to by <code>$work</code>.
</p>
<p>
My standard procedure when making a Python project called <code>$work/blah</code>
is to also create a venv for it at within the project, at <code>$work/blah/.venv/</code>.
I add an entry to <code>.gitignore</code> that merely consists of a line that says <code>.venv/</code>.
</p>
<p>
A bash alias could be defined called <code>blah</code>
that makes the project directory current and activates the venv:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>~/.bash_aliases</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='idb591c2f0d61c'><button class='copyBtn' data-clipboard-target='#idb591c2f0d61c' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button>alias blah="cd $work/blah; source ./venv/bin/activate"</pre>
</div>
<div class="right" style="font-size: 3em;">😁</div>
<p>
Now you could type <code>blah</code> at a shell prompt,
and you would be working on that project.
Boom!
</p>
<!-- endregion -->
<!-- #region deactivate -->
<h2 id="deactivate">Deactivate a VEnv</h2>
<p>
Stop using venvs with <code>deactivate</code>.
Notice that the prompt changes.
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ida984d1fad387'><span class='unselectable'>(aw) $ </span>deactivate
<span class='unselectable'>$ </span></pre>
</div>
<!-- endregion -->
<!-- #region Directory-Locked -->
<h2 id="virt" class="clear">Directory-Locked Python Virtualization</h2>
<p>
After setting up a Python virtual environment,
a quick examination of the <code>pip</code> script
shows that it is hard-coded to the directory that it was made for:
</p>
<div class="jekyll_pre" >
<div class='codeLabel unselectable' data-lt-active='false'>Shell</div>
<pre data-lt-active='false' class='maxOneScreenHigh copyContainer' id='ide8b172a18a23'><button class='copyBtn' data-clipboard-target='#ide8b172a18a23' title='Copy to clipboard'><img src='/assets/images/clippy.svg' alt='Copy to clipboard' style='width: 13px'></button><span class='unselectable'>$ </span>head -n 1 ~/venv/aw/bin/pip
<span class='unselectable'><span class="bg_yellow">#!/home/mslinn/venv/aw/bin/python</span> </span></pre>
</div>
<p>
For virtualized environments, such as Docker,
this means that a Python virtual environment created without Docker can only be
used within a Docker image if the path to it is the same from within the Docker image as when it was created.
</p>
<!-- endregion -->
<!-- #region further reading -->
<h2 id="further">For Further Reading</h2>
<p>
<a href='https://mitelman.engineering/blog/python-best-practice/automating-python-best-practices-for-a-new-project/' target='_blank' rel='nofollow'>Python Best Practices for a New Project in 2021</a>.
This article is already becoming dated.
YMMV.
</p>
<!-- endregion -->
<!-- #region summary -->
<h2 id="summary">Summary</h2>
<ul>
<li>Demonstrated how to make an alias for working with Python virtual environments (<i>venvs</i>) that are coupled with Python projects.</li>
<li>Deactivating the current venv was demonstrated using the <code>deactivate</code> command, provided with every venv.</li>
<li>Locked directories mean that Python virtual environments should normally only be created in the same environment they are intended to be used.</li>
</ul>
<!-- endregion -->
Escaping HTML on Clipboard From a Windows Hot Key via WSL2021-04-03T00:00:00-04:00https://mslinn.github.io/blog/2021/04/03/escape-html-clipboard<p>
I frequently show HTML source code when I write.
That HTML must be escaped prior to displaying it on a web page.
</p>
<h2 id="bash_script">Script to Apply HTML Escape to Clipboard</h2>
<p>
This bash script applies an HTML escape conversion to the contents of the system clipboard.
If you have WSL on your machine, you could store it on the WSL file system, for example in
<code>~/.local/bin/escapeHtml</code>.
</p>
<div class="codeLabel"><a href='data:text/plain;charset=UTF-8,escapeHtml' download='escapeHtml'
title='Click on the file name to download the file'>escapeHtml</a>
</div>
<pre data-lt-active="false" class="pre_tag maxOneScreenHigh copyContainer" id="id7cf39e18c70e">#!/bin/bash
# SPDX-License-Identifier: Apache-2.0
function help {
echo "$(basename $0) - Escapes HTML with entities.
Reads from STDIN or pipe, or converts the cl