summaryrefslogtreecommitdiffstats
path: root/net/nut/files/nut-server-service.sh.functions
blob: eed71d1085a01cab63940c745f784bc6a0ab05ca (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
#!/bin/sh
# Shebang line included so editors and shellcheck/shfmt know this contains
# shell code

# Helper functions for NUT server service handling

# In recent (relevant) versions of shellcheck busybox is a valid shell type
# shellcheck shell=busybox

# config_load is done by the sourcing script, before executing any functions in
# this file

# Pre-requisite sourcing for functions this script uses
# * /lib/functions.sh has been sourced
# * /lib/functions/nut/nut-common.sh has been sourced

# find_runas to find the RUNAS user should already have been
# run by nut-server initscript prior to calling
# ensure_usb_ups_access.

# Ensure the RUNAS user has access to the USB UPS device
ensure_usb_ups_access() {
	local ups="$1"
	local known_devname="$2"
	local cfg_vendorid
	local cfg_productid
	local device_owner
	local device_group
	local usb_bus
	local usb_dev
	local device_path
	local cur_devname
	local device_serial
	local sysfs_vendorid
	local sysfs_productid
	local serial
	# When no USB bus or USB device is found, the value returned in each case,
	# after printf,	will be '000', which is not a valid USB bus or USB device
	# number (i.e. it is a missing data indicator).
	local missing_usb_num="000"

	device_owner="$RUNAS"
	[ -n "$RUNAS" ] || {
		log_error "No RUNAS user specified. Skipping ups '$ups'" nut-server-service.sh nut-server-service
		return 1
	}

	device_group="$(id -gn "$RUNAS")" || {
		log_error "Unable to find group for RUNAS='$RUNAS'. Skipping ups '$ups'" nut-server-service.sh nut-server-service
		return 1
	}

	config_get cfg_vendorid "$ups" vendorid
	# replacement in variable parameter expansion is supported by ash in OpenWrt
	# since at least OpenWrt-24.10 (the previous stable release, which two
	# versions older than the targetted next release of OpenWrt (after
	# OpenWrt-25.12). This has been verified by the author of this script).
	if [ -n "$cfg_vendorid" ] && ! check_valid_hex_number "${cfg_vendorid/0x/}"; then
		log_error "Configured vendorid for '$ups' is not valid" nut-server-service.sh nut-server-service
		return 1
	fi

	# Configuration might not have a vendorid. This is allowed, but we have
	# nothing to do here in that case.
	[ -n "$cfg_vendorid" ] || return 0

	case "$cfg_vendorid" in
	0x*)
		cfg_vendorid="$(printf "%04x" "$cfg_vendorid")"
		;;
	*)
		cfg_vendorid="$(printf "%04x" "0x$cfg_vendorid")"
		;;
	esac

	config_get cfg_productid "$ups" productid
	if [ -n "$cfg_productid" ] && ! check_valid_hex_number "${cfg_productid/0x/}"; then
		log_error "Configured productid for '$ups' is not valid" nut-server-service.sh nut-server-service
		return 1
	fi

	# Configuration might not have a productid. This is allowed, but we have
	# nothing to do here in that case.
	[ -n "$cfg_productid" ] || return 0

	case "$cfg_productid" in
	0x*)
		cfg_productid="$(printf "%04x" "$cfg_productid")"
		;;
	*)
		cfg_productid="$(printf "%04x" "0x$cfg_productid")"
		;;
	esac

	config_get serial "$ups" serial

	# $'\x' syntax is supported by ash in OpenWrt since at least OpenWrt-24.10
	# (the previous stable release, which two versions older than the targetted
	# next release of OpenWrt (after OpenWrt-25.12). This has been verified by
	# the author of this script).
	# shellcheck disable=SC3003
	local NL=$'\n'

	# Loop on the idVendor of all USB devices attached to the system
	# We do not need any return values from this loop, which exists solely for the side-effects of
	# setting the appropriate device permissions and group, so subshell is okay.
	# IFS is set to newline *only*. Default IFS also includes tab and space
	# Disable globbing. `find` will still use patterns.
	set -f
	find /sys/devices -name idVendor -a -path '*/usb[0-9]*/*' | while IFS="$NL" read -r vendor_path; do
		set +f
		usb_bus=""
		usb_dev=""
		device_path=""
		cur_devname=""
		device_serial=""
		# Filter by vendor ID first

		sysfs_vendorid="$(cat "$vendor_path")"
		if [ -z "$sysfs_vendorid" ]; then
			log_error "Could not read vendor ID from $vendor_path" nut-server-service.sh nut-server-service
			continue
		fi

		if [ "$sysfs_vendorid" != "$cfg_vendorid" ]; then
			continue
		fi

		device_path="$(dirname "$vendor_path")"

		# Then filter by product ID

		sysfs_productid="$(cat "$device_path/idProduct")"

		if [ -z "$sysfs_productid" ]; then
			log_error "Could not read product ID from $device_path" nut-server-service.sh nut-server-service
			continue
		fi
		if [ "$sysfs_productid" != "$cfg_productid" ]; then
			continue
		fi

		# Next filter by serial (number), if provided

		# guard against disappearing device_path (e.g. due to 'bouncing' device connection)
		if [ ! -e "$device_path" ]; then
			continue
		fi

		# Only check against device serial number if a serial number is
		# in the configuration for this UPS
		if [ -n "$serial" ]; then
			device_serial="$(cat "$device_path/serial" 2>/dev/null)"

			if [ "$serial" != "$device_serial" ]; then
				continue
			fi
		fi

		usb_bus="$(printf "%03d" "$(cat "$device_path/busnum")")"
		usb_dev="$(printf "%03d" "$(cat "$device_path/devnum")")"

		# usb_bus and usb_dev must each be at least 001
		# a missing value will be present as 000 due to 'printf "%03d"'
		if [ "$usb_bus" = "$missing_usb_num" ] || [ "$usb_dev" = "$missing_usb_num" ]; then
			# If this sysfs device is not valid, keep looking (and ignore this
			# sysfs device).
			continue
		else
			# If we are called from hotplug, we know the device name, so skip any other devices
			cur_devname="/dev/bus/usb/$usb_bus/$usb_dev"
			if [ -n "$known_devname" ] && [ "$known_devname" != "$cur_devname" ]; then
				continue
			fi

			# guard against disappearing cur_devname (e.g. due to 'bouncing' device connection)
			if [ -e "$cur_devname" ]; then
				chmod 0660 "$cur_devname" || {
					log_error "'chmod' failed for $cur_devname (ups '$ups')" nut-server-service.sh nut-server-service
					continue
				}
				# Re-check in case device disappeared after chmod
				if [ -e "$cur_devname" ]; then
					chown "${device_owner}:${device_group}" "$cur_devname" || {
						log_error "'chown' failed for $cur_devname (ups '$ups')" nut-server-service.sh nut-server-service
						continue
					}
				fi
			else
				continue
			fi

			# Serial numbers are defined as unique, so do not loop further (on /sys/devices - while),
			# if serial was present and matched
			if [ -n "$serial" ]; then
				# Break out of while loop (so we do not process more 'find' results)
				break
			fi
			# If a serial number is not provided we need all vendor:product matches
			# to have permissions for NUT as we do not know the method used
			# to match UPS device to a configuration, in this script
			# (but if we have a known_devname, limit to that device)
		fi
		set -f
	done
	set +f
}