CategoryProgramming

Locally remount volumes from Docker to be used by local user using bindfs

#!/bin/bash
set -exou pipefail

# Location of the script (not the location from where it is executed from
THISDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

DOCKER_VOLUME_DIR=volumes  # This is the directory docker mounts to
LOCAL_DOCKER_VOLUME_DIR=localvolumes # This is the directory you want to locally mount to.
INSIDE_CONTAINER_USER=1000

# Bindfs is required
APP=bindfs; [ -x "`which ${APP}`" ] || sudo apt install ${APP}

# Create local directory to map volume to.
[ ! -d ${THISDIR}/${LOCAL_DOCKER_VOLUME_DIR} ] && mkdir -p ${THISDIR}${LOCAL_DOCKER_VOLUME_DIR}

# Unmount if already mounted
sudo umount ${THISDIR}/${LOCAL_DOCKER_VOLUME_DIR} || true

# Bet local users group
GROUP=`id -g -n $USER`

# Mount
sudo bindfs -u $USER -g "$GROUP" --create-for-user=${INSIDE_CONTAINER_USER} --create-for-group=${INSIDE_CONTAINER_USER} ${THISDIR}/${DOCKER_VOLUME_DIR} ${THISDIR}/${LOCAL_DOCKER_VOLUME_DIR}

Based on https://www.fullstaq.com/knowledge-hub/blogs/docker-and-the-host-filesystem-owner-matching-problem

Bash cheatsheet!

Just a collection of oneliners often use, but always forget 🙂 The following can be used from the terminal, or be used as a script.

#!/bin/bash
# ^^ Always start with a shebang!

# -e = immediately exit if any command has non-zero exit status
# -o pipefail = prevents masking of errors in a pipeline
# -u = error when using reference that has not been defined 
set -eou pipefail

# Check if directory is present before creating to avoid annoying warning.
[ ! -d /tmp/whatever ] && mkdir -p /tmp/whatever

# Check if file exists before performing operation on it.
[ -f file.txt ] && echo "blah" >> file.txt

# Check that script is NOT run as root
[[ $EUID -ne 0 ]] || {
    echo "[ERROR]     Do not run as root!"
    exit 1
}

# Check that script is run as root
[[ $EUID -ne 0 ]] && {
    echo "[ERROR]     This script must be run as root"
}

# Location of the script (not the location from where it is executed from
THISDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

# Grep, but do not match grep itself -> put first character of grep match in square brackets []. Suppose we want to see if blender is still running:
ps ax | grep [b]lender

# Kill application by name (when pkill is absent ;) ) Suppose we want to kill "my-awesome-app" 
ps ax | grep -i [m]y-awesome-app | awk '{ print $2 }'

# Install only if not installed.
APP=bindfs; [ -x "`which ${APP}`" ] || sudo apt install ${APP}

# Use default if first argument on command line is not set.
FIRST_ARG="${1:-some_default_var}"  

# Get the amount of CPUs on the device (-1, to use for parallel jobs)
cpus=$(($(nproc) -1))
# Or
cpus=$(($(grep -c "^processor" /proc/cpuinfo) - 1))

Replace variables from file using sed

Some configuration files are dependent on where the location of you location is. Therefore I mostly have variables in the configuration files, that I replace upon installation using sed.

As an example I have a systemd service file (example.service):

[Unit]
Description=My app

[Service]
WorkingDirectory={{WORKING_DIR}}
ExecStart=/usr/bin/python3 {{WORKING_DIR}}/app.sh
Restart=always

[Install]
WantedBy=multi-user.target

Then run the following on this file:

sed "s,{{WORKING_DIR}},/location/to/application/dir,g" example.service > /etc/systemd/system/example.service

Then the service file is installed and the variables are replaced with the path to your liking.

Useful one-liners

https://gist.github.com/johnnypea/b0cd77e5734d65691fa21d93274b305b

Mounting USB stick systemd

udev rules 99-usb-mount-rules

# Warning this mounts only the first partition to the defined place, assuming first partition is 1
KERNEL=="sd[a-z]*1", SUBSYSTEMS=="usb", ACTION=="add", RUN+="/bin/systemctl start automount@%k.service"
KERNEL=="sd[a-z]*1", SUBSYSTEMS=="usb", ACTION=="remove", RUN+="/bin/systemctl stop automount@%k.service"

Add systemd service: automount@.service

[Unit]
Description=Automount usb drive to %i

[Service]
Type=oneshot
RemainAfterExit=true

Environment="MOUNTPOINT=/media/user_data/EXTERNAL"

ExecStart=/usr/bin/mount-usb-drive.sh add %i
ExecStop=/usr/bin/mount-usb-drive.sh remove %i

Add script to filesystem that actually mounts the usb drive:

#!/bin/bash

# First parameter indicates to add or remove drive.
# last parameter indicates the device name.

WATCHDIR="/tmp/swuwatchdir"
mkdir -p ${WATCHDIR}

usage()
{
    echo "Usage: $0 {add|remove} device_name (e.g. sdb1)"
    exit 1
}

function do_mount()
{
    check_mount_state

    # Get formatting of the drive. TYPE
    # "blkid -o udev" option does not work for busybox version of blkid.
    eval $(/sbin/blkid ${DEVICE} | tr " " "\n" | tail -n 1)

    echo "Mounting ${DEVICE} of type ${TYPE} to ${MOUNTPOINT}"

    /bin/mkdir -p ${MOUNTPOINT}

    # Global mount options
    OPTS="rw,relatime,sync"

    # File system type specific mount options
    if [[ ${TYPE} == "vfat" ]]; then
        OPTS+=",users,gid=100,umask=000,shortname=mixed,utf8=1,flush"
    fi

    if ! /bin/mount -o ${OPTS} ${DEVICE} ${MOUNTPOINT}; then
        echo "Error mounting ${DEVICE} (status = $?)"
        /bin/rmdir ${MOUNTPOINT}
        exit 1
    fi

    # Place dummy file in MOUNTPOINT/../mountWatch to not
    # have to watch the entire user directory.
    touch ${WATCHDIR}/asdf
    echo "**** Mounted ${DEVICE} at ${MOUNTPOINT} ****"
}

function do_unmount()
{
    if [[ ! -d ${MOUNTPOINT} ]]; then
        echo "Warning: ${DEVICE} is not mounted"
    else
        /bin/umount -l ${DEVICE}
        echo "**** Unmounted ${DEVICE}"
    fi


    rm ${WATCHDIR}/asdf
    /bin/rmdir ${MOUNTPOINT}
}

function check_mount_state() {
    # See if this drive is already mounted, and if so where
    MOUNTED_TO=$(/bin/mount | /bin/grep ${DEVICE} | /usr/bin/awk '{ print $3 }')
    if [[ -n ${MOUNTED_TO} ]]; then
        echo "Warning: ${DEVICE} is already mounted at ${MOUNTED_TO}"
        echo "We only expect one USB drive to be inserted at a time."
        exit 1
    fi
}

function main() {

    if [[ $# -ne 2 ]]; then
        usage
    fi

    ACTION=$1
    DEVBASE=$2
    DEVICE="/dev/${DEVBASE}"

    # Var MOUNTPOINT should be set by the systemd script. Check it.
    if [[ -z ${MOUNTPOINT} ]]; then
        echo "MOUNTPOINT is not set by systemd script."
        exit 1
    fi

    case "${ACTION}" in
        add)
            do_mount
            ;;
        remove)
            do_unmount
            ;;
        *)
            usage
            ;;
    esac
}

main $@

Based on: https://serverfault.com/questions/766506/automount-usb-drives-with-systemd

Blurry vscode and chrome browser in Ubuntu 22.04

I recently bought a new laptop and installed a fresh Ubuntu! (Ubuntu 22.04.1 LTS). But due to Wayland the Electron apps (in my case Chrome and VSCode) do not support scaling.

I often search for fixes online, but could not find a fix at first. But a week later I fortunately stumbled upon the answers!

Chrome

The fix for chrome is to install the unstable version:

sudo apt install google-chrome-unstable

And alter the desktop launch file to make sure it starts with the arguments as provide in this post:

sudo code /usr/share/applications/google-chrome-unstable.desktop

Make sure the execute reads the following:

Exec=/usr/bin/google-chrome-unstable %U -enable-features=UseOzonePlatform -ozone-platform=wayland

Visual Studio Code

For vscode we follow a similar approach, but we do not have to install an unstable version. Open the desktop file.

sudo code /usr/share/applications/code.desktop

Make sure the execute line under [Desktop Entry] reads:

Exec=/usr/share/code/code --unity-launch %F --enable-features=UseOzonePlatform --ozone-platform=wayland --log debug

And under [Desktop Action new-empty-window] reads:

Exec=/usr/share/code/code --new-window %F --enable-features=UseOzonePlatform --ozone-platform=wayland --log debug

I often start vscode from a terminal, and that has not the arguments that we added in the *.desktop files by default. Therefore I created an alias in my ~/.zshrc (Yes, I used zsh).

alias code='f() {/usr/bin/code $@ --enable-features=UseOzonePlatform --ozone-platform=wayland};f'

now, when launching code it launches it with the correct parameters.

Last step is to fix the titlebar again, in vscode:

ctrl + shift + p

Open the settings in JSON and add:

"window.titleBarStyle": "custom"

Get status of git repositories in directory

I have a development directory (~/dev) which houses all of my git repositories.
Recently I bought another laptop, but to make sure I pushed all my local development I check the status of the git repositories with the following bash script:

#!/bin/bash

gitrepos=`find . -type d -name ".git" -prune -print | xargs -I {} dirname {}`

for repo in $gitrepos; do
  pushd $PWD > /dev/null
  cd $repo
  # echo $PWD
  if git diff-index --quiet HEAD --; then
    echo -e "UP TO DATE\t - $repo"
  else
    echo -e "CHANGES\t\t - $repo"
  fi
  popd > /dev/null
done

Place the code above in a file e.g. repostatus.sh and make it executable: chmod +x repostatus.sh.

Programatically switch tabs in a browser

So, if been looking to programatically switch tabs of a browser in kiosk mode. This is a quick way to do exactly that.

So first you have to install xdotool. I did this on a Ubuntu machine using apt:

$ sudo apt install xdotool

Then put the following in a file and make that executable (chmod +x yourscript.sh):

#!/bin/bash

BROWSER="Brave"
direction=$1

action="Ctrl+Tab"
if [[ "${direction}" == "left" ]]; then
  action="Ctrl+Shift+Tab"
fi

xdotool windowactivate --sync $(xdotool search --name ${BROWSER} | tail -n 1) && xdotool key ${action}

Execute it with an argument “left” or “right” (right = default). On execution it will locate your browser (“Brave”), then it emulates a shortcut to go to the next or previous tab.

The goals is to fire this script once a button has been pressed on a Kiosk such that the user can choose which tab to show.

From CSV to Herma 8632 PDF file

Our first born is about to arrive. Instead of writing out all the birth announcements by hand I decided to write a little python script that creates a PDF that matches sticker sheets (Herma 8632 format). It exactly took me one laptop battery charge to create this script. I refactored a bit later to be a bit more OO.


#!/usr/bin/env python3

from fpdf import FPDF
import sys
import os

# Very simple CSV to Herma 8632

# CSV expected in following format (comma separated , WITH header):
#
#   Name, Relation, Street + number, ZIP, City, Country
#   John Doe, friends, Stationstraat 12, 1234 DD, Duckcity, NL


def main():

  args = InputParser(sys.argv)

  addresses = CSVParser(args.inputFile)
  hermaTemplate = Herma()

  hermaTemplate.drawAddressesOnHerma(addresses.data)
  hermaTemplate.save(args.outputFile)

# ------------------------------------------------------------------

## Class Definitions
class InputParser:
  def __init__(self, args) -> None:
    self.args = args
    if len(args) != 3:
      self.help()
      exit(1)

    self.input = args[1]
    self.output = args[2]

    if not os.path.exists(self.input):
      print(f"Inputfile \"{self.input}\" does not exist!")
      exit(1)

  def help(self):
    print(f"Use python3 {self.args[0]} input.csv output.pdf")

  @property
  def inputFile(self):
    return self.input

  @property
  def outputFile(self):
    return self.output

class CSVParser:
  def __init__(self, filename) -> None:
    self.addresses = []
    self.filename = filename
    self.parse()

  def parse(self):
    # TODO: File present checking
    with open(self.filename) as f:
      data = f.readlines()
      for d in data[1:]:
        split = d.split(',')
        address = "\n".join([split[0], split[2], split[3], split[4], split[5]])
        self.addresses.append(address)

  @property
  def data(self):
    return self.addresses

class Herma:
  def __init__(self) -> None:
    self.pdf = FPDF()
    self.pdf.add_page()
    self.pdf.set_auto_page_break(False)
    self.pdf.set_font("Times", size = 10)

    self.CELL_WIDTH = 63.5
    self.CELL_HEIGHT = 38.1
    self.LEFT_PAGE_MARGIN = 7.21
    self.TOP_PAGE_MARGIN = 15.15
    self.BETWEEN_CELL_SPACE = 2.54

    self.row = 0
    self.col = 0

    self.MAX_ROWS = 7
    self.MAX_COLS = 3

  def drawAddressesOnHerma(self, addresses):
    for a in addresses:
      x,y = self._selectCell(self.row, self.col)
      self._drawAddress(x, y, a)
      self._autoIncrement()


  def _drawAddress(self, x, y, address):
    self.pdf.set_xy(x, y+10)

    for l in address.split('\n'):
      self.pdf.set_x(x)
      self.pdf.cell(w = self.CELL_WIDTH, h = 5, ln = 1, txt = l, align= "C")


  def _selectCell(self, row, col):
    x = self.LEFT_PAGE_MARGIN + (col % self.MAX_COLS) * (self.BETWEEN_CELL_SPACE + self.CELL_WIDTH)
    y = self.TOP_PAGE_MARGIN + (row % self.MAX_ROWS * self.CELL_HEIGHT)

    self.pdf.set_xy(x, y)
    self.pdf.cell(w = self.CELL_WIDTH, h = self.CELL_HEIGHT, txt = "", border = 0)

    return x,y

  def _autoIncrement(self):
    self.col += 1

    if self.col % self.MAX_COLS == 0:
      self.row += 1
      if self.row % self.MAX_ROWS == 0:
        self.pdf.add_page()
        self.col = 0
        self.row = 0

  def save(self, filename):
    self.pdf.output(filename)
    print(f"Please find your file here: {filename}")

main()

Mount docker volume as same user as on host machine

docker containers often run as the root user (uid = 0, guid = 0). Files that the users generates are therefore also owned by the root user.

By creating an extra user on the docker system, and giving that user the same uid and guid as the user on you host system you will be able to modify your files without the need to be root on you host machine.

The following assumes you created an additional user in your docker container, and that it got 1000 for uid and guid.

#!/bin/bash

# Mr. R
# 06-2020

args=$@
cmd="builder.sh ${args}"

# current working directory is a volume mount to something on the host system.
# Therefore stat -c will provide the uid and gid of the host user.
# setting this uid and gid for the container user results in files written to the
# volume as host user.
usr=`id -nu 1000`
grp=`id -ng 1000`
groupmod -g $(stat -c "%g" .) $grp
usermod -u $(stat -c "%u" .) -g $(stat -c "%g" .) $usr

# Force all volume mounts to be of the appuser!
chown -R ${usr}:${grp} ${WORKDIR}

# If nothing given, start a shell, else run the builder script with arguments.
if [ "x${args}x" == "xx" ]; then
  cmd="/bin/bash"
fi

su -m -c "PATH=${PATH}; ${cmd}" ${usr}

© 2025 Roholt

Thema door Anders NorénOmhoog ↑