]> git.99rst.org Git - openwrt-packages.git/commitdiff
uvol: update to 1.2, expose volume operations over ubus
authorDaniel Golle <redacted>
Mon, 24 Aug 2026 02:06:28 +0000 (03:06 +0100)
committerDaniel Golle <redacted>
Mon, 24 Aug 2026 16:54:41 +0000 (17:54 +0100)
Move the storage backend probe, the ctx construction, the locking
helpers and the volume-name check out of the CLI script into
/usr/lib/uvol/common.uc, so a second entry point can reuse them
without duplicating the logic.

Publish ubus object 'uvol' with the volume operations and a readiness
query. Consumers such as uxc are pure ubus frontends and must not exec
the command line tool. Ship an rpcd exec plugin, installed by the uvol
package itself, taking the same locks as the CLI so both entry points
stay serialised. As stdout is the plugin's reply channel, point file
descriptor 1 at stderr for the duration of a call.

Send a 'uvol.ready' ubus event at the end of 'uvol boot', carrying the
active backend name and whether the .meta volume is ready. Consumers
such as uxc can wait for this event instead of polling volume state.
Sending is best-effort: boot keeps its exit code even when ubusd is
not reachable.

Signed-off-by: Daniel Golle <redacted>
utils/uvol/Makefile
utils/uvol/files/common.uc [new file with mode: 0644]
utils/uvol/files/uvol
utils/uvol/files/uvol-rpcd [new file with mode: 0644]

index 02bf997c0eda43c2cb6e6ed320920257b5953001..eb7c6da9b2f7b61f48d402f632fe3a84ee049740 100644 (file)
@@ -1,7 +1,7 @@
 include $(TOPDIR)/rules.mk
 
 PKG_NAME:=uvol
-PKG_VERSION:=1.1.1
+PKG_VERSION:=1.2
 PKG_RELEASE:=1
 
 PKG_MAINTAINER:=Daniel Golle <daniel@makrotopia.org>
@@ -27,7 +27,7 @@ define Package/uvol
   CATEGORY:=Utilities
   SUBMENU:=Disc
   TITLE:=OpenWrt UBI/LVM volume abstraction
-  DEPENDS:=+blockd +ucode +ucode-mod-fs +ucode-mod-math +ucode-mod-uci +ucode-mod-ubus
+  DEPENDS:=+blockd +ucode +ucode-mod-fs +ucode-mod-math +ucode-mod-uci +ucode-mod-ubus +rpcd
   PKGARCH=all
 endef
 
@@ -41,6 +41,10 @@ define Package/uvol/description
   Also install the 'autopart' package to easily make use of 'uvol' on
   block-storage based devices.
 
+  Volume operations are also exposed as ubus object 'uvol' through an
+  rpcd exec plugin, using the same backends and locks as the command
+  line tool.
+
   Examples:
   uvol create example_volume_1 268435456 rw
   uvol up example_volume_1
@@ -67,14 +71,21 @@ define Package/autopart/install
 endef
 
 define Package/uvol/install
-       $(INSTALL_DIR) $(1)/etc/init.d $(1)/usr/lib/uvol/backends $(1)/usr/sbin
+       $(INSTALL_DIR) $(1)/etc/init.d $(1)/usr/lib/uvol/backends $(1)/usr/sbin $(1)/usr/libexec/rpcd
        $(INSTALL_BIN) ./files/uvol.init $(1)/etc/init.d/uvol
        $(INSTALL_DATA) ./files/blockdev_common.uc $(1)/usr/lib/uvol/
+       $(INSTALL_DATA) ./files/common.uc $(1)/usr/lib/uvol/
        $(INSTALL_DATA) ./files/mount.uc $(1)/usr/lib/uvol/
        $(INSTALL_DATA) ./files/lvm.uc $(1)/usr/lib/uvol/backends/
        $(INSTALL_DATA) ./files/ubi.uc $(1)/usr/lib/uvol/backends/
        $(INSTALL_BIN) ./files/uvol $(1)/usr/sbin
        $(SED) 's/@PKG_VERSION@/$(PKG_VERSION)/g' $(1)/usr/sbin/uvol
+       $(INSTALL_BIN) ./files/uvol-rpcd $(1)/usr/libexec/rpcd/uvol
+endef
+
+define Package/uvol/postinst
+#!/bin/sh
+[ -n "$$IPKG_INSTROOT" ] || /etc/init.d/rpcd reload
 endef
 
 $(eval $(call BuildPackage,autopart))
diff --git a/utils/uvol/files/common.uc b/utils/uvol/files/common.uc
new file mode 100644 (file)
index 0000000..22fada0
--- /dev/null
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+// shared backend selection and locking for uvol entry points
+//  (c) 2022 Daniel Golle <daniel@makrotopia.org>
+
+let common_fs = require("fs");
+let common_uci = require("uci");
+
+include("/usr/lib/uvol/mount.uc");
+
+let shell_quote = function(word) {
+       return "'" + replace(`${word}`, /'/g, "'\\''") + "'";
+};
+
+// volumes are taken down whether or not anything mounted them, so umount is
+// expected to fail and stays quiet. That needs a shell, hence the quoting.
+let umount_dev = function(dev) {
+       return system(sprintf("umount %s 2>/dev/null", shell_quote(sprintf("/dev/%s", dev))));
+};
+
+let lock_open = function(path) {
+       common_fs.mkdir("/tmp/run", 0755);
+       let lockfd = common_fs.open(path, "a");
+       if (lockfd)
+               lockfd.lock("x");
+       return lockfd;
+};
+
+uvol_common = {
+       // volume names reach tool arguments and lock file paths; anything outside
+       // this set is refused before it gets there. A leading '.' is reserved for
+       // internal volumes such as .meta, and a leading '-' would be taken for an
+       // option by the backend tools.
+       name_valid: function(name, allow_internal) {
+               if (type(name) != "string")
+                       return false;
+               if (allow_internal && name == ".meta")
+                       return true;
+               return !!match(name, /^[A-Za-z0-9_][A-Za-z0-9._-]*$/);
+       },
+
+       shell_quote: shell_quote,
+
+       ctx_init: function() {
+               let ctx = {};
+               ctx.cursor = common_uci ? common_uci.cursor() : null;
+               ctx.fs = common_fs;
+               ctx.register = uvol_mount.register;
+               ctx.unregister = uvol_mount.unregister;
+               ctx.shell_quote = shell_quote;
+               ctx.umount_dev = umount_dev;
+               return ctx;
+       },
+
+       backend_select: function(ctx) {
+               let backend = null;
+               let tried = [];
+               for (let plugin in common_fs.glob("/usr/lib/uvol/backends/*.uc")) {
+                       let current_backend = {};
+                       include(plugin, { backend: current_backend });
+                       push(tried, current_backend.backend);
+                       if (type(backend) == "object" &&
+                           type(backend.priority) == "int" &&
+                           type(current_backend.priority) == "int" &&
+                           backend.priority > current_backend.priority)
+                               continue;
+                       if (type(current_backend.init) == "function" &&
+                           current_backend.init(ctx)) {
+                               backend = current_backend;
+                               break;
+                       }
+               }
+               return { backend: backend, tried: tried };
+       },
+
+       // released on process exit; the caller keeps the returned fd alive
+       lock_device: function() {
+               return lock_open("/tmp/run/uvol.lock");
+       },
+
+       lock_volume: function(vol_name) {
+               return lock_open(sprintf("/tmp/run/uvol.lock.%s", vol_name));
+       }
+};
index 44a7f141a404432dd76a4d0ad5cbab03b8608934..1dd6eb17235b1147daa32f736979f484dca93897 100644 (file)
@@ -44,60 +44,14 @@ if (!cmd || cmd == "-h" || cmd == "--help" || cmd == "help") {
 }
 
 let fs = require("fs");
-let uci = require("uci");
-let cursor = uci ? uci.cursor() : null;
+include("/usr/lib/uvol/common.uc");
 
-let shell_quote = function(word) {
-       return "'" + replace(`${word}`, /'/g, "'\\''") + "'";
-};
-
-// volumes are taken down whether or not anything mounted them, so umount is
-// expected to fail and stays quiet. That needs a shell, hence the quoting.
-let umount_dev = function(dev) {
-       return system(sprintf("umount %s 2>/dev/null", shell_quote(sprintf("/dev/%s", dev))));
-};
-
-// volume names reach tool arguments and lock file paths; anything outside
-// this set is refused before it gets there. A leading '.' is reserved for
-// internal volumes such as .meta, and a leading '-' would be taken for an
-// option by the backend tools.
-let name_valid = function(name, allow_internal) {
-       if (type(name) != "string")
-               return false;
-       if (allow_internal && name == ".meta")
-               return true;
-       return !!match(name, /^[A-Za-z0-9_][A-Za-z0-9._-]*$/);
-};
-
-let ctx = {};
-ctx.cursor = cursor;
-ctx.fs = fs;
-include("/usr/lib/uvol/mount.uc");
-ctx.register = uvol_mount.register;
-ctx.unregister = uvol_mount.unregister;
-ctx.shell_quote = shell_quote;
-ctx.umount_dev = umount_dev;
-
-let backend = null;
-let tried_backends = [];
-for (plugin in fs.glob("/usr/lib/uvol/backends/*.uc")) {
-       let current_backend = {};
-       include(plugin, { backend: current_backend });
-       push(tried_backends, current_backend.backend);
-       if (type(backend) == "object" &&
-           type(backend.priority) == "int" &&
-           type(current_backend.priority) == "int" &&
-           backend.priority > current_backend.priority)
-               continue;
-       if (type(current_backend.init) == "function" &&
-           current_backend.init(ctx)) {
-               backend = current_backend;
-               break;
-       }
-}
+let ctx = uvol_common.ctx_init();
+let selection = uvol_common.backend_select(ctx);
+let backend = selection.backend;
 
 if (!backend) {
-       printf("No backend available. (tried: %s)\n", join(" ", tried_backends));
+       printf("No backend available. (tried: %s)\n", join(" ", selection.tried));
        printf("To setup devices with block storage install 'autopart'.\n");
        exit(2);
 }
@@ -119,7 +73,7 @@ let ro_name_cmds = [ "status", "device", "size", "list" ];
 let rw_name_cmds = [ "create", "remove", "up", "down", "resize", "write" ];
 let ro_name = index(ro_name_cmds, cmd) >= 0;
 if (index(rw_name_cmds, cmd) >= 0 || (ro_name && ARGV[0] != null)) {
-       if (!name_valid(ARGV[0], ro_name)) {
+       if (!uvol_common.name_valid(ARGV[0], ro_name)) {
                printf("invalid volume name\n");
                exit(22);
        }
@@ -135,18 +89,10 @@ let uvol_mutating = [ "boot", "detect", "create", "up", "down", "remove", "reap"
 let uvol_vol_mutating = [ "create", "write" ];
 let uvol_lockfd;
 let uvol_vol_lockfd;
-if (index(uvol_mutating, cmd) >= 0 || index(uvol_vol_mutating, cmd) >= 0)
-       fs.mkdir("/tmp/run", 0755);
-if (index(uvol_mutating, cmd) >= 0) {
-       uvol_lockfd = fs.open("/tmp/run/uvol.lock", "a");
-       if (uvol_lockfd)
-               uvol_lockfd.lock("x");
-}
-if (index(uvol_vol_mutating, cmd) >= 0 && ARGV[0]) {
-       uvol_vol_lockfd = fs.open(sprintf("/tmp/run/uvol.lock.%s", ARGV[0]), "a");
-       if (uvol_vol_lockfd)
-               uvol_vol_lockfd.lock("x");
-}
+if (index(uvol_mutating, cmd) >= 0)
+       uvol_lockfd = uvol_common.lock_device();
+if (index(uvol_vol_mutating, cmd) >= 0 && ARGV[0])
+       uvol_vol_lockfd = uvol_common.lock_volume(ARGV[0]);
 
 let meta_init = function() {
        let sz = backend.size(".meta");
@@ -166,6 +112,14 @@ let meta_init = function() {
 if (cmd == "boot") {
        backend.boot();
        meta_init();
+       // the event is best-effort; boot must not fail when ubusd is unavailable
+       try {
+               require("ubus").event("uvol.ready", {
+                       backend: backend.backend,
+                       meta: backend.status(".meta") == 0,
+               });
+       } catch(e) {
+       }
        exit(0);
 }
 
@@ -204,7 +158,7 @@ let ca_parse = function(name) {
 };
 
 let ca_digest = function(cmd, size, path) {
-       let src = path ? sprintf("head -c %d %s", size, shell_quote(path)) : sprintf("head -c %d", size);
+       let src = path ? sprintf("head -c %d %s", size, uvol_common.shell_quote(path)) : sprintf("head -c %d", size);
        let f = fs.popen(sprintf("%s | %s", src, cmd), "r");
        if (!f)
                return null;
diff --git a/utils/uvol/files/uvol-rpcd b/utils/uvol/files/uvol-rpcd
new file mode 100644 (file)
index 0000000..105e8d2
--- /dev/null
@@ -0,0 +1,153 @@
+#!/usr/bin/ucode -R
+// SPDX-License-Identifier: GPL-2.0-or-later
+// rpcd exec plugin exposing uvol volume operations over ubus
+//  (c) 2026 Daniel Golle <daniel@makrotopia.org>
+
+let fs = require("fs");
+
+let methods = {
+       ready:  { args: {} },
+       free:   { args: {} },
+       total:  { args: {} },
+       align:  { args: {} },
+       list:   { args: { name: "volname" }, ro: true },
+       status: { args: { name: "volname" }, need_name: true, ro: true },
+       up:     { args: { name: "volname" }, need_name: true, lock: true },
+       down:   { args: { name: "volname" }, need_name: true, lock: true },
+       remove: { args: { name: "volname" }, need_name: true, lock: true },
+       create: { args: { name: "volname", size: 64, mode: "rw" },
+                 need_name: true, need_size: true, lock: true, vol_lock: true },
+       resize: { args: { name: "volname", size: 64 },
+                 need_name: true, need_size: true, lock: true },
+};
+
+// rpcd runs '<plugin> list' once at startup; the method table must not
+// depend on backend availability or the object vanishes until reboot
+if (ARGV[0] == "list") {
+       let signature = {};
+       for (let name, m in methods)
+               signature[name] = m.args;
+       print(signature);
+       exit(0);
+}
+
+let method = (ARGV[0] == "call") ? methods[ARGV[1]] : null;
+if (!method)
+       exit(22);
+
+let cmd = ARGV[1];
+
+// stdout is the JSON reply channel; keep a copy and point fd 1 at stderr
+// so mkfs and lvm output from the backends cannot corrupt the reply
+fs.dup2(1, 3);
+fs.dup2(2, 1);
+let reply_out = fs.fdopen(3, "w");
+
+include("/usr/lib/uvol/common.uc");
+
+let error_text = {
+       "2": "no such volume",
+       "16": "write in progress",
+       "17": "volume already exists",
+       "22": "invalid argument",
+       "27": "payload larger than volume",
+       "74": "content verification failed",
+       "95": "operation not supported",
+};
+
+let status_states = {
+       "0": "ready",
+       "1": "down",
+       "2": "nonexistent",
+       "16": "write-pending",
+       "22": "write-only",
+};
+
+// rpcd ignores the exit code and requires a JSON object on stdout
+function reply(data) {
+       reply_out.write(sprintf("%J", data));
+       reply_out.flush();
+       exit(0);
+}
+
+function reply_code(code) {
+       if (code == 0)
+               reply({ code: 0 });
+       reply({ code: code, error: error_text[code] ?? "operation failed" });
+}
+
+let args = {};
+try {
+       args = json(fs.stdin.read("all"));
+} catch(e) {
+       args = {};
+}
+if (type(args) != "object")
+       args = {};
+
+let name = args.name;
+if (name != null && !uvol_common.name_valid(name, method.ro))
+       reply_code(22);
+if (method.need_name && !name)
+       reply_code(22);
+
+let size;
+if (method.need_size) {
+       if (type(args.size) != "int" && type(args.size) != "string")
+               reply_code(22);
+       size = +args.size;
+       if (size != size || size <= 0)
+               reply_code(22);
+}
+
+if (cmd == "create" && args.mode != "ro" && args.mode != "rw")
+       reply_code(22);
+
+let selection = uvol_common.backend_select(uvol_common.ctx_init());
+let backend = selection.backend;
+
+if (cmd == "ready") {
+       let res = { ready: backend != null, meta: false };
+       if (backend) {
+               res.backend = backend.backend;
+               res.meta = backend.status(".meta") == 0;
+       }
+       reply(res);
+}
+
+if (!backend)
+       reply({ code: 2, error: "no backend available", tried: selection.tried });
+
+let uvol_lockfd;
+let uvol_vol_lockfd;
+if (method.lock)
+       uvol_lockfd = uvol_common.lock_device();
+if (method.vol_lock)
+       uvol_vol_lockfd = uvol_common.lock_volume(name);
+
+if (cmd == "status") {
+       let code = backend.status(name);
+       reply({ code: code, state: status_states[code] ?? "unknown" });
+}
+
+if (cmd == "list") {
+       let volumes = backend.list(name);
+       for (let vol in volumes)
+               vol.size = +vol.size;
+       reply({ volumes: volumes });
+}
+
+if (cmd == "free" || cmd == "total" || cmd == "align") {
+       let res = backend[cmd]();
+       if (type(res) == "int")
+               reply({ code: res, error: "capacity unavailable" });
+       reply({ bytes: +res });
+}
+
+if (cmd == "create")
+       reply_code(backend.create(name, size, args.mode));
+
+if (cmd == "resize")
+       reply_code(backend.resize(name, size));
+
+reply_code(backend[cmd](name));
git clone https://git.99rst.org/PROJECT