Skip to content

Instantly share code, notes, and snippets.

@ergosteur
Created November 12, 2025 03:07
Show Gist options
  • Select an option

  • Save ergosteur/a1bee4dcb2c81771ed77270cd65cbf7b to your computer and use it in GitHub Desktop.

Select an option

Save ergosteur/a1bee4dcb2c81771ed77270cd65cbf7b to your computer and use it in GitHub Desktop.
script to fix USB permissions for NUT (Network UPS Tools)
#!/bin/bash
# fix-ups-permissions.sh
# Standalone script to fix USB permissions for NUT (Network UPS Tools)
set -e
if [ "$EUID" -ne 0 ]; then
echo "ERROR: Please run as root or with sudo." >&2
exit 1
fi
# --- Functions ---
select_ups_device() {
echo "Scanning for USB devices..." >&2
mapfile -t devices < <(lsusb)
if [ ${#devices[@]} -eq 0 ]; then
echo "No USB devices found." >&2
return 1
fi
echo "" >&2
echo "Select your UPS device:" >&2
local i=1
for device in "${devices[@]}"; do
echo "$i) $device" >&2
((i++))
done
echo "$i) Quit" >&2
read -p "Enter number: " choice
if [[ "$choice" =~ ^[Qq]$ ]] || [ "$choice" -eq "$i" ]; then
return 1
fi
if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt "${#devices[@]}" ]; then
echo "Invalid choice." >&2
return 1
fi
local selected=${devices[$((choice - 1))]}
if [[ "$selected" =~ ID[[:space:]]*([0-9a-fA-F]{4}):([0-9a-fA-F]{4}) ]]; then
echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"
else
echo "Failed to parse Vendor/Product ID." >&2
return 1
fi
}
create_udev_rule() {
local idVendor=$1 idProduct=$2
local rule_file="/etc/udev/rules.d/50-nut-ups.rules"
# Two complementary lines: one for HID devices, one for plain USB devices
local rule_hid="SUBSYSTEM==\"hidraw\", ATTRS{idVendor}==\"$idVendor\", ATTRS{idProduct}==\"$idProduct\", MODE=\"0660\", GROUP=\"nut\""
local rule_usb="SUBSYSTEM==\"usb\", ATTR{idVendor}==\"$idVendor\", ATTR{idProduct}==\"$idProduct\", MODE=\"0660\", GROUP=\"nut\""
echo "Adding or verifying rules in $rule_file..."
# Ensure file exists and each line appears exactly once
touch "$rule_file"
grep -qF "$rule_hid" "$rule_file" || echo "$rule_hid" >> "$rule_file"
grep -qF "$rule_usb" "$rule_file" || echo "$rule_usb" >> "$rule_file"
echo "Reloading udev..."
udevadm control --reload-rules
udevadm trigger
udevadm settle
}
verify_permissions() {
local idVendor=$1 idProduct=$2
echo "Verifying device permissions..."
local idVendorUpper=$(echo "$idVendor" | tr '[:lower:]' '[:upper:]')
local idProductUpper=$(echo "$idProduct" | tr '[:lower:]' '[:upper:]')
local pattern=":$idVendorUpper:$idProductUpper"
local found_dev=""
for symlink in /sys/class/hidraw/hidraw*; do
if [ -L "$symlink" ] && [[ "$(readlink "$symlink")" == *"$pattern"* ]]; then
found_dev="/dev/$(basename "$symlink")"
break
fi
done
# Fallback: check /dev/bus/usb
if [ -z "$found_dev" ]; then
local busdev
busdev=$(lsusb -d "$idVendor:$idProduct" | awk '{printf "/dev/bus/usb/%03d/%03d", $2, $4}' | tr -d ':')
[ -e "$busdev" ] && found_dev="$busdev"
fi
if [ -z "$found_dev" ]; then
echo "⚠️ Device not found in hidraw or /dev/bus/usb. Try replugging it."
return 1
fi
echo "✅ Found device node: $found_dev"
echo ""
echo " $(ls -l "$found_dev")"
echo ""
local perms=$(stat -c "%a" "$found_dev")
local group=$(stat -c "%G" "$found_dev")
echo " Current permissions: $perms | Group: $group"
# Split out the numeric comparison (stat always gives decimal perms)
if [ "$group" != "nut" ]; then
echo "⚠️ Group is '$group' but expected 'nut'. You may need to replug or reload udev."
return
fi
if (( perms < 660 )); then
echo "❌ Permissions too restrictive (expected at least 660)."
echo " Try replugging the UPS or reloading udev rules."
elif (( perms > 660 )); then
echo "✅ Permissions are at least 660 — access should be fine."
echo " (Note: currently world-readable, which is more permissive than necessary.)"
else
echo "✅ Permissions exactly 660 — perfect for NUT."
fi
echo ""
echo "You can now restart the NUT driver if needed:"
echo " sudo systemctl restart nut-driver.target"
}
# Fix or clean up /etc/nut/ups.conf bus/device mappings
fix_ups_conf_device_mapping() {
local UPS_CONF="/etc/nut/ups.conf"
local idVendor="$1"
local idProduct="$2"
echo "Checking $UPS_CONF for outdated or fragile bus/device mapping..."
# Ensure config exists
if [ ! -f "$UPS_CONF" ]; then
echo "⚠️ No $UPS_CONF found — skipping."
return
fi
# Always remove any busport line — it's not needed
if grep -qE '^\s*busport\s*=' "$UPS_CONF"; then
echo "🧹 Removing obsolete 'busport' line..."
sed -i '/^\s*busport\s*=/d' "$UPS_CONF"
fi
# Check if bus/device lines exist
local has_bus has_dev
has_bus=$(grep -qE '^\s*bus\s*=' "$UPS_CONF" && echo 1 || echo 0)
has_dev=$(grep -qE '^\s*device\s*=' "$UPS_CONF" && echo 1 || echo 0)
if [ "$has_bus" -eq 0 ] && [ "$has_dev" -eq 0 ]; then
echo "✔ No fixed bus/device entries found — already portable."
return
fi
# Find the actual current device path for the given vendor/product
local actual_path
actual_path=$(lsusb | grep -i "${idVendor}:${idProduct}" | awk '{print "/dev/bus/usb/" $2 "/" $4}' | sed 's/://')
if [ -z "$actual_path" ]; then
echo "⚠️ Could not locate UPS via lsusb (maybe unplugged)."
return
fi
local actual_bus actual_dev
actual_bus=$(echo "$actual_path" | awk -F'/' '{print $(NF-1)}')
actual_dev=$(echo "$actual_path" | awk -F'/' '{print $NF}')
echo ""
echo "Detected current UPS location: bus=$actual_bus, device=$actual_dev"
echo ""
echo "Choose how to handle 'bus' and 'device' lines in ups.conf:"
echo " 1) Keep and update them (bus=$actual_bus, device=$actual_dev)"
echo " 2) Remove them entirely (recommended for stability)"
echo -n "Enter choice [1/2, default=2]: "
read -r choice
choice=${choice:-2}
if [ "$choice" == "1" ]; then
echo "🔧 Updating 'bus' and 'device' lines..."
if grep -qE '^\s*bus\s*=' "$UPS_CONF"; then
sed -i "s/^\s*bus\s*=.*/ bus = \"$actual_bus\"/" "$UPS_CONF"
else
echo " bus = \"$actual_bus\"" >> "$UPS_CONF"
fi
if grep -qE '^\s*device\s*=' "$UPS_CONF"; then
sed -i "s/^\s*device\s*=.*/ device = \"$actual_dev\"/" "$UPS_CONF"
else
echo " device = \"$actual_dev\"" >> "$UPS_CONF"
fi
echo "✔ ups.conf updated with new bus/device values."
else
echo "🧹 Removing static 'bus' and 'device' entries..."
sed -i '/^\s*bus\s*=/d' "$UPS_CONF"
sed -i '/^\s*device\s*=/d' "$UPS_CONF"
echo "✔ ups.conf cleaned up — NUT will now auto-detect via vendor/product."
fi
}
# --- Main ---
echo "=== NUT USB Permission Fixer ==="
read -r idVendor idProduct < <(select_ups_device)
if [ -z "$idVendor" ] || [ -z "$idProduct" ]; then
echo "No device selected or unable to parse device IDs. Exiting." >&2
exit 1
fi
echo "Selected: Vendor=$idVendor Product=$idProduct"
create_udev_rule "$idVendor" "$idProduct"
verify_permissions "$idVendor" "$idProduct"
fix_ups_conf_device_mapping "$idVendor" "$idProduct"
echo ""
echo "Done. You may now restart the NUT driver:"
echo " sudo systemctl restart nut-driver.target"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment