HiveBrain v1.2.0
Get Started
← Back to all entries
patternbashMinor

Raspberry Pi headless server using bash and USB automounting

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
andautomountingraspberryusbheadlessusingserverbash

Problem

Revised from: Bash scripts and udev rules to handle USB auto mounting / unmounting

Tested:

Uses USB insert/remove to control a headless Raspberry Pi 3 with Raspian Jessie Lite

Changes:

  • Implement more functions



  • Improve comments



  • Modify if-fi exit handling



  • Add optional flag for auto processing on insert/remove



  • Optional function for USB insert - copy file (future: read config/start streaming process)



  • Optional auto shutdown for USB removal (future: close process started by insert)



Goals:

  • Improve bash coding and learn by implementing suggestions



  • Improve my understanding of if-fi blocks and using inline commands (I'm not confident at all when it comes to streamlining code)



Current Code:

Uses udev rules to automount USB and create folder for device. Optionally can use an init constant to cause the automount/dismount process to auto start a process (in this case copy a config file) and shutdown pi on removal.

Work Flow:

uDev rules 
   usb-initloader.sh   
      Insert -> usb-automount.sh
      Remove -> usb-unloader.sh


udev rules

# /etc/udev/rules.d/85-usb-loader.rules
# ADD rule: 
#       if  USB inserted, 
#       and flash drive loaded as sd#
#           pass on dev name and device formatting type
#       run short script to fork another processing script
#       run script to initiate another script (first script must finish quickly)
#             to mkdir and mount, process file
#
# reload rules on PI by: 
#                        sudo udevadm control --reload-rules
#
ACTION=="add", KERNEL=="sd*[0-9]", SUBSYSTEMS=="usb", RUN+="/home/pi/scripts/usb-initloader.sh ADD %k $env{ID_FS_TYPE}"
ACTION=="remove", KERNEL=="sd*[0-9]", SUBSYSTEMS=="usb", RUN+="/home/pi/scripts/usb-initloader.sh %k"


usb-initloader.sh

```
#!/bin/bash
#
# /home/pi/scripts/usb-initloader.sh
#
# this script uses udev rules
# is initiated when usb device is inserted or removed
#
# DEVICE INSERTED - new USB device inserted
# ---------

Solution

Introduce a helper function: is_mounted

As I suggested in the previous review, I recommend to replace this code:

device_mounted=$(grep "$DEVICE" /etc/mtab)
if [ "$device_mounted" ]; then
    echo "Error: seems /dev/$DEVICE is already mounted"
    exit 1
fi

# ...

device_mounted=$(grep "$DEVICE" /etc/mtab)
if [ "$device_mounted" == "" ]; then
    echo "Error: Failed to Mount $MOUNT_DIR/$DEVICE"
    exit 1
fi


With this:

if is_mounted "$DEVICE"; then
    echo "Error: seems /dev/$DEVICE is already mounted"
    exit 1
fi

# ...

if ! is_mounted "$DEVICE"; then
    echo "Error: Failed to Mount $MOUNT_DIR/$DEVICE"
    exit 1
fi


Where the implementation of is_mounted:

is_mounted() {
    grep -q "$1" /etc/mtab
}


It's shorter and actually quite intuitive.

Introduce a helper function: fatal

Another repeating pattern I see is the check-then-exit combos, like this:

if some_requirement_fails; then
    echo "Error: Failed some_requirement"
    exit 1
fi


You could create a helper function to make repeated usages slightly easier:

fatal() {
    echo "Error: $*"
    exit 1
}

if some_requirement_fails; then
    fatal "Failed some_requirement"
fi


Actually, this form opens up the possibility for a more compact syntax:

some_requirement || fatal "Failed some_requirement"


With this and the earlier suggestions, automount could be written like this:

automount() {

    dt=$(date '+%Y-%m-%d/ %H:%M:%S')
    echo "--- USB Auto Mount --- $dt"

    # check input parameters
    [ "$MOUNT_DIR" ] || fatal "Missing Parameter: MOUNT_DIR"
    [ "$DEVICE" ] || fatal "Missing Parameter: DEVICE"
    [ "$FILESYSTEM" ] || fatal "Missing Parameter: FILESYSTEM"

    # Allow time for device to be added
    sleep 2

    is_mounted "$DEVICE" && fatal "seems /dev/$DEVICE is already mounted"

    # test mountpoint - it shouldn't exist
    [ -e "$MOUNT_DIR/$DEVICE" ] && fatal "seems mountpoint $MOUNT_DIR/$DEVICE already exists"

    # make the mountpoint
    sudo mkdir "$MOUNT_DIR/$DEVICE"

    # make sure the pi user owns this folder
    sudo chown -R pi:pi "$MOUNT_DIR/$DEVICE"

    # mount the device base on USB file system
    case "$FILESYSTEM" in

        # most common file system for USB sticks
        vfat)  sudo mount -t vfat -o utf8,uid=pi,gid=pi "/dev/$DEVICE" "$MOUNT_DIR/$DEVICE"
              ;;

        # use locale setting for ntfs
        ntfs)  sudo mount -t auto -o uid=pi,gid=pi,locale=en_US.UTF-8 "/dev/$DEVICE" "$MOUNT_DIR/$DEVICE"
              ;;

        # ext2/3/4 do not like uid option
        ext*)  sudo mount -t auto -o sync,noatime "/dev/$DEVICE" "$MOUNT_DIR/$DEVICE"
              ;;
    esac

    is_mounted "$DEVICE" || fatal "Failed to Mount $MOUNT_DIR/$DEVICE"

    echo "SUCCESS: /dev/$DEVICE successfully mounted as $MOUNT_DIR/$DEVICE"
}


Notice that the calls to fatal can be chained using || or &&, depending on whether the requirement checked should be true or false, respectively.

If you're not comfortable yet with chaining commands with || and &&,
you can stick to the if-fi syntax, there's nothing wrong with that.

Explanation about exit codes, grep -q, if, && and ||

You mentioned in comment that the grep -q part is not exactly easy to understand, so here's a bit more explanation, I hope it helps.

grep exits with exit code 0 if there was a match, and some non-zero exit code if there was no match. For example:

$ echo hello | grep e
hello
$ echo $?
0
$ echo hello | grep x
$ echo $?
1


The $? variable stores the exit code of the last command.

We can build conditions using the exit codes of commands, for example:

$ if echo hello | grep e; then echo success; else echo failure; fi
hello
success
$ if echo hello | grep x; then echo success; else echo failure; fi
failure


Notice that in case of success, the matched pattern is printed. Of course. That's why we normally use grep, to find matching lines.
If we don't care about the matching line, if we just want to know if there was a matching line or not, then we can suppress the output using the -q flag.
Rerunning the above using the -q flag:

$ if echo hello | grep -q e; then echo success; else echo failure; fi
success
$ if echo hello | grep -q x; then echo success; else echo failure; fi
failure


Notice the difference from earlier: no more "hello" line,
the matched pattern was not printed.

Lastly, the same example using && and || instead of if statement:

$ echo hello | grep -q e && echo success || echo failure
success
$ echo hello | grep -q x && echo success || echo failure
failure


Slightly more compact, but equivalent solution.
But this is by no means a preferred syntax.
It's "ok" to use this syntax when the condition is simple and easy to understand.
It's not well-suited and can get very confusing with more complex conditions,
and then it's not recommended.

Code Snippets

device_mounted=$(grep "$DEVICE" /etc/mtab)
if [ "$device_mounted" ]; then
    echo "Error: seems /dev/$DEVICE is already mounted"
    exit 1
fi

# ...

device_mounted=$(grep "$DEVICE" /etc/mtab)
if [ "$device_mounted" == "" ]; then
    echo "Error: Failed to Mount $MOUNT_DIR/$DEVICE"
    exit 1
fi
if is_mounted "$DEVICE"; then
    echo "Error: seems /dev/$DEVICE is already mounted"
    exit 1
fi

# ...

if ! is_mounted "$DEVICE"; then
    echo "Error: Failed to Mount $MOUNT_DIR/$DEVICE"
    exit 1
fi
is_mounted() {
    grep -q "$1" /etc/mtab
}
if some_requirement_fails; then
    echo "Error: Failed some_requirement"
    exit 1
fi
fatal() {
    echo "Error: $*"
    exit 1
}

if some_requirement_fails; then
    fatal "Failed some_requirement"
fi

Context

StackExchange Code Review Q#134233, answer score: 3

Revisions (0)

No revisions yet.