Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a simple PoC for NUT readings seen in FUSE file system #2592

Merged
merged 3 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NEWS.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,9 @@ during a NUT build.
migrated the builders to Apple Silicon from x86 (deprecated by CircleCI).
[#2502]

- Introduced a simple experiment to expose NUT client readings as filesystem
objects via FUSE, in `scripts/fuse/execfuse-nut` now. [#2591]


Release notes for NUT 2.8.2 - what's new since 2.8.1
----------------------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion docs/nut.dict
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
personal_ws-1.1 en 3208 utf-8
personal_ws-1.1 en 3209 utf-8
AAC
AAS
ABI
Expand Down Expand Up @@ -1872,6 +1872,7 @@ everyone's
everything's
evilhack
exe
execfuse
executables
executeCommand
execve
Expand Down
99 changes: 99 additions & 0 deletions scripts/fuse/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
NUT access via FUSE
===================

After a discussion with friends about the
link:https://en.wikipedia.org/wiki/Filesystem_in_Userspace[FUSE] technology
an idea resurfaced, to have NUT readings available as filesystem objects.

Previously this was vaguely envisioned as a sort of `/dev/shm` on Linux or
equivalent on other OSes (where SHM is available with a filesystem API).
But FUSE being more portable, it is actually a better bet for this approach.

* https://github.com/networkupstools/nut/issues/2591
NUT access via execfuse
~~~~~~~~~~~~~~~~~~~~~~~

For a proof-of-concept, the link:https://github.com/vi/execfuse[execfuse]
seemed appropriate. It is a daemon which knows to call user-provided programs
to handle certain filesystem API requests. Essentially, shell scripts exposed
as a file system!

Sure, it is not very efficient or perhaps even secure, but for quick and dirty
experiments it proved to be great! Relatively easy to debug the new code by
saving log files, and quick to iterate by editing the shell scripts (no need
to rebuild and restart a daemon).

Installation:

----
:; git clone https://github.com/vi/execfuse
:; cd execfuse
:; make
----

That's about it, an `execfuse` binary should appear.

To run with the PoC scripts here, assuming a built NUT in `~/nut` location:

----
:; mkdir /tmp/nut-fuse
:; PATH="$HOME/nut/clients:$PATH" \
./execfuse "$HOME/nut/scripts/fuse/execfuse-nut" /tmp/nut-fuse
----

Then as you walk the `/tmp/nut-fuse` location, you would see a sub-directory
called `by-server` under which a `localhost:3493` is exposed and any token
(even if not visible) can be accessed with the naive assumption that it is
a hostname/IP address and an optional port.

Either way, this token is also treated as a directory, assuming it is a
device name known to the data server `upsd` on that host. In fact, this
directory would be populated by `upsc -l` of that host(:port) automatically.

Each device represented this way is also a "directory", with "files" in it
populated by `upsc` queries to list the currently known variables, and file
contents populated by `upsc` queries for that variable name at that device
on that server.

Example data walk:

----
:; grep -rH /tmp/nut-fuse/localhost/dummy
/tmp/nut-fuse/localhost/dummy/battery.charge:100
/tmp/nut-fuse/localhost/dummy/battery.charge.low:20
/tmp/nut-fuse/localhost/dummy/battery.runtime:1456
/tmp/nut-fuse/localhost/dummy/battery.type:PbAc
...
----

As another experiment, a root-directory pseudo-file was added to reveal
the `upsc` client location and version used in the queries, just for kicks:

----
:; ls -la /tmp/nut-fuse
total 23
drwxr-xr-x 16 root root 4096 Jan 1 1970 .
drwxrwxrwt 154 root root 20480 Aug 13 23:55 ..
drwxr-xr-x 16 root root 4096 Jan 1 1970 by-server
-r--r--r-- 1 root root 0 Jan 1 1970 client-version
:; cat /tmp/nut-fuse/client-version
/home/abuild/nut/clients/upsc
Network UPS Tools upsc 2.8.2.901.2-903-g533aa7eaf (development iteration after 2.8.2)
----

Eventually, to stop this experiment:

----
:; fusermount -u /tmp/nut-fuse
----

As far as a PoC goes, this is already a fun and quickly achieved result,
although for production use something like a FUSE-capable `upsmon`-like
client would be needed both for efficiency (to avoid dozens of process
forks and networked queries to get each reading), and to deal with security
somehow (for a shot at `upsrw` and `upscmd` equivalents).

Hope this helps get the real development going,
Jim Klimov
51 changes: 51 additions & 0 deletions scripts/fuse/execfuse-nut/getattr
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/bin/bash

# Simple PoC of NUT client as a FUSE mountable filesystem
# Requires https://github.com/vi/execfuse
# Copyright (C) 2024 by Jim Klimov <[email protected]>
# Licensed GPLv2+ as NUT codebase

if [ x"$NUT_DEBUG_FUSE" = xtrue ] ; then
exec 2>"/tmp/nut-debug-fuse-`basename $0`.log"
echo "ARGS($#, $0): 1='$1' 2='$2' @='$@'" >&2
fi

D=""
F=""
case "$1" in
/|/by-server)
D="$1"
;;
/by-server/*/*/*/*) # Don't want subdirs here
;;
/by-server/*/*/*)
# VARNAME
F="`basename "$1"`"
# UPSSRV/UPSNAME
D="`echo "$1" | sed 's,^/by-server/\(.*\)/'"$F"'$,\1,'`"
;;
/by-server/*/*)
# UPSSRV/UPSNAME
D="`echo "$1" | sed 's,^/by-server/\(.*\)$,\1,'`"
;;
/by-server/*) # No further slash
# UPSSRV
D="`basename "$1"`"
;;
/client-version)
F="$1"
;;
*)
;;
esac

if [ -n "$F" ] ; then
printf 'ino=1 mode=-r--r--r-- nlink=1 uid=0 gid=0 rdev=0 size=0 blksize=512 blocks=2 atime=0 mtime=0 ctime=0 %s\0' "$1"
else
if [ -n "$D" ] ; then
printf 'ino=1 mode=drwxr-xr-x nlink=16 uid=0 gid=0 rdev=0 size=4096 blksize=512 blocks=2 atime=0 mtime=0 ctime=0 %s\0' "$1"
else
exit 1
fi
fi

1 change: 1 addition & 0 deletions scripts/fuse/execfuse-nut/init
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#!/bin/sh
40 changes: 40 additions & 0 deletions scripts/fuse/execfuse-nut/read_file
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash

# Simple PoC of NUT client as a FUSE mountable filesystem
# Requires https://github.com/vi/execfuse
# Copyright (C) 2024 by Jim Klimov <[email protected]>
# Licensed GPLv2+ as NUT codebase

if [ x"$NUT_DEBUG_FUSE" = xtrue ] ; then
exec 2>"/tmp/nut-debug-fuse-`basename $0`.log"
echo "ARGS($#, $0): 1='$1' 2='$2' @='$@'" >&2
fi

#PATH="~/nut/clients:$PATH"
#export PATH

case "$1" in
/by-server/*/*/*/*) # Don't want subdirs here
;;
/by-server/*/*/*)
VARNAME="`basename "$1"`"
UPS="`echo "$1" | sed 's,^/by-server/\([^/]*\)/\([^/]*\)/'"$VARNAME"'$,\2@\1,'`"
if [ x"$NUT_DEBUG_FUSE" = xtrue ] ; then
#echo "+upsc '$UPS' '$VARNAME'" >&2
set -x
fi
upsc "$UPS" "$VARNAME"
exit $?
;;
/client-version)
if [ x"$NUT_DEBUG_FUSE" = xtrue ] ; then
#echo "+upsc '$UPS' '$VARNAME'" >&2
set -x
fi
command -v upsc
NUT_DEBUG_LEVEL=6 upsc -V
exit $?
;;
esac

exit 1
65 changes: 65 additions & 0 deletions scripts/fuse/execfuse-nut/readdir
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/bin/bash

# Simple PoC of NUT client as a FUSE mountable filesystem
# Requires https://github.com/vi/execfuse
# Copyright (C) 2024 by Jim Klimov <[email protected]>
# Licensed GPLv2+ as NUT codebase

if [ x"$NUT_DEBUG_FUSE" = xtrue ] ; then
exec 2>"/tmp/nut-debug-fuse-`basename $0`.log"
echo "ARGS($#, $0): 1='$1' 2='$2' @='$@'" >&2
#echo "ARGS: $#: $@" >&2
#set | grep -E '^[^ =]*=' >&2
fi

#PATH="~/nut/clients:$PATH"
#export PATH

#exec find "$2$1" -mindepth 1 -maxdepth 1 -printf 'ino=%i mode=%M nlink=%n uid=%U gid=%G rdev=0 size=%s blksize=512 blocks=%b atime=%A@ mtime=%T@ ctime=%C@ %f\0'

# $1 = (parent?) dirname
# $2 = dir basename?

print_dots() {
printf 'ino=1 mode=drwxr-xr-x nlink=4 uid=0 gid=0 rdev=0 size=4096 blksize=512 blocks=2 atime=0 mtime=0 ctime=0 .\0'
printf 'ino=1 mode=drwxr-xr-x nlink=4 uid=0 gid=0 rdev=0 size=1111 blksize=512 blocks=2 atime=0 mtime=0 ctime=0 ..\0'
}

case "$1" in
/)
print_dots
for D in "by-server" ; do
printf 'ino=1 mode=drwxr-xr-x nlink=4 uid=0 gid=0 rdev=0 size=4096 blksize=512 blocks=2 atime=0 mtime=0 ctime=0 %s\0' "$D"
done
for F in "client-version" ; do
printf 'ino=1 mode=-r--r--r-- nlink=1 uid=0 gid=0 rdev=0 size=4096 blksize=512 blocks=2 atime=0 mtime=0 ctime=0 %s\0' "$F"
done
;;
/by-server)
print_dots
printf 'ino=1 mode=drwxr-xr-x nlink=16 uid=0 gid=0 rdev=0 size=16 blksize=512 blocks=1 atime=0 mtime=0 ctime=0 %s\0' "localhost:3493"
;;
/by-server/*/*/*) # Don't want subdirs here
exit 2 # ENOENT
;;
/by-server/*/*) # list device variables
UPSNAME="`basename "$1"`"
UPSSRV="`echo "$1" | sed 's,^/by-server/\(.*\)/'"$UPSNAME"'$,\1,'`"
print_dots
for VARNAME in `upsc "$UPSNAME@$UPSSRV" | awk -F: '{print $1}'` ; do
printf 'ino=1 mode=-r--r--r-- nlink=1 uid=0 gid=0 rdev=0 size=16 blksize=512 blocks=1 atime=0 mtime=0 ctime=0 %s\0' "$VARNAME"
done
;;
/by-server/*) # devices on hostname
UPSSRV="`basename "$1"`"
print_dots
for UPSNAME in `upsc -l "$UPSSRV"` ; do
printf 'ino=1 mode=drwxr-xr-x nlink=16 uid=0 gid=0 rdev=0 size=16 blksize=512 blocks=1 atime=0 mtime=0 ctime=0 %s\0' "$UPSNAME"
done
;;
*)
exit 2 # ENOENT
;;
esac

exit 0
Loading