- let’s encrypt nginx

SCHEDULED: <2020-06-02 Tue>

Install nginx:

pkg_add nginx

Using the example acme-client.conf as a starting point.

cp /etc/examples/acme-client.conf /etc/acme-client.conf
diff -u /etc/examples/acme-client.conf /etc/acme-client.conf


--- /etc/examples/acme-client.conf      Fri May  8 04:51:59 2020
+++ /etc/acme-client.conf       Tue Jun  2 22:50:04 2020
@@ -11,9 +11,9 @@
        account key "/etc/acme/letsencrypt-staging-privkey.pem"

-domain {
-       alternative names { }
-       domain key "/etc/ssl/private/"
-       domain full chain certificate "/etc/ssl/"
+domain {
+       alternative names { }
+       domain key "/etc/ssl/private/"
+       domain full chain certificate "/etc/ssl/"
        sign with letsencrypt
cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.orig
diff -u /etc/nginx/nginx.conf.orig /etc/nginx/nginx.conf


--- /etc/nginx/nginx.conf.orig  Tue Jun  2 22:35:37 2020
+++ /etc/nginx/nginx.conf       Wed Jun  3 01:51:44 2020
@@ -89,6 +89,12 @@
         #location ~ /\.ht {
         #    deny  all;
+        location /.well-known/acme-challenge/ {
+            rewrite ^/.well-known/acme-challenge/(.*) /$1 break;
+            root /var/www/acme;
+        }

Start nginx:

rcctl start nginx

Request the intial certificate:

acme-client -v

Do the bare minimum to enable https:

-        ssl_certificate      /etc/ssl/server.crt;
-        ssl_certificate_key  /etc/ssl/private/server.key;
+        ssl_certificate      /etc/ssl/;
+        ssl_certificate_key  /etc/ssl/private/;

Reload nginx:

rcctl reload nginx

Enable nginx (on boot):

rcctl enable nginx

Setup automatic certificate renewal:

crontab -e
~       *       *       *       *       acme-client && rcctl reload httpd

SCHEDULED: <2020-06-01 Mon>

I am re-purposing and re-installing my ARP Thunder Cloud Dedicated Server.

Load ISO:


Power Off


  1. DVD/CD


System hostname: blog

IPv4 config is manual: IPv4 address, Netmask, Default IPv4 route and DNS nameservers can all be found in the ARP Networks portal under the IP Assignments service.

IPv6 address: autoconf (this seems to work…)

DNS domain name:

I do not expect to run the X Window System.

I chose to setup a user (pdericson).

The timezone that I am in is NZ.

The disk auto layout was 👍


Login as root on console

reboot  # some firmware has been installed

Login as root on console

reboot  # the kernel has been re-linked

Login as root on console

Because this is a re-install, the existing SATA disk is still present. I need to fsck the disk because I didn’t shutdown the server gracefully.

mkdir /data
fsck -y /dev/sd1a
duid=`disklabel sd1 | grep ^duid: | awk '{print $2}'`
cat >> /etc/fstab <<EOF
$duid.a /data ffs rw,nodev,nosuid 1 2
mount /data

I need git to clone my blog engine.

pkg_add git

I need emacs to adjust my blog engine.

pkg_add -i emacs  # 3: ...no_x11

My blog engine is written in Elixir.

pkg_add elixir
pkg_add node

My blog engine uses Pygments to highlight code.

pkg_add python3
ln -s python3 /usr/local/bin/python
pkg_add py3-pygments
pkg_add rsync

Login as pdericson via ssh

Setup public key authentication.

Elixir expects UTF-8 which is reasonable.

cat >> .profile <<EOF
export LC_CTYPE="en_NZ.UTF-8"

Login as pdericson via ssh


Setup .gitconfig.

ansible callback plugins

SCHEDULED: <2020-05-16 Sat>

The latest version of ansible is:


Callback Plugins:

Developing plugins, Developing particular plugin types, Callback plugins:

The CallbackBase class:

What methods are available?

curl | grep def\ v2_


def v2_on_any(self, *args, **kwargs):
def v2_runner_on_failed(self, result, ignore_errors=False):
def v2_runner_on_ok(self, result):
def v2_runner_on_skipped(self, result):
def v2_runner_on_unreachable(self, result):
def v2_runner_on_async_poll(self, result):
def v2_runner_on_async_ok(self, result):
def v2_runner_on_async_failed(self, result):
def v2_playbook_on_start(self, playbook):
def v2_playbook_on_notify(self, handler, host):
def v2_playbook_on_no_hosts_matched(self):
def v2_playbook_on_no_hosts_remaining(self):
def v2_playbook_on_task_start(self, task, is_conditional):
def v2_playbook_on_cleanup_task_start(self, task):
def v2_playbook_on_handler_task_start(self, task):
def v2_playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None, unsafe=None):
def v2_playbook_on_import_for_host(self, result, imported_file):
def v2_playbook_on_not_import_for_host(self, result, missing_file):
def v2_playbook_on_play_start(self, play):
def v2_playbook_on_stats(self, stats):
def v2_on_file_diff(self, result):
def v2_playbook_on_include(self, included_file):
def v2_runner_item_on_ok(self, result):
def v2_runner_item_on_failed(self, result):
def v2_runner_item_on_skipped(self, result):
def v2_runner_retry(self, result):
def v2_runner_on_start(self, host, task):

Logitech K780

SCHEDULED: <2020-05-10 Sun>

I’m quite happy with my keyboard, but… the fn keys are swapped by default!

Logitech K780

We can use xev to find out more…


KeyPress event, serial 34, synthetic NO, window 0x200001,
    root 0x203, subw 0x0, time 349417, (155,9), root:(3039,559),
    state 0x0, keycode 180 (keysym 0x1008ff18, XF86HomePage), same_screen YES,
    XLookupString gives 0 bytes:
    XmbLookupString gives 0 bytes:
    XFilterEvent returns: False


KeyPress event, serial 34, synthetic NO, window 0x200001,
    root 0x203, subw 0x40000e, time 424031, (376,218), root:(3260,768),
    state 0x0, keycode 70 (keysym 0xffc1, F4), same_screen YES,
    XLookupString gives 0 bytes:
    XmbLookupString gives 0 bytes:
    XFilterEvent returns: False

Solaar is a Linux manager for Logitech’s devices that connect via a USB Unifying, Lightspeed, or Nano receiver.

I found that I needed version 1.0.2rc2, the version from pypi (1.0.1) and the version from debian buster (0.9.2) were too old.

sudo apt install python3-pyudev
git clone
cd Solaar
sudo bash -c 'umask 022 ; pip3 install .'

I think a reboot was required but I’m not sure…

I’m using the following in my ~/.xsession file:

# turn off fn-swap
sudo solaar config 1 fn-swap off
# keep solaar running so we don't lose our settings when the keyboard sleeps
sudo solaar -w hide &

Notebook tutorial - Part 5: Animations!

SCHEDULED: <2020-05-01 Fri>

Just a bit more reading please:

But also, take a look at the Python tutorial - it’s really nice!

If we bring together everything we have learnt, at a stretch we can start thinking about creating a cool animation.

Here’s one I put together:

Pretty neat huh?

Right, let’s walk through how it was made…


import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML

fig, ax = plt.subplots()

ax.tick_params(axis='x', rotation=90)

data = {}

def animate(i):
    # The animate function is called for each frame of the animation - in this case a frame per day!

    data.setdefault((date1 + datetime.timedelta(days=i)).strftime('%Y-%m-%d'), {})

    # This dataframe is a subset of the results...  e.g. Date of report less than
    # or equal to first date + i days
    df3 = df1[df1["Date of report"] <= date1 + datetime.timedelta(days=i)][["Date of report", "DHB"]].groupby([df1["Date of report"].dt.strftime("%Y-%m-%d"), "DHB"]).size()
    for item in df3.items():
        data.setdefault(item[0][0], {})[item[0][1]] = item[1]

    # print(data)
    # This is what "data" looks like for index 0 (frame 1)
    # e.g. {'2020-02-26': {'Auckland': 1}}

    args = [sorted(data)]
    # We're creating a plot per DHB (?)
    for dhb in dhbs:
        args.append([data[x].get(dhb, 0) for x in data])

    # stackplot expects multiple lists of data, first one being the x axis (dates),
    # then, in our case, a list per dhb (for this time period.)

    ax.legend(dhbs, loc='upper left', fontsize='xx-small')

# Think of an animation as a sequence of graphs (or frames)
ani = animation.FuncAnimation(fig, animate, frames=days)

# Finally, let's create a video!'part-5.mp4', fps=1, dpi=200)

What did we learn?

  • We learnt one way to make an animation and that an animation is simply a sequence of frames (or graphs), can you create your own animation? Have a think about what data you can extract and how it could be presented and send me a copy!

Notebook tutorial - Part 4: Pandas and DataFrames

SCHEDULED: <2020-04-30 Thu>

Time to do some reading, take a look at this page in the Panda’s tutorial:

Do some experimenting:

df1[["Date of report"]].groupby(df1["Date of report"]).count()
df1[["Date of report", "Overseas travel"]].groupby([df1["Date of report"].dt.strftime("%Y-%m-%d"), "Overseas travel"]).size().unstack()
df1[["Date of report", "Last country before return"]].groupby([df1["Date of report"].dt.strftime("%Y-%m-%d"), "Last country before return"]).size().unstack()

What did we learn?

  • We learnt that pandas refers to a table of data as a “DataFrame” and that a subset of a DataFrame can be selected to present different features of the data. Please select some data and share the Python code with me!