From: Daniel Golle Date: Mon, 24 Aug 2026 02:06:28 +0000 (+0100) Subject: uvol: update to 1.2, expose volume operations over ubus X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=267a9c889256724ab8f9de81df5a306970e2555c;p=openwrt-packages.git uvol: update to 1.2, expose volume operations over ubus 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 --- diff --git a/utils/uvol/Makefile b/utils/uvol/Makefile index 02bf997c0..eb7c6da9b 100644 --- a/utils/uvol/Makefile +++ b/utils/uvol/Makefile @@ -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 @@ -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 index 000000000..22fada077 --- /dev/null +++ b/utils/uvol/files/common.uc @@ -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 + +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)); + } +}; diff --git a/utils/uvol/files/uvol b/utils/uvol/files/uvol index 44a7f141a..1dd6eb172 100644 --- a/utils/uvol/files/uvol +++ b/utils/uvol/files/uvol @@ -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 index 000000000..105e8d23b --- /dev/null +++ b/utils/uvol/files/uvol-rpcd @@ -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 + +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 ' 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));