Use udev to auto mount external disks
6 min read

Use udev to auto mount external disks

Background

I have an Ubuntu mini box acting as a storage server, and I want Ubuntu to automatically mount my external hard drive to a specific path and start the backup process whenever I plug in the hard drive. By default, Ubuntu Desktop mounts external disks to somewhere hidden deeply in the filesystem tree. GUI user usually does’t care about the real path on which the disk is mounted, because they can always find that place in GUI file manager. But for my situation, I want the disk is mounted to a predetermined path every time the disk array is turned on.

Brief introduction to udev

Except from Arch Linux’s wiki: https://wiki.archlinux.org/title/udev:

udev is a userspace system that enables the operating system administrator to register userspace handlers for events. The events received by udev's daemon are mainly generated by the (Linux) kernel in response to physical events relating to peripheral devices. As such, udev's main purpose is to act upon peripheral detection and hot-plugging, including actions that return control to the kernel, e.g., loading kernel modules or device firmware. Another component of this detection is adjusting the permissions of the device to be accessible to non-root users and groups.

Rules

For udev users, they only need to be concerned with rules. Rules are descriptive files that specify what actions to take when an event matching certain filters occurs. For example, if a devices is an usb device and is a block device and the file system label is MY_FS_LABEL, then mount this device to /mnt/my_fs.

Rules location

According to man udev:

The udev rules are read from the files located in the default rules directory /lib/udev/rules.d/, the custom rules directory /etc/udev/rules.d/ and the temporary rules directory /dev/.udev/rules.d/. All rule files are sorted and processed in lexical order, regardless in which of these directories they live.

Custom udev rules should be placed in /etc/udev/rules.d, and like rc.d, files in this directory are read in lexical order that means prefix your rule with a number to control the order of rule processing.

Rule of udev Rules

  1. A rule consists of a list of one or more key value pairs separated by a comma.
  2. Every line in the rules file contains at least one key value pair.
  3. Two kind of keys, match and assignment keys.

Rule contains match keys and assignment keys. Match keys acting like filters to match a device which has some properties, and assignment keys are sort of actions telling what to do if a device is found and matches our rule.

Some of the most used match keys:

  • ACTION: Match the name of the event action. For example, if a usb devices is plugged in, the action is add. Other actions include remove and change .
  • KERNEL: Match the name of the event device. For example sda for block device.
  • SUBSYSTEM: Match the subsystem of the event device. For example, block or usb.
  • SUBSYSTEMS: Search the devpath upwards for a matching device subsystem name.
  • ATTR{attr}: Match sysfs attribute value of the event device.
  • ATTRS{attr}: Search the devpath upwards for a device with matching sysfs attribute values. If multiple ATTRS matches are specified, all of them must match on the same device. One example is device’s vendor id.
  • ENV{key}: Match against a device property value.

Some of the most used assignment keys:

  • SYMLINK: The name of a symlink targeting the node. Every matching rule adds this value to the list of symlinks to be created.
  • RUN: Specify a program to be executed after processing of all the rules for the event.
  • ENV{env}: Set a device property value.

Targeting your device

SUBSYSTEM & ATTR

Suppose we have a usb storage stick, and we want to get to know how to write rule match keys to match this device. The first thing is how to find the whatever match keys our rule can use.

To list available information for specific device:

udevadm info --query=all --attribute-walk --name=/dev/sda

The output is lengthy and contains information from different layers of Linux's device module. In simple terms, a device that eventually appears in the /dev directory goes through various layers of the device module. For example, USB storage involves the PCI, USB, SCSI, and block layers.

Each layer has its own attributes and its up to you to choose the needed attributes. But note that there are SUBSYSTEMS vs SUBSYSTEM and ATTR vs ATTRS match keys.

  • Plural form of a match keys indicates searching up to the top layers.
  • Singular form indicates the last layers.

For example, subsystem of the last layer of USB storage stick is block, and it is also a subsystem of usb. To match the whole path of the device, using SUBSYSTEMS instead of SUBSYSTEM.

The attributes are listed as well by using udevadm info command. Keep in mind to differentiate between ATTRS and ATTR.

KERNEL

It is confusing of this name. The KERNEL indicates how the Linux kernel names this device. For example, sda is a standard Kernel Name for block storage, and sda1 is a standard kernel name designate the first partition of sda.

ENV

ENV{key} Match against a device property value.

It's almost like saying nothing at all. Basically, ENV{env} passes variable across different rules, give the later rules chances to know the result generated by the previous rules. For example, RuleA writes ENV{mnt_path} = /mnt/path, then subsequent rules can use this result. That’s why naming your rules prefixed with a number is important, process sequence is critical. You are sure that an ENV is set, but cannot obtain it from your own rule, confirm the sequence is correct.

String substitution

Imagine a situation where you want to mount a disk to a path following this pattern: /mnt/{disk_fs_label}. How can you obtain the disk_fs_label? First, there is already an existing ENV{ID_FS_LABEL} that tells you what the filesystem's label is. You can utilize this environment by employing string substitution.

One example of usage:

RUN+="/usr/local/bin/tri-mount /dev/%k $env{ID_FS_LABEL}"

%k and $env{IF_FS_LABEL} are string substation format, and according to the man page:

$kernel, %k: The kernel name for this device.
$env{key}, %E{key}: A device property value.

Practical Use

A simple example:  Each time when I insert my usb storage stick, I want to record its UUID to /var/usb_history.

1. Get device’s attributions, submodule, and kernel.

udevadm info --query=all --attribute-walk --name=/dev/sda1

2. Pick out the fields meeting your need.

 looking at device '/devices/pci0000:00/0000:00:14.0/usb2/2-2/2-2:1.0/host2/target2:0:0/2:0:0:0/block/sda/sda1':
    KERNEL=="sda1"
    SUBSYSTEM=="block"

 looking at device '/devices/pci0000:00/0000:00:14.0/usb2/2-2/2-2:1.0/host2/target2:0:0/2:0:0:0/block/sda1':
    KERNEL=="sda"
    SUBSYSTEM=="block"

  looking at parent device '/devices/pci0000:00/0000:00:14.0/usb2/2-2/2-2:1.0/host2/target2:0:0/2:0:0:0':
    KERNELS=="2:0:0:0"
    SUBSYSTEMS=="scsi"
    DRIVERS=="sd"

  looking at parent device '/devices/pci0000:00/0000:00:14.0/usb2/2-2/2-2:1.0':
    KERNELS=="2-2:1.0"
    SUBSYSTEMS=="usb"

Because the device is a usb storage device, the parent SUBSYSTEM we want to match is usb and the final SUBSYSTEM is block. I can’t find a way to match multiple parent SUBSYSTEM , if you happen to know that, please let me know.

3. Get available ENV.

udevadm test /devices/pci0000:00/0000:00:14.0/usb2/2-2/2-2:1.0/host2/target2:0:0/2:0:0:0/block/sda/sda1

ID_VENDOR=Generic
ID_VENDOR_ENC=Generic\x20
...
ID_FS_UUID=0123-4567
ID_FS_UUID_ENC=0123-4567
ID_FS_VERSION=1.0
ID_FS_TYPE=exfat
ID_FS_USAGE=filesystem
...

The long string:

/devices/pci0000:00/0000:00:14.0/usb2/2-2/2-2:1.0/host2/target2:0:0/2:0:0:0/block/sda/sda1

is copied from step2. I have marked that as bold.

From the result, you can find ID_FS_UUID, that is the environment we want.

4. Write the rule:

ACTION=="add",
SUBSYSTEMS=="usb",
SUBSYSTEM=="block",
ENV{ID_FS_TYPE}=="exfat",
RUN+="/usr/bin/bash -c '/usr/bin/echo %E{ID_FS_UUID} >> /var/usb_history'"

DO NOT SPLIT THIS RULE ACROSS MULTIPLE LINES. I USE MULTIPLE LINES HERE TO MAKE THINGS CLEAR.

Match Keys:

  • ACTION=="add": When device is plugged in,
  • SUBSYSTEMS=="usb": And when parent subsystems has “usb”,
  • SUBSYSTEM=="block": And the last subsystem is “block”,
  • ENV{ID_FS_TYPE}=="exfat": And when FS_TYPE is exfat.

Assignment Keys :

  • RUN+="mkdir /mnt/$env{ID_FS_UUID} && /usr/bin/mount /dev/%k /mnt1/%E{ID_FS_UUID}". Create a dir and mount this device on.

5. Test your rule

Run the same command as in step 3.

udevadm test /devices/pci0000:00/0000:00:14.0/usb2/2-2/2-2:1.0/host2/target2:0:0/2:0:0:0/block/sda/sda1

And you can find something like this:

run: '/usr/bin/bash -c '/usr/bin/echo 8a9a51c0-28d5-42a2-83b9-72f70cef0031 >> /var/usb_history''

This comes from our rule, and means our rule works.

6. Reload and Trigger

udevadm control --reload
udevadm trigger

Side notes

Debug

To debug, reloading your rules use:

udevadm control --reload --log-priority=debug

Then check the log:

journalctl -f

Mount disk

a) systemd by default runs systemd-udevd.service with a separate "mount namespace" (see namespaces(7)), which means that mounts will not be visible to the rest of the system.
b) Even if you change the service parameters to fix this (commenting out the PrivateMounts and MountFlags lines), there is another problem which is that processes started from Udev are killed after a few seconds.

Too mount a device in a rule, either write a shell script and invoke it in the RUN or using:

RUN+="/usr/bin/systemd-mount --no-block --collect /dev/%k /media/usb"

Reference:

  1. https://man7.org/linux/man-pages/man5/udev.conf.5.html
  2. https://man7.org/linux/man-pages/man7/udev.7.html
  3. https://man7.org/linux/man-pages/man8/udevadm.8.html
  4. http://www.reactivated.net/writing_udev_rules.html
  5. https://unix.stackexchange.com/a/613748