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>
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
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
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))
--- /dev/null
+// 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));
+ }
+};
}
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);
}
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);
}
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");
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);
}
};
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;
--- /dev/null
+#!/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));