Skip to content

Instantly share code, notes, and snippets.

@thiscantbeserious
Created October 14, 2025 15:02
Show Gist options
  • Select an option

  • Save thiscantbeserious/9be74ec317e73a2bd24f3fc41ba6f91f to your computer and use it in GitHub Desktop.

Select an option

Save thiscantbeserious/9be74ec317e73a2bd24f3fc41ba6f91f to your computer and use it in GitHub Desktop.
Detect mounted filesystems and add missing <mntent> entries to OMV's config.xml. Writes to a temp copy, validates XML, then atomically replaces the real file. Default: DRY-RUN (prints). Use --apply to write, --deploy to refresh OMV.
#!/usr/bin/env bash
set -Eeuo pipefail
# omv-auto-import-mounts (safe XML edition)
# Detect mounted filesystems and add missing <mntent> entries to OMV's config.xml.
# Writes to a temp copy, validates XML, then atomically replaces the real file.
# Default: DRY-RUN (prints). Use --apply to write, --deploy to refresh OMV.
OMV_XML="/etc/openmediavault/config.xml"
DRY_RUN=1
DO_DEPLOY=0
INCL_NFS=0
INCL_CIFS=0
INCL_MERGERFS=0
usage() {
cat <<'USAGE'
Usage: omv-auto-import-mounts [--apply] [--deploy]
[--include-nfs] [--include-cifs] [--include-mergerfs]
--apply Write <mntent> entries to /etc/openmediavault/config.xml (safely).
--deploy omv-salt deploy run fstab && restart openmediavault-engined.
--include-nfs Also import nfs/nfs4 mounts.
--include-cifs Also import cifs/smb mounts.
--include-mergerfs Also import fuse.mergerfs mounts.
USAGE
}
log(){ echo "[$(date +'%F %T')] $*"; }
# Parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--apply) DRY_RUN=0; shift ;;
--deploy) DO_DEPLOY=1; shift ;;
--include-nfs) INCL_NFS=1; shift ;;
--include-cifs) INCL_CIFS=1; shift ;;
--include-mergerfs) INCL_MERGERFS=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown arg: $1"; usage; exit 2 ;;
esac
done
# Deps
need(){ command -v "$1" >/dev/null 2>&1 || { echo "ERROR: missing '$1'." >&2; exit 1; }; }
need xmlstarlet
need findmnt
need lsblk
need awk
need blkid
[[ -f "$OMV_XML" ]] || { echo "ERROR: $OMV_XML not found." >&2; exit 1; }
# UUID generator (uuid -> uuidgen -> /proc -> python)
gen_uuid() {
if command -v uuid >/dev/null 2>&1; then
uuid
elif command -v uuidgen >/dev/null 2>&1; then
uuidgen
elif [[ -r /proc/sys/kernel/random/uuid ]]; then
cat /proc/sys/kernel/random/uuid
elif command -v python3 >/dev/null 2>&1; then
python3 - <<'PY'
import uuid; print(uuid.uuid4())
PY
elif command -v python >/dev/null 2>&1; then
python - <<'PY'
import uuid; print(uuid.uuid4())
PY
else
echo "ERROR: No UUID generator available (uuid/uuidgen/proc/python)" >&2
exit 1
fi
}
# Skip sets
FSTYPES_SKIP='^(autofs|overlay|overlayfs|tmpfs|devtmpfs|squashfs|cgroup2?|fuse\.lxcfs|fuse\.snapfuse|zram|rpc_pipefs|nsfs|securityfs|debugfs|tracefs|configfs|pstore|bpf|hugetlbfs|mqueue|proc|sysfs|ramfs|zfs_member|crypto_LUKS|LVM2_member)$'
DIRS_SKIP='^/(proc|sys|run|dev|boot|snap|var/(run|lib/(docker|containers))|lib|lost\+found|tmp)(/|$)?'
# Read existing OMV entries -> dedupe sets
mapfile -t OMV_DIRS < <(xmlstarlet sel -t -m '//mntent' -v 'normalize-space(dir)' -n "$OMV_XML" | sort -u)
mapfile -t OMV_FSNAMES < <(xmlstarlet sel -t -m '//mntent' -v 'normalize-space(fsname)' -n "$OMV_XML" | sort -u)
has_dir(){ [[ -n "${1:-}" ]] && printf '%s\n' "${OMV_DIRS[@]}" | grep -Fxq -- "$1"; }
has_fs(){ [[ -n "${1:-}" ]] && printf '%s\n' "${OMV_FSNAMES[@]}" | grep -Fxq -- "$1"; }
# Helpers
dev_to_byuuid() {
local src="$1" uuidv
[[ "$src" =~ ^/dev/disk/by-uuid/ ]] && { echo "$src"; return 0; }
uuidv=$(blkid -s UUID -o value -- "$src" 2>/dev/null | head -n1 || true)
[[ -n "$uuidv" ]] || { echo "$src"; return 0; }
echo "/dev/disk/by-uuid/$uuidv"
}
is_real_mount() {
local src="$1" dir="$2"
[[ "$dir" == "/" ]] && { log "SKIP: root filesystem '/'"; return 1; }
[[ -d "$dir" ]] || { log "SKIP: target not a directory: $dir"; return 1; }
if ! findmnt -nr -T "$dir" >/dev/null 2>&1; then
log "SKIP: not currently mounted: $dir"; return 1
fi
if [[ "$src" =~ ^/dev/ ]]; then
local found
found="$(findmnt -nr -S "$src" -T "$dir" -o TARGET 2>/dev/null || true)"
[[ -n "$found" ]] || { log "SKIP: $dir is mounted, but not from $src"; return 1; }
fi
return 0
}
declare -A seen_dir
declare -a CAND
CAND=()
echo "=== OMV known dirs: ${#OMV_DIRS[@]} ==="
for d in "${OMV_DIRS[@]}"; do echo " $d"; done
echo "=== Scan: block-backed ==="
while IFS='|' read -r DEV TYPE MNT FST; do
if [[ -z "$MNT" ]]; then log "SKIP(block): empty mountpoint for $DEV"; continue; fi
if [[ "$FST" =~ $FSTYPES_SKIP ]]; then log "SKIP(block): fs=$FST ($MNT)"; continue; fi
if [[ "$MNT" =~ $DIRS_SKIP ]]; then log "SKIP(block): system dir $MNT"; continue; fi
if [[ "$DEV" =~ /loop[0-9]+$ ]]; then log "SKIP(block): loop dev $DEV"; continue; fi
OPTS=$(findmnt -nr -S "$DEV" -T "$MNT" -o OPTIONS 2>/dev/null || true)
[[ "$OPTS" =~ (^|,)bind(,|$) ]] && { log "SKIP(block): bind $MNT"; continue; }
if ! is_real_mount "$DEV" "$MNT"; then continue; fi
[[ -n "${seen_dir[$MNT]:-}" ]] && { log "SKIP(block): dup dir $MNT"; continue; }
seen_dir["$MNT"]=1
FSNAME="$DEV"
[[ "$DEV" =~ ^/dev/ ]] && FSNAME="$(dev_to_byuuid "$DEV")"
if has_dir "$MNT"; then log "SKIP(block): already in OMV (dir) $MNT"; continue; fi
if has_fs "$FSNAME"; then log "SKIP(block): already in OMV (fsname) $FSNAME"; continue; fi
echo "ADD(block) -> type=$FST | fsname=$FSNAME | dir=$MNT"
CAND+=("{\"type\":\"$FST\",\"fsname\":\"$FSNAME\",\"dir\":\"$MNT\"}")
done < <(
lsblk -pn -o NAME,TYPE,MOUNTPOINT,FSTYPE -P \
| awk '
{
name=""; type=""; mnt=""; fst="";
for (i=1;i<=NF;i++) {
if ($i ~ /^NAME=/) { sub(/^NAME="/,"",$i); sub(/"$/,"",$i); name=$i }
else if ($i ~ /^TYPE=/) { sub(/^TYPE="/,"",$i); sub(/"$/,"",$i); type=$i }
else if ($i ~ /^MOUNTPOINT=/){ sub(/^MOUNTPOINT="/,"",$i); sub(/"$/,"",$i); mnt=$i }
else if ($i ~ /^FSTYPE=/){ sub(/^FSTYPE="/,"",$i); sub(/"$/,"",$i); fst=$i }
}
if (type ~ /^(part|lvm|crypt|md|rom)$/) {
printf "%s|%s|%s|%s\n", name, type, mnt, fst
}
}'
)
echo "=== Scan: zfs ==="
if command -v zfs >/dev/null 2>&1; then
while IFS=$'\t' read -r NAME MOUNT CANMOUNT MOUNTED; do
[[ "$MOUNTED" == "yes" ]] || { log "SKIP(zfs): not mounted $NAME"; continue; }
[[ "$CANMOUNT" == "off" ]] && { log "SKIP(zfs): canmount=off $NAME"; continue; }
[[ "$MOUNT" == "legacy" || -z "$MOUNT" ]] && { log "SKIP(zfs): legacy/- $NAME"; continue; }
[[ "$MOUNT" =~ $DIRS_SKIP ]] && { log "SKIP(zfs): system dir $MOUNT"; continue; }
if ! is_real_mount "$NAME" "$MOUNT"; then continue; fi
[[ -n "${seen_dir[$MOUNT]:-}" ]] && { log "SKIP(zfs): dup dir $MOUNT"; continue; }
seen_dir["$MOUNT"]=1
if has_dir "$MOUNT"; then log "SKIP(zfs): already in OMV (dir) $MOUNT"; continue; fi
if has_fs "$NAME"; then log "SKIP(zfs): already in OMV (fsname) $NAME"; continue; fi
echo "ADD(zfs) -> type=zfs | fsname=$NAME | dir=$MOUNT"
CAND+=("{\"type\":\"zfs\",\"fsname\":\"$NAME\",\"dir\":\"$MOUNT\"}")
done < <(zfs list -H -o name,mountpoint,canmount,mounted 2>/dev/null || true)
else
echo "(zfs not installed)"
fi
echo "=== Scan: network/union (optional) ==="
if [[ $INCL_NFS -eq 1 || $INCL_CIFS -eq 1 || $INCL_MERGERFS -eq 1 ]]; then
while IFS=' ' read -r SRC TGT FST OPTS; do
[[ -z "$TGT" ]] && { log "SKIP(net): empty target for $SRC"; continue; }
[[ "$TGT" =~ $DIRS_SKIP ]] && { log "SKIP(net): system dir $TGT"; continue; }
[[ "$OPTS" =~ (^|,)bind(,|$) ]] && { log "SKIP(net): bind $TGT"; continue; }
case "$FST" in
nfs|nfs4) [[ $INCL_NFS -eq 1 ]] || { log "SKIP(net): nfs disabled $TGT"; continue; } ;;
cifs|smb3|smbfs) [[ $INCL_CIFS -eq 1 ]] || { log "SKIP(net): cifs disabled $TGT"; continue; } ;;
fuse.mergerfs) [[ $INCL_MERGERFS -eq 1 ]] || { log "SKIP(net): mergerfs disabled $TGT"; continue; } ;;
*) log "SKIP(net): other fs $FST at $TGT"; continue ;;
esac
if ! is_real_mount "$SRC" "$TGT"; then continue; fi
[[ -n "${seen_dir[$TGT]:-}" ]] && { log "SKIP(net): dup dir $TGT"; continue; }
seen_dir["$TGT"]=1
if has_dir "$TGT"; then log "SKIP(net): already in OMV (dir) $TGT"; continue; fi
if has_fs "$SRC"; then log "SKIP(net): already in OMV (fsname) $SRC"; continue; fi
echo "ADD($FST) -> type=$FST | fsname=$SRC | dir=$TGT"
CAND+=("{\"type\":\"$FST\",\"fsname\":\"$SRC\",\"dir\":\"$TGT\"}")
done < <(findmnt -rno SOURCE,TARGET,FSTYPE,OPTIONS | sed 's/|/\\|/g')
else
echo "(skipping NFS/CIFS/mergerfs; enable with flags)"
fi
TOTAL=${#CAND[@]}
echo "=== Summary ==="
echo "To add: $TOTAL"
if [[ $TOTAL -eq 0 ]]; then
echo "Nothing to add. OMV already knows all detected mounts."
exit 0
fi
# ---------- APPLY (safe) ----------
if [[ $DRY_RUN -eq 1 ]]; then
echo "DRY-RUN. Re-run with --apply to write, add --deploy to refresh OMV."
exit 0
fi
# Serialize edits with a lock (best-effort)
LOCK="/run/lock/omv-config.lock"
mkdir -p /run/lock
exec 9>"$LOCK"
if ! flock -n 9; then
echo "ERROR: could not acquire lock $LOCK (another process editing?)." >&2
exit 1
fi
# 1) Create temp working copy
TMP="$(mktemp)"
cp -a "$OMV_XML" "$TMP"
# Ensure required XML nodes exist in temp
if ! xmlstarlet sel -t -c "/config/system/fstab" "$TMP" >/dev/null 2>&1; then
xmlstarlet ed -L \
-s "/config/system" -t elem -n "fstab" -v "" \
"$TMP"
fi
# Function operates on temp file
add_mntent() {
local TYPE="$1" FSNAME="$2" DIR="$3" FILE="$4"
local OPTS="noauto" FREQ="0" PASSNO="0" HIDDEN="0"
case "$TYPE" in
ext4|xfs|btrfs|vfat|ntfs) PASSNO="2" ;;
esac
[[ "$TYPE" == "zfs" ]] && OPTS=""
# De-dupe guard in temp file (fsname+dir pair)
if xmlstarlet sel -t -v \
"count(/config/system/fstab/mntent[fsname='$FSNAME' and dir='$DIR'])" \
"$FILE" | grep -qx '0'; then
local UUID; UUID="$(gen_uuid)"
xmlstarlet ed -L \
-s "/config/system/fstab" -t elem -n "mntent" -v "" \
-s "/config/system/fstab/mntent[last()]" -t elem -n "uuid" -v "$UUID" \
-s "/config/system/fstab/mntent[last()]" -t elem -n "fsname" -v "$FSNAME" \
-s "/config/system/fstab/mntent[last()]" -t elem -n "dir" -v "$DIR" \
-s "/config/system/fstab/mntent[last()]" -t elem -n "type" -v "$TYPE" \
-s "/config/system/fstab/mntent[last()]" -t elem -n "opts" -v "$OPTS" \
-s "/config/system/fstab/mntent[last()]" -t elem -n "freq" -v "$FREQ" \
-s "/config/system/fstab/mntent[last()]" -t elem -n "passno" -v "$PASSNO" \
-s "/config/system/fstab/mntent[last()]" -t elem -n "hidden" -v "$HIDDEN" \
"$FILE"
echo "CREATED: $TYPE $FSNAME -> $DIR [uuid=$UUID]"
else
log "SKIP(xml): already present $FSNAME -> $DIR"
fi
}
# 2) Append all candidates to temp file
for t in "${CAND[@]}"; do
TYPE=$(echo "$t" | sed -n 's/.*"type":"\([^"]*\)".*/\1/p')
FSNM=$(echo "$t" | sed -n 's/.*"fsname":"\([^"]*\)".*/\1/p')
DIR=$(echo "$t" | sed -n 's/.*"dir":"\([^"]*\)".*/\1/p')
add_mntent "$TYPE" "$FSNM" "$DIR" "$TMP"
done
# 3) Validate temp XML (well-formed)
if ! xmlstarlet val -w "$TMP" >/dev/null 2>&1; then
echo "ERROR: XML not well-formed after edits; aborting. Temp at: $TMP" >&2
exit 1
fi
# 4) Pretty-print (also fails on malformed)
if ! xmlstarlet fo "$TMP" > "${TMP}.pretty" 2>/dev/null; then
echo "ERROR: xmlstarlet format failed; aborting. Temp at: $TMP" >&2
exit 1
fi
mv -f "${TMP}.pretty" "$TMP"
# 5) Backup live, then atomic replace
BK="$OMV_XML.bak.$(date +%F_%H%M%S)"
cp -a "$OMV_XML" "$BK"; log "Backup: $BK"
install -m 0644 "$TMP" "$OMV_XML"
# 6) Optional deploy
if [[ $DO_DEPLOY -eq 1 ]]; then
log "Deploying fstab & restarting OMV engine..."
if command -v omv-salt >/dev/null 2>&1; then
omv-salt deploy run fstab || true
fi
systemctl restart openmediavault-engined || true
fi
# 7) Final check: can OMV read mountpoints now?
if command -v omv-confdbadm >/dev/null 2>&1; then
echo "=== Sanity: confdb mountpoints ==="
omv-confdbadm read conf.system.filesystem.mountpoint 2>/dev/null \
| jq -r '.[] | "\(.type): \(.fsname) -> \(.dir) [uuid=\(.uuid)]"' || true
fi
echo "Done."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment