Created
October 14, 2025 15:02
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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