Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

Preparing my offsite backup server, part 1

Updated!

This is part of the story, which has been completely rewritten as part 2.

Backstory

I picked up a mid-sized tower that will become my first offsite backup server! As my local network has matured, I have finally reached the point to establish my offsite backup storage. I had toyed with the idea of setting up a synology device, but I really only need storage; nothing else: no Plex, or docker, or anything of that kind. The name of this system is server2.

I am experimenting with using Devuan Ceres as a server. I could have used CentOS 7 which is still my standard for server OSes, but I can afford some experimentiation here.

Overview

I know I have a few requirements, and I chose my solutions and started the buildout.

  • vpn (wireguard)
  • RAID 5 (mdadm)
  • backup scripts (rsync+sh)
  • permissions (ipa user and various rules)

VPN

Install wireguard.

sudo apt-get install wireguard resolvconf

Wireguard already works with resolvconf to control the dns when entering/leaving the vpn, so it is accepted here.

Establish /etc/wireguard/wg0.conf. References include [servername]:/etc/wireguard/wg0.conf and server1:/etc/wireguard/wg0.conf.

[Interface]
Address = 10.222.0.4/24
ListenPort = 51820
# from `wg genkey`
PrivateKey = SCRUBBED
# server2 public key
# SCRUBBED
DNS = 192.168.1.10,192.168.1.11, ipa.internal.com, vm.internal.com, internal.com
[Peer]
# server1
PublicKey = SCRUBBED
AllowedIPs = 192.168.1.10/32, 192.168.1.11/32, 192.168.1.14/32, 192.168.1.18/32, 10.222.0.0/24
PersistentKeepalive = 25
Endpoint = www.example.com:51820

Also I had to add this as a peer on server1!

Set up a custom init script, /etc/init.d/wireguard.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#! /bin/sh
# adapted to sh from bash https://gist.github.com/kbabioch/5dd8801e702e519ed18d9b17cacae716
# 2021-11-18

# Copyright (c) 2021 Karol Babioch <karol@babioch.de>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# LSBInitScript for Wireguard: This is a leightweight init script for
# Wireguard. While Wireguard itself requires only minimal overhead to setup and
# start, it still requires some script invocations (e.g. during boot).
#
# Most distributions are using systemd by now, and as such can use
# wg-quick@.service. However some distributions / images / Linux appliances
# are not (yet) using systemd. In such cases, this init script could be used
# to (re)start and/or stop Wireguard.
#
# It can handle all configured Wireguard interfaces (within /etc/wireguard)
# globally and/or individual interfaces, e.g. (/etc/init.d/wireguard start wg0).
#
# It relies on wg(8) and wg-quick(8) in the background.

### BEGIN INIT INFO
# Provides:          wireguard
# Required-Start:    $network $syslog
# Required-Stop:     $network $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Starts Wireguard interfaces
# Description:       Sets up Wireguard interfaces (by means of wg-quick).
### END INIT INFO

CONFIG_DIR=/etc/wireguard

function get_active_wg_interfaces() {
  INTERFACES=$(wg | awk '/interface:/{print $NF}')
  echo "$INTERFACES"
}

# This is required for wg-quick(1) to work correctly, i.e. for process
# substitution (`<()`) to work in Bash. If missing, wg-quick will fail with a
# "fopen: No such file or directory" error.
#[ -e /dev/fd ] || ln -sf /proc/self/fd /dev/fd

case "$1" in

  start)
    if [ -z "$2" ]; then
      echo "Starting all configured Wireguard interfaces"
      for CONFIG in $(cd $CONFIG_DIR; ls *.conf); do
        wg-quick up ${CONFIG%%.conf}
      done
    else
      echo "Starting Wireguard interface: $2"
      wg-quick up "$2"
    fi
    ;;

  stop)
    if [ -z "$2" ]; then
      echo "Stopping all active Wireguard interfaces"
      INTERFACES=$(get_active_wg_interfaces)
      for INTERFACE in $INTERFACES; do
        wg-quick down "$INTERFACE"
      done
    else
      echo "Stopping Wireguard interface: $2"
      wg-quick down "$2"
    fi
    ;;

  reload|force-reload)
    if [ -z "$2" ]; then
      echo "Reloading configuration for all active Wireguard interfaces"
      INTERFACES=$(get_active_wg_interfaces)
      for INTERFACE in $INTERFACES; do
        wg-quick strip "$INTERFACE" | wg syncconf "$INTERFACE"
      done
    else
      echo "Reloading configuration for Wireguard interface: $2"
      wg-quick strip "$2" | wg syncconf "$2"
    fi
    ;;

  restart)
    $0 stop "$2"
    sleep 1
    $0 start "$2"
    ;;

  status)
    # TODO Check exit codes and align them with LSB requirements
    if [ -z "$2" ]; then
      INTERFACES=$(get_active_wg_interfaces)
      for INTERFACE in $INTERFACES; do
        wg show $INTERFACE
      done
    else
      wg show "$2"
    fi
    ;;

  *)
    echo "Usage: $0 { start | stop | restart | reload | force-reload | status } [INTERFACE]"
    exit 1
    ;;

esac

Set it to start with the defaults.

sudo update-rc.d wireguard defaults

Setting up the RAID 5 disk array

I chose to use RAID 5, so the array can handle 1 disk failure and still keep going. I prefer to be able to survive 2 disks failed but I only have 4 disks due to the size of the chassis so this is a compromise I make. See Reference 2

sudo apt-get install mdadm #The following NEW packages will be installed: # bsd-mailx exim4-base exim4-config exim4-daemon-light libgnutls-dane0 libidn12 liblockfile1 libunbound8 mdadm # psmisc

Begin to make the array. This runs the job in the background already!

sudo mdadm --create --verbose /dev/md0 --level=5 --raid-devices=4 /dev/sda /dev/sdb /dev/sdc /dev/sde 2>&1 | tee -a /root/mdadm.create.$( date "+%F" ).log

Check the status with

cat /proc/mdstat

When it is done, make the filesystem. I don't care about btrfs or zfs.

time sudo mkfs.ext4 /dev/md0 sudo mkdir -p /var/server2/ sudo mount -t ext4 -o noatime,nodev -v /dev/md0 /var/server2 sudo mdadm --detail --scan | sudo tee -a /etc/mdadm/mdadm.conf

ARRAY /dev/md0 metadata=1.2 name=server2:0 UUID=671846fe:2d384a18:fa0c45af:8754ba8c

That last command added the ARRAY line to /etc/mdadm/mdadm.conf.

Add this filesystem to /etc/fstab:

/dev/md0 /var/server2 ext4 auto,rw,noatime,discard,nodev 0 0

And with those lines in those files, now we need to run:

sudo update-initramfs -u

Backup script

The main script is still in progress so check back for future blog posts, but I know I would need a number of permissions.

Permissions

This is what I have modified in my freeipa domain so far. See Reference 3.

Make a service account:

ipa user-add --first=sync --last=user --cn='syncuser' --homedir /home/syncuser --gecos='Service account for syncing data' --email='[myemail]' --password --noprivate --displayname='syncuser' --shell=/bin/bash syncuser --gidnumber=888800013

Establish the hbac rule that allows the user to log in to the two systems.

ipa hbacrule-add --servicecat=all syncuser_rule
ipa hbacrule-add-user --users=syncuser syncuser_rule
ipa hbacrule-add-host --hosts=dns2 syncuser_rule
ipa hbacrule-add-host --hosts=server2 syncuser_rule
ipa hbacrule-mod --desc='Allow syncuser to access the relevant systems for backups' syncuser_rule

As syncuser@dns2:

ssh-keygen
ipa user-mod "${USER}" --sshpubkey="$( cat ~/.ssh/id_rsa.pub )"

Establish sudoers permission in FreeIPA for service account syncuser.

ipa sudocmd-add --desc='rsync full permissions' rsync
ipa sudocmd-add --desc='rsync full permissions' /usr/bin/rsync
ipa sudorule-add syncuser-rsync
ipa sudorule-add-host syncuser-rsync --hosts server2
ipa sudorule-add-host syncuser-rsync --hosts dns2
ipa sudorule-add-allow-command syncuser-rsync --sudocmds rsync
ipa sudorule-add-allow-command syncuser-rsync --sudocmds /usr/bin/rsync
ipa sudorule-add-option syncuser-rsync --sudooption '!authenticate'
ipa sudorule-add-user syncuser-rsync --users syncuser
ipa sudorule-mod syncuser-rsync --desc='syncuser can run rsync on dns2, server2'

References

  1. server2a:/etc/installed/server2a.log
  2. How to Create a RAID 5 Storage Array with 'mdadm' on Ubuntu 16.04
  3. Rsync 'Permission denied' for root - Top 4 reasons and solutions

Jellyfin notifications using webhooks

I replaced my Plex instance with the free and open source alternative Jellyfin (https://jellyfin.org/). I previously wrote about my alerting project for Plex, and now I have my solution for Jellyfin alerting.

Jellyfin, too, has the ability to send alerts including to email. You use a plugin. I enabled the crobibero repo and then installed the Webhook plugin in the web ui. After restarting the application from the command line, I was able to navigate to Admin Dashboard -> Advanced -> Plugins.

I set up an smtp webhook. I think the webhook url can just be empty, but I filled in something on my site just in case. My web logs don't indicate it hits that url at all. I chose my desired notification types and users. I use this as a template for the message:

<pre>Username: {{Username}}
Action: {{NotificationType}}
Timestamp: {{UtcTimestamp}}
Title:: {{Name}}
{{#if_exist SeriesName}}
Series: {{SeriesName}}
Season: {{SeasonNumber00}}
Episode: {{EpisodeNumber00}}
{{/if_exist}}
DeviceName: {{DeviceName}}
ClientName: {{ClientName}}
PlaybackPosition: {{PlaybackPosition}}
</pre>

I used these attributes too:

Sender address: [my email]
Receiver address: [my email]
SMTP server address: smtp.gmail.com
SMTP port: 465
Use credentials: True
Username: []
Password: []
Use SSL: True
Is Html Body: True
Subject template: Jellyfin activity for {{Username}}

Because I used gmail, I had to go into my account's security settings and enable old/insecure application access, but that wasn't directly related to the Jellyfin config. The Jellyfin logs were useful to troubleshoot that failure to log in.

My kinit invocation

Some of my systems use a fingerprint reader to allow user login. On these systems, I use LUKS encryption so don't worry, I still need a passphrase at boot time.

When pam authenticates me with a fingerprint, it doesn't perform kerberos authentication which facilitates things like seamless ssh authentication. So I have to manually run kinit. I always run it with a few parameters:

kinit -r 14d -l 14d -f -p

The r sets renewable life to 14 days. The l (lima) sets the lifetime to 14 days. And f requests a forwardable ticket, and p a proxiable ticket.

See also

Previously, I wrote about showing kerberos ticket status in the system tray.

Run 640x480 game in wine with mounted cd

I wanted to run an old computer game in Wine, that uses a 640x480 resolution on a touchscreen. This game wants to check the CD volume label, which regular loop mounting doesn't provide. But cdemu does!

I wrote a whole script to run the program after mounting the disc and setting the correct screen resolution.

#!/bin/sh
# File: run-game.sh
# Location:
#    ~/bin/
# Author: bgstack15
# Startdate: 2021-10-30 19:11
# Title: Script that Runs 640x480 game in Wine with CD
# Purpose: get 640x480 screen and run wine game
# History:
# Usage:
# Reference:
#    https://bgstack15.ddns.net/blog/posts/2021/10/01/convert-b6i-to-iso/
# Improve:
# Dependencies:
#    wine32 from dpkg --add-architecture i386 && apt-get install wine32
#    ISO format image of disc
#    cdemu from https://build.opensuse.org/project/show/home:bgstack15:cdemu
#    hand-crafted scripts in ~/.screenlayout/ from arandr
#    rotate solution https://bgstack15.ddns.net/blog/posts/2021/10/29/menu-for-choosing-screen-orientation-on-a-tablet-computer/
#    /usr/local/bin/set-touchscreen-resolution.sh
# change screensize
#exec 1>&/dev/pts/3 # useful for testing
~/.screenlayout/640x480_inverted.sh
/usr/local/bin/rotate.sh inverted
APPLY=1 sh -x /usr/local/bin/set-touchscreen-resolution.sh 640 480
#sleep 2 # just in case, for wine
# mount cd
echo "Mounting cdrom..."
sudo modprobe vhba 2>/dev/null
sudo chmod 0666 /dev/vhba_ctl 2>/dev/null
ps -ef | grep -q -E '[c]demu-daemon' || { cdemu-daemon 1>/dev/null 2>&1 & sleep 2 ; }
cdemu load 0 /opt/CDROMs/game.iso
thisdev="$( cdemu status 2>/dev/null | awk '/game.iso/{print $1}' )"
thisscsi="$( cdemu device-mapping | awk -v "dev=${thisdev:-NONE}" '$1 ~ dev {print $2}' )"
mount | grep -qE '\/dev\/cdrom' && sudo umount -lv /mnt/cdrom
sudo mkdir -p /mnt/cdrom
sudo mount -v "${thisscsi}" /mnt/cdrom
# run game
echo "Running game..."
cd ~"/.wine32/drive_c/Program Files/Game/"
WINEARCH=win32 WINEPREFIX=~/.wine32 wine game.exe
# restore resolution
echo "Restoring resolution"
~/.screenlayout/normal.sh
/usr/local/bin/rotate.sh normal
APPLY=1 /usr/local/bin/set-touchscreen-resolution.sh 1366 768

Screen resolution scripts

I used arandr to make precanned selections for 640x480 and the default resolution of 1366x768.

Menu entry file

Dependencies

  1. Manually generated arandr scripts in ~/.screenlayout
  2. cdemu
  3. wine32
  4. My rotate screen solution
  5. Adjust touchscreen input for fullscreen aspect ratio

Adjust touchscreen input for fullscreen aspect ratio

My Thinkpad X230 of course has the 1366x768 display endemic to the era. I wanted to use an old program that is best viewed in a full-screen, 640x480 resolution. Of course xrandr can set this resolution no problem, but the tablet input needs to be adjusted to scale to the size of the display.

Here is my solution for that.

#!/bin/sh
# File: set-touchscreen-resolution.sh
# Locations:
#    LTB-019:/usr/local/bin/
#    /mnt/public/Support/Platforms/Thinkpad-X230/screen/
# Author: bstack15
# Startdate: 2021-10-30 20:42
# Title: Script that Adjusts Thinkpad Touchscreen to Aspect Ratios of Smaller Resolutions
# Purpose: change touchscreen input to match resolution.
# History:
# Usage:
#    set-touchscreen-resolution.sh 640 480
# References:
#    https://stackoverflow.com/questions/20558710/bc-truncate-floating-point-number/20562313#20562313
# Improve:
#    discover how to actually make the logic work with 1366x768
# Dependencies:
#    xsetwacom
# Documentation:
#    full height input is always useful on the Thinkpad X230, so we are only calculating X.
# get touchscreen native aspect ratio
xi="$( xinput list )"
stylus="$( echo "${xi}" | awk '/stylus.*slave.*pointer/{print}' | awk -F'=' '{print $2}' | awk '{print $1}' )"
touch="$( echo "${xi}" | awk '/touch.*slave.*pointer/{print}' | awk -F'=' '{print $2}' | awk '{print $1}' )"
eraser="$( echo "${xi}" | awk '/eraser.*slave.*pointer/{print}' | awk -F'=' '{print $2}' | awk '{print $1}' )"
#desiredx=640
#desiredy=480
desiredx="${1}"
desiredy="${2}"
# for some reason the real aspect ratio does not work, but setting a 1x1 ratio will make the tablet input work
test "${desiredx}" = "1366" && test "${desiredy}" = "768" && { desiredx=1 ; desiredy=1 ; }
test -z "${desiredx}" && test -z "${desiredy}" && { desiredx=1 ; desiredy=1 ; }
# For each item, because the resolution can be different across items.
for word in ${touch} ${stylus} ${eraser} ;
do
   origArea="$( xsetwacom get "${word}" Area )"
   xsetwacom set "${word}" ResetArea
   resetarea="$( xsetwacom --get "${word}" Area )"
   xsetwacom set "${word}" Area ${origArea}
   fullx="$( echo "${resetarea}" | awk '{print $3}' )"
   fully="$( echo "${resetarea}" | awk '{print $4}' )"
   width="$( printf "scale=4\na=(${fullx}/(${desiredx}/${desiredy}))\na\n" | bc )"
   x1="$( printf "scale=4\ndefine trunc(x) { auto s; s=scale; scale=0; x=x/1; scale=s; return x }\na=(${fullx}/2)-(${width}/2)\ntrunc(a)\n" | bc )"
   x2="$( printf "scale=4\ndefine trunc(x) { auto s; s=scale; scale=0; x=x/1; scale=s; return x }\na=${x1}+${width}\ntrunc(a)\n" | bc )"
   #echo "For width=${width} run:"
   echo "setting device ${word} Area ${x1} 0 ${x2} ${fully}"
   test -n "${APPLY}" && {
      xsetwacom set "${word}" Area ${x1} 0 ${x2} ${fully}
   }
done

Any guidance on the mathematics for the aspect ratios would be welcome! Notice how I had to hardcode the full resolution of the display (1366x768). Apparently my math is... off.

References

  1. A lot of my own lousy mathematics
  2. Wacom tablet - ArchWiki
  3. linux - bc truncate floating point number - Stack Overflow

Monitor freeipa certificate expirations

Project freeipa-cert-alert

Overview

Freeipa-cert-alert is a small project that lists the certificates from an IPA server that will expire soon. The idea is to pass the output to a mail or logging utility.

I wanted to manipulate the objects coming from freeipa more directly than parsing the textual output (which is not a terrible way to do it), because I know that FreeIPA is a Python project. Come to find out, the python3-freeipa package is not a core part of freeipa, which uses python-ipa* package names. But python3-freeipa provides the suitable commands that return useful objects we can iterate through.

Even the cert_find() implementation lets you pick start and stop times for the validity period, which is most of the work involved.

I also devised some dirty tricks to columnize the output.

Using freeipa-cert-alert

You configure it with environment variables at runtime, including:

  • FREEIPA_SERVER
  • FREEIPA_USERNAME
  • FREEIPA_PASSWORD
  • DAYS

For some reason, domain name does not suffice as the server name. You must pick a server name. This is discoverable in a properly-functioning Kerberos domain with:

dig +short -t srv _ldap._tcp.yourdomain.com | awk '{print $4}'

Example

$ DAYS=180 ./freeipa-cert-alert.py
Certificates expiring within 180 days from 2021-10-27
Not valid before               Not valid after                Subject
Thu Jan 16 21:18:28 2020 UTC   Sun Jan 16 21:18:28 2022 UTC   CN=d2-02a.ipa.example.com,O=IPA.EXAMPLE.COM

Upstream

My gitlab repo is the source.

Alternatives

Examine the output of ipa cert-find manually. Otherwise, I found no examples that do what I do here.

Menu for choosing screen orientation on a tablet computer

Screenshot of Choose Screen Orientation

Overview

I found some great sources on the Internet for how to set up a menu for choosing screen orientation on a tablet computer that doesn't have gyros or other sensors to orient the screen "up." My solution involves a number of scripts, a .desktop file, and some config entries in a few spots.

Design

I wanted a menu, with arrows, where the user selects the arrow that points up. So the current screen orientation will affect the chosen value. Research on the Internet showed that I will need to rotate the tablet input and optionally the wallpaper. My environment uses fluxbox, so the architecture will hardcode in fluxbox controls but obviously this can be adapted as needed.

Dependencies

  • yad
  • xrandr
  • xinput
  • fbsetbg (optional)
  • mktrayicon (optional)

Files involved

I placed all the scripts in /usr/local/bin. The desktop file is placed in /usr/share/applications.

Files modified

  • /etc/rc.local
  • ~/.fluxbox/keys

New files

  • rotate-menu.sh
  • rotate.sh
  • rotate-wallpaper.sh
  • rotate-trayicon.sh
  • rotate-trayicon.desktop
Rotate menu

The main GUI is in rotate-menu.sh. This script uses yad (because it had way fewer dependencies than zenity) to generate a simple window with buttons. The chosen orientation is calculated relative to the current orientation, and the true orientation is passed to the Rotate script.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/bin/sh
# File: rotate-menu.sh
# Locations:
#    /usr/local/bin/
# Author: bgstack15
# Startdate: 2021-10-20 09:50
# SPDX-License-Identifier: GPL-3.0
# Title: Menu for Choosing Screen Orientation on Thinkpad X230
# Purpose: Make it easy to rotate screen
# Usage: ./rotate-menu.sh
#    or press the "screen rotate" button
# Dependencies: yad, xrandr
#    In /etc/rc.local: "setkeycodes 6c 132"
#    In ~/.fluxbox/keys "140 :Exec /usr/local/bin/rotate-menu.sh"
# Reference:
#    https://forums.linuxmint.com/viewtopic.php?t=110395
#    https://forum.thinkpads.com/viewtopic.php?t=108785
#    https://wiki.archlinux.org/title/HiDPI#GDK_3_(GTK_3)
# Improve:
# Documentation:
#    Chose yad because zenity wanted 121MB of dependencies and yad needed no additional dependencies.
#    YES, the fluxbox and rc.local keycodes are not the same. I don't know why they need to be different to work.

rotate_script=/usr/local/bin/rotate.sh
order="left,normal,right,inverted,left,normal,right"

current_orientation="$( xrandr -q --verbose | grep 'connected' | grep -o  -E '\) (normal|left|inverted|right) \(' | grep -o -E '(normal|left|inverted|right)' )"
echo "Currently facing: ${current_orientation}"

new_orientation() {
   # call: new_orientation ${orientation} ${NUMBER}
   # where number is [0-3] and we want to move to the orientation adjacent to old orientation
   first="$( echo "${order}" | tr ',' '\n' | awk -v "or=${1}" '$0 ~ or && a==0 {a=1;print NR}' )"
   #echo "first=${first}"
   echo "${order}" | tr ',' '\n' | awk -v "add=${2}" -v "or=${first}" 'NR==(add+or){print;}'
}

GDK_SCALE=2 yad --form --center --buttons-layout=center --window-icon=display --title='Change screen orientation' --align=center --field='Which direction should be up?:LBL' --button='!go-previous:152' --button='!up:150' --button='!down:151' --button='!go-next:153' 1>/dev/null 2>&1
result=$?
#echo "${result}"
case "${result}" in
   150) echo "pressed button up" ; new="$( new_orientation ${current_orientation} 0 )" ;;
   151) echo "pressed button down" ; new="$( new_orientation ${current_orientation} 2 )" ;;
   152) echo "pressed button left" ; new="$( new_orientation ${current_orientation} 3 )" ;;
   153) echo "pressed button right" ; new="$( new_orientation ${current_orientation} 1 )" ;;
   *) echo "invalid response: ${result}" ;;
esac

echo "new rotation should be ${new}"
"${rotate_script}" "${new}"
Rotate

The actual rotation logic is stored in a separate script, rotate.sh. Decoupling the UI from the functions is always useful, particularly for someone who wants to run arbitary rotation commands. This script rotates the X screen, and also the stylus, eraser (other end of the stylus?), and touch inputs. Without the input rotations, hilarity can ensue. You should try it sometime just to see what it's like.

#/bin/sh
# File: rotate.sh
# Locations:
#    /usr/local/bin/
# Author: bgstack15
# Startdate: 2021-10-20
# SPDX-License-Identifier: GPL-3.0
# Title: Rotate display and inputs
# Purpose: Rotates X display and also the inputs
# History:
# Usage:
#    rotate.sh [left|right|normal|inverted]
#    Called from rotate-menu.sh, which calculates which of the directions to use.
# Reference:
#    https://forums.linuxmint.com/viewtopic.php?t=110395
# Improve:
# Dependencies:
#    xinput

test -z "${DISPLAY}" && { echo "Fatal! Need DISPLAY set. Aborted." 1>&2 ; exit 1 ; }
test -z "${ROTATE_WALLPAPER_SCRIPT}" && ROTATE_WALLPAPER_SCRIPT=/usr/local/bin/rotate-wallpaper.sh
orientation="${1}"

case "${orientation}" in
   normal) wacom_orientation="none" ;;
   inverted) wacom_orientation="half" ;;
   left) wacom_orientation="ccw" ;;
   right) wacom_orientation="cw" ;;
   *) echo "Invalid orientation ${orientation}. Aborted." 1>&2 ; exit 1 ;;
esac

# collect stylus, touch, and eraser IDs
xi="$( xinput list )"
stylus="$( echo "${xi}" | awk '/stylus.*slave.*pointer/{print}' | awk -F'=' '{print $2}' | awk '{print $1}' )"
touch="$( echo "${xi}" | awk '/touch.*slave.*pointer/{print}' | awk -F'=' '{print $2}' | awk '{print $1}' )"
eraser="$( echo "${xi}" | awk '/eraser.*slave.*pointer/{print}' | awk -F'=' '{print $2}' | awk '{print $1}' )"

# MAIN
xrandr -o "${orientation}"
"${ROTATE_WALLPAPER_SCRIPT}"
xsetwacom set "${stylus}" rotate "${wacom_orientation}"
xsetwacom set "${touch}" rotate "${wacom_orientation}"
xsetwacom set "${eraser}" rotate "${wacom_orientation}"
Rotate wallpaper

For additional aesthetic value, I decided to build a script that rotates the wallaper. The admin can establish symlinks in directory /etc/installed/wallpapers, named the same as the orientations that X reports (normal, inverted, left, right). The symlinks can point to whatever you want.

$ cd /etc/installed/wallpapers
$ ln -s wallpaper_wide.jpg normal
$ ln -s wallpaper_wide.jpg inverted
$ ln -s wallpaper_tall.jpg left
$ ln -s wallpaper_tall.jpg right

The script rotate-wallpaper.sh uses these symlinks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/sh
# File: rotate-wallpaper.sh
# Locations:
#    /usr/local/bin/
# Author: bgstack15
# Startdate: 2021-10-20 22:23
# SPDX-License-Identifier: GPL-3.0
# Title: Choose Wallpaper for Current Orientation
# Purpose: Choose wallpapers from the symlinks in /etc/installed/wallpapers/
# History:
# Usage:
#    rotate-wallpaper.sh
#    Called by ~/.fluxbox/startup or manually, or from rotate-menu.sh
# Reference:
#    rotate-menu.sh
# Improve:
# Dependencies:
#    xrandr, fbsetbg
#    symlinks named same as xrandr orientations: normal, right, inverted, left
# Documentation:
current_orientation="$( xrandr -q --verbose | grep 'connected' | grep -o  -E '\) (normal|left|inverted|right) \(' | grep -o -E '(normal|left|inverted|right)' )"
echo "Currently facing: ${current_orientation}"

case "${current_orientation}" in
   normal|inverted|left|right) fbsetbg -f /etc/installed/wallpapers/"${current_orientation}" ;;
   *) echo "Unknown orientation ${current_orientation}! Aborted." ; exit 1 ;;
esac
Tray icon

The configurations described below set up the hardware "Screen rotate" button to trigger rotate-menu.sh. My hardware doesn't always register presses of the button, so I wanted to add a software button. This script, rotate-trayicon.sh, uses mktrayicon to make a simple icon that runs the main GUI when clicked.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/sh
# File: rotate-trayicon.sh
# Locations:
#    /usr/local/bin/
# Author: bgstack15
# Startdate: 2021-10-21 11:47
# SPDX-License-Identifier: GPL-3.0
# Title: Tray icon for rotate-menu
# Purpose: Display the display icon in the system tray, because the "rotate screen" physical button is not always registered.
# History:
# Usage:
#    rotate-trayicon.sh
#    Can be called from ~/.fluxbox/startup
# References:
# Improve:
# Dependencies:
#    mktrayicon
fifo="${XDG_RUNTIME_DIR:-/tmp}/$$.icon"
mkfifo "${fifo}"
mktrayicon "${fifo}" &
echo "i display" >> "${fifo}"
echo "m Exit,echo 'q'>> ${fifo};sleep 2;rm ${fifo};" >> "${fifo}"
echo "c /usr/local/bin/rotate-menu.sh" >> "${fifo}"
echo "s" >> "${fifo}"
Tray icon menu entry

In case the trayicon needs to be started from the application menu, here is my rotate-trayicon.desktop file.

[Desktop Entry]
Comment=Tray icon for asking user the desired screen orientation
Exec=rotate-trayicon.sh
Categories=Utility;TrayIcon;
GenericName=Display orientation helper tray icon
Icon=display
Keywords=display;rotate;
Name=Rotate-menu tray icon
Terminal=false
Type=Application

Configurations

To take advantage of my specific hardware's rotate-screen button, I had to assign the scancode to a keycode. References [1][1] and [2][2] are great for describing how to assign a keycode to a scancode (what happens when you press a button). If you need a reminder, use xev(1) and showkey(1).

Once you know the scancode for your button, set up the setkeycode command in /etc/rc.local.

setkeycodes 6c 132

And then in ~/.fluxbox/keys I used this directive.

140 :Exec /usr/local/bin/rotate-menu.sh

Yes, for some reason the setkeycode and code that Fluxbox uses are not the same. I don't know why, unless one of them is not base10 or something. But I got it to work, some of the time anyway.

References

  1. Tablet PC rotation HOW TO - Linux Mint Forums
  2. [Guide] Setting up Tablet Screen Rotation with Linux - Thinkpads Forum
  3. HiDPI - ArchWiki#GDK

Fprintd and old policykit

On my Thinkpad X230 Tablet running Devuan GNU+Linux, I could see my fingerprint reader but my user did not have permission to enroll fingers. I would get this error:

$ fprintd-enroll --finger right-thumb
Using device /net/reactivated/Fprint/Device/0
Enrolling right-thumb finger.
EnrollStart failed: GDBus.Error:net.reactivated.Fprint.Error.PermissionDenied: Not Authorized: net.reactivated.fprint.device.enroll

Clearly the device is accessible to the root user.

$ sudo fprintd-enroll --finger right-thumb
Using device /net/reactivated/Fprint/Device/0
Enrolling right-thumb finger.

I am used to working with PolicyKit, so I whipped up a .rules file! This is file /usr/share/polkit-1/rules.d/80-fprintd.rules.

polkit.addRule(function(action, subject) {
   if (action.id.indexOf("net.reactivated.fprint.") == 0 || action.id.indexOf("net.reactivated.Fprint.") == 0) {
      polkit.log("action=" + action);
      polkit.log("subject=" + subject);
      return polkit.Result.YES;
   }
});

Unfortunately, it didn't fix my problem! I recalled that Debian uses an older version of PolicyKit (technically the name of the old package, before it was rewritten or renamed to polkit). So I had to go learn how to write a .pkla file. This goes in file /etc/polkit-1/localuthority/20-org.d/fprintd.pkla

[Everyone fingerprints]
Identity=unix-group:*
Action=net.reactivated.fprint.device.*
ResultAny=yes
ResultInactive=no
ResultActive=yes

No daemon restarts are necessary. Just define this file with these contents, and now the fingerprint reader enrollment is available to all users!

References

Man pages

  1. pklocalauthority(8)

Polkit rule for Fedora Media Writer

Policy Kit is a complex piece of software that tries to do more than it probably should. And sometimes you get stuck using software like that. Thankfully, there's a great manpage on the Internet for writing the rules.

I added a policykit rule so that Fedora Media Writer (a gui for placing an image on removable media) would work with my "admins" user group. I use a FreeIPA group named admins instead of the local group wheel.

Write a new file, /usr/share/spolkit-1/rules.d/fedora-media-writer.rules and fill with these contents.

polkit.addRule(function(action, subject) {
   if (action.id == "org.freedesktop.udisks2.open-device") {
      polkit.log("action=" + action);
      polkit.log("subject=" + subject);
      if (subject.isInGroup("wheel") || subject.isInGroup("admins")) {
         return polkit.Result.YES;
      }
   }
});

The policykit daemon should immediately detect the changes.

Adding statistics to my static website

Adding statistics

My previous blog solution had a statistics page, and I wanted to see something similar. I chose awstats as my solution.

Installing awstats

yum install awstats htmldoc

Set up file /etc/awstats.doc7-01a.conf with these interesting settings. See included file awstats.doc7-01a.conf.example for the full file.

LogFile="/usr/share/awstats/tools/logresolvemerge.pl /var/log/nginx/access.log /var/log/nginx/access.log*.gz |"
LogType=W
LogFormat="%host - %logname %time1 %methodurl %code %bytesd %refererquot %uaquot %otherquot "
LogSeparator=" "
SiteDomain="bgstack15.ddns.net"
HostAliases="bgstack15.ddns.net"
DNSLookup=1
DynamicDNSLookup=0
DirData="/var/lib/awstats/regular"
SkipFiles="REGEX[^\/stats] REGEX[^\/isso] /blog/tagcloud.html"
Logo="favicon_128.png"
LogoLink="https://bgstack15.ddns.net/blog/"
KeepBackupOfHistoricFiles=1

This LogFormat setting can parse the default nginx log_format directive.

I have set up my 128x128 logo as file /usr/share/awstats/wwwroot/icon/other/favicon_128.png.

Configuring nginx for awstats

Write /etc/nginx/default.d/awstats.conf with these contents.

location /stats/ {
   alias /var/lib/awstats/static/;
   index awstats.doc7-01a.html;
   location /stats/css/ {
      alias /usr/share/awstats/wwwroot/css/;
   }
   location /stats/icon/ {
      alias /usr/share/awstats/wwwroot/icon/;
   }
   auth_basic "Admin";
   auth_basic_user_file /etc/nginx/awstats.htpasswd;
}
location /awstatscss/ {
   alias /usr/share/awstats/wwwroot/css/;
}
location /awstatsicons/ {
   alias /usr/share/awstats/wwwroot/icon/;
}

Reload nginx.

Add a htpassword file, manually Reference 1.

printf "`read -p Username:\ ; echo $REPLY`:`openssl passwd -apr1`\n" | sudo tee /etc/nginx/awstats.htpasswd

Choose a suitable username and password.

Generating stats

Write a new script, /usr/local/bin/run-awstats.sh with contents:

1
2
3
4
#!/bin/sh
# Startdate: 2021-09-26 12:25
/usr/share/awstats/wwwroot/cgi-bin/awstats.pl -update -config=doc7-01a -configdir="/etc/awstats" -showdropped
/usr/share/awstats/tools/awstats_buildstaticpages.pl -config=doc7-01a -configdir="/etc/awstats" -dir=/var/lib/awstats/static -buildpdf=/usr/bin/htmldoc

The first command generates the raw awstats entry files which are probably used by the cgi scripts (not used in my solution), and the second command generates the static files that the previous nginx config points to.

Add a cron job for this script, /etc/cron.d/90_awstats_cron.

# file: /etc/cron.d/90_awstats_cron
3 0 * * *   root  /usr/local/bin/run-awstats.sh 1>/dev/null 2>&1
5 5 * * 2   root  /usr/local/bin/run-awstats-lastmonth.sh 1>/dev/null 2>&1

The lastmonth script is designed to generate the end-of-month pages for historical daily data from that month. File run-awstats-lastmonth.sh contains these contents:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/sh
# Startdate: 2021-10-03 14:46
# Documentation: blog-README.md
# Reference:
#    https://joshuakugler.com/generating-static-pages-for-all-awstats-files.html
test -z "${M}" && {
   YM="$( date --date "now-1 month" "+%Y-%m" )"
   Y="$( echo "${YM}" | awk -F'-' '{print $1}' )"
   M="$( echo "${YM}" | awk -F'-' '{print $2}' )"
}
echo "Y=${Y}"
echo "M=${M}"
/usr/share/awstats/tools/awstats_buildstaticpages.pl -config=doc7-01a -configdir=/etc/awstats -dir=/var/lib/awstats/static -builddate="${YM}" -year="${Y}" -month="${M}"

I generated menu.html manually, with links to the month pages. To simplify the matter, the links exist, but the pages do not yet, for future months. Some trimmed contents of this file are below.

<html>
<head>
<base target="_top" href="/stats/">
<style type="text/css">
a.gone {
   text-decoration: line-through;
   color: #808080;
}
</style>
</head>
<body>
<table>
<tbody>
<tr><th><a href="">current</a></th></tr>
<tr>
<th>2021</th>
<td><a href="awstats.doc7-01a.2021-01.html" class="gone">01</a></td>
<td><a href="awstats.doc7-01a.2021-02.html" class="gone">02</a></td>
<td><a href="awstats.doc7-01a.2021-09.html">09</a></td>
<td><a href="awstats.doc7-01a.2021-10.html">10</a></td>
</tr>
<th>2022</th>
<td><a href="awstats.doc7-01a.2022-01.html">01</a></td>
<td><a href="awstats.doc7-01a.2022-12.html">12</a></td>
</tr>
</tbody>
</table>
</body>
</html>

Configuring SELinux

I want to use SELinux, so I established this policy using filename awstats1.te.

module awstats1 1.0;

require {
    type httpd_t;
    type awstats_var_lib_t;
    class file { getattr open read };
}

#============= httpd_t ==============

allow httpd_t awstats_var_lib_t:file { open read getattr };

To apply, run these commands.

sudo checkmodule -M -m -o awstats1.mod awstats1.te && sudo semodule_package -o awstats1.pp -m awstats1.mod && sudo semodule -i awstats1.pp ;

Additional assets deployed at initialization time

Files on doc7-01a

  • /usr/local/bin/run-awstats.sh
  • /usr/local/bin/run-awstats-lastmonth.sh
  • /var/lib/awstats/static/menu.html

Operations

Changing observed statistics

Over time, I might learn what statistics need to be modified or excluded. The most interesting is SkipFiles in file /etc/awstats/awstats.doc7-01a.conf.

References

  1. Installing AWStats on Ubuntu 20.04 with Nginx
  2. Install, configure and protect Awstats for multiple nginx vhost in Debian/Ubuntu
  3. AWStats for Nginx
  4. Generating Static Pages For All AWStats files - TechOpinionation
  5. javascript - Adjust width and height of iframe to fit with content in it - Stack Overflow