From: Daniel Golle Date: Sat, 25 Jul 2026 17:46:50 +0000 (+0100) Subject: uvol: bump version to 1.1 X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=cb60600aec1b46b781c3bf72bdef2f4ce95fe9e1;p=openwrt-packages.git uvol: bump version to 1.1 manage volume state over ubus instead of the rootfs Rework the volume lifecycle around blockd's ubus API, so volume state lives entirely on the self-describing LVM/UBI backing store and never leaks into the firmware rootfs. - register active volumes with blockd over ubus (mount.uc) instead of spooling mounts into /etc/config/fstab (uci.uc) - bootstrap .meta from 'uvol boot' and drop the broken 90-uvol-init uci-default - run 'uvol boot' straight from the mount.ready trigger, and query 'ubus call block status' at service start to cover storage already being ready - defer removal of a volume whose backing device is still held, and reap it from the mount.umount trigger once it is free - enforce content-addressed "-" volumes in the dispatcher: reuse an existing volume of the same digest, verify every write - verify a content-addressed write in place while the volume is still incomplete, reporting EBADMSG on mismatch instead of flipping volume state - purge incomplete wo/wp leftovers on boot and reclaim an exactly matching one on create - serialise mutating commands with a device-wide lock, plus a per-volume lock ordered after it so a slow write cannot stall other volumes - add a grow-only 'resize' verb to both backends: lvextend plus the matching fs-grow tool for lvm, ubirsvol for ubi - create ext4 volumes with a journal (was ext2) and request check_fs when registering a read-write volume - declare read-only mounts to blockd rather than letting block guess from the filesystem - reject non-numeric and non-positive sizes in create and resize - report its own version - simplify command line parsing (drop compatibility with ancient ucode) Requires the matching fstools changes from openwrt/openwrt@9b11fa4088c6a. Signed-off-by: Daniel Golle --- diff --git a/utils/uvol/Makefile b/utils/uvol/Makefile index 5c0b3d307..3d0220319 100644 --- a/utils/uvol/Makefile +++ b/utils/uvol/Makefile @@ -1,7 +1,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=uvol -PKG_VERSION:=1.0 +PKG_VERSION:=1.1 PKG_RELEASE:=1 PKG_MAINTAINER:=Daniel Golle @@ -14,7 +14,7 @@ define Package/autopart CATEGORY:=Utilities SUBMENU:=Disc TITLE:=Automatically initialize LVM partition - DEPENDS:=+lvm2 +partx-utils +sfdisk + DEPENDS:=+lvm2 +partx-utils +sfdisk +e2fsprogs +mkf2fs +f2fsck PKGARCH=all endef @@ -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 + DEPENDS:=+blockd +ucode +ucode-mod-fs +ucode-mod-math +ucode-mod-uci +ucode-mod-ubus PKGARCH=all endef @@ -67,14 +67,14 @@ define Package/autopart/install endef define Package/uvol/install - $(INSTALL_DIR) $(1)/etc/init.d $(1)/usr/lib/uvol/backends $(1)/usr/sbin $(1)/etc/uci-defaults + $(INSTALL_DIR) $(1)/etc/init.d $(1)/usr/lib/uvol/backends $(1)/usr/sbin $(INSTALL_BIN) ./files/uvol.init $(1)/etc/init.d/uvol $(INSTALL_DATA) ./files/blockdev_common.uc $(1)/usr/lib/uvol/ - $(INSTALL_DATA) ./files/uci.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 - $(INSTALL_BIN) ./files/uvol.defaults $(1)/etc/uci-defaults/90-uvol-init + $(SED) 's/@PKG_VERSION@/$(PKG_VERSION)/g' $(1)/usr/sbin/uvol endef $(eval $(call BuildPackage,autopart)) diff --git a/utils/uvol/files/lvm.uc b/utils/uvol/files/lvm.uc index c93a50e8e..133daa2d1 100644 --- a/utils/uvol/files/lvm.uc +++ b/utils/uvol/files/lvm.uc @@ -41,8 +41,8 @@ function lvm(cmd, ...args) { } function pvs() { - let fstab = cursor.get_all('fstab'); - for (let k, section in fstab) { + let fstab = cursor ? cursor.get_all('fstab') : null; + for (let k, section in (fstab ?? {})) { if (section['.type'] != 'uvol' || !section.vg_name) continue; @@ -95,6 +95,45 @@ function lvs(vg_name, vol_name, extra_exp) { return ret; } +function lvs_deleting(vg_name) { + let ret = []; + let tmp = lvm("lvs", "-o", "lv_active,lv_name,lv_full_name,lv_dm_path", "-S", + sprintf("\"lvname=~^dd_.* && vg_name=%s\"", vg_name)); + if (tmp && tmp.report.lv) { + ret = tmp.report.lv; + for (let r in ret) + r.lv_active = (r.lv_active == "active"); + } + return ret; +} + +function lvs_incomplete(vg_name, vol_name) { + let ret = []; + let tmp = lvm("lvs", "-o", "lv_active,lv_name,lv_full_name,lv_size", "-S", + sprintf("\"lvname=~^w[op]_%s\$ && vg_name=%s\"", vol_name ?? ".*", vg_name)); + if (tmp && tmp.report.lv) { + ret = tmp.report.lv; + for (let r in ret) { + r.lv_active = (r.lv_active == "active"); + r.lv_size = +(rtrim(r.lv_size, "B")); + } + } + return ret; +} + +// purge incomplete (wo_/wp_) leftovers; with match_size, only those of exactly +// that allocated size (null purges all) +function lvm_purge_incomplete(vol_name, match_size) { + for (let lv in lvs_incomplete(vg_name, vol_name)) { + if (match_size != null && lv.lv_size != match_size) + continue; + if (lv.lv_active) + lvm("lvchange", "-a", "n", lv.lv_full_name); + lvm("lvremove", "-y", lv.lv_full_name); + } + return 0; +} + function getdev(lv) { if (!lv) return null; @@ -123,10 +162,8 @@ function lvm_init(ctx) { return false; vg = vgs(vg_name); - uvol_uci_add = ctx.uci_add; - uvol_uci_commit = ctx.uci_commit; - uvol_uci_remove = ctx.uci_remove; - uvol_uci_init = ctx.uci_init; + register = ctx.register; + unregister = ctx.unregister; return true; } @@ -198,7 +235,11 @@ function lvm_status(vol_name) { return 2; let mode = substr(res[0].lv_name, 0, 2); - if ((mode != "ro" && mode != "rw") || !res[0].lv_active) + if (mode == "wo") + return 22; + if (mode == "wp") + return 16; + if (!res[0].lv_active) return 1; return 0; @@ -235,27 +276,28 @@ function lvm_updown(vol_name, up) { wildcard(lv.lv_path, "/dev/*/wp_*"))) return 22; - if (up) - uvol_uci_commit(vol_name); - - if (lv.lv_active == up) - return 0; - if (!up) { let devname = getdev(lv); - if (devname) - system(sprintf("umount /dev/%s", devname)); + if (devname) { + unregister(devname); + system(sprintf("umount /dev/%s 2>/dev/null", devname)); + } } - let lvchange_r = lvm("lvchange", up?"-k":"-a", "n", lv.lv_full_name); - if (up && lvchange_r.retval != 0) - return lvchange_r.retval; + if (lv.lv_active != up) { + let lvchange_r = lvm("lvchange", up?"-k":"-a", "n", lv.lv_full_name); + if (up && lvchange_r.retval != 0) + return lvchange_r.retval; + + lvchange_r = lvm("lvchange", up?"-a":"-k", "y", lv.lv_full_name); + if (lvchange_r.retval != 0) + return lvchange_r.retval; + } - lvchange_r = lvm("lvchange", up?"-a":"-k", "y", lv.lv_full_name); - if (lvchange_r.retval != 0) - return lvchange_r.retval; + if (up) + return register(vol_name, getdev(lv), substr(lv.lv_name, 0, 2) == "ro"); - return 0 + return 0; } function lvm_up(vol_name) { @@ -271,16 +313,20 @@ function lvm_create(vol_name, vol_size, vol_mode) { return 22; vol_size = +vol_size; - if (vol_size <= 0) + if (vol_size != vol_size || vol_size <= 0) return 22; + let size_ext = vol_size / vg.vg_extent_size; + if (vol_size % vg.vg_extent_size) + ++size_ext; + + // reclaim only an exact name+size retry; a size mismatch surfaces as EEXIST + lvm_purge_incomplete(vol_name, size_ext * vg.vg_extent_size); + let res = lvs(vg_name, vol_name); if (res[0]) return 17; - let size_ext = vol_size / vg.vg_extent_size; - if (vol_size % vg.vg_extent_size) - ++size_ext; let lvmode, mode; if (vol_mode == "ro" || vol_mode == "wo") { lvmode = "r"; @@ -315,7 +361,7 @@ function lvm_create(vol_name, vol_size, vol_mode) { return mkfs_ret; } } else { - let mkfs_ret = system(sprintf("/usr/sbin/mke2fs -F -L \"%s\" \"%s\"", vol_name, lv.lv_path)); + let mkfs_ret = system(sprintf("/usr/sbin/mke2fs -F -t ext4 -O has_journal -L \"%s\" \"%s\"", vol_name, lv.lv_path)); if (mkfs_ret != 0) { lvchange_r = lvm("lvchange", "-a", "n", lv.lv_full_name); if (lvchange_r.retval != 0) @@ -323,7 +369,6 @@ function lvm_create(vol_name, vol_size, vol_mode) { return mkfs_ret; } } - uvol_uci_add(vol_name, sprintf("/dev/%s", getdev(lv)), "rw"); ret = lvm("lvchange", "-a", "n", lv.lv_full_name); if (ret.retval != 0) @@ -336,26 +381,117 @@ function lvm_create(vol_name, vol_size, vol_mode) { return 0; } -function lvm_remove(vol_name) { +// identify the on-disk filesystem by superblock magic (no external tool needed) +function fs_type(devpath) { + let f = fs.open(devpath, "r"); + if (!f) + return null; + f.seek(1024); + let sb = f.read(58); + f.close(); + if (type(sb) != "string" || length(sb) < 58) + return null; + if (ord(sb, 0) == 0x10 && ord(sb, 1) == 0x20 && ord(sb, 2) == 0xf5 && ord(sb, 3) == 0xf2) + return "f2fs"; + if (ord(sb, 56) == 0x53 && ord(sb, 57) == 0xef) + return "ext"; + return null; +} + +// Grow a rw volume in place; the fs (ext4 or f2fs, chosen at create by size) is +// grown with its own tool. Shrink is refused. The fs-grow tool is an optional +// dependency: if absent, report and refuse rather than grow the LV past the fs. +function lvm_resize(vol_name, vol_size) { if (!vol_name || !vg_name) return 22; + vol_size = +vol_size; + if (vol_size != vol_size || vol_size <= 0) + return 22; + let res = lvs(vg_name, vol_name); if (!res[0]) return 2; - if (res[0].lv_active) - return 16; + if (substr(res[0].lv_name, 0, 2) != "rw") + return 1; + + let size_ext = vol_size / vg.vg_extent_size; + if (vol_size % vg.vg_extent_size) + ++size_ext; + + let new_size = size_ext * vg.vg_extent_size; + if (new_size == +res[0].lv_size) + return 0; + if (new_size < +res[0].lv_size) + return 22; + + let dev = getdev(res[0]); + if (!dev) + return 2; + + let fstype = fs_type(sprintf("/dev/%s", dev)); + let tool = (fstype == "f2fs") ? "resize.f2fs" : (fstype == "ext") ? "resize2fs" : null; + if (!tool) { + warn(sprintf("uvol: cannot identify filesystem on %s; not resizing\n", vol_name)); + return 95; + } - let ret = lvm("lvremove", "-y", res[0].lv_full_name); + // the fs-grow tool first, so the LV is never left larger than its filesystem + if (system(sprintf("command -v %s >/dev/null 2>&1", tool)) != 0) { + warn(sprintf("uvol: %s not found; install %s to grow this %s volume\n", + tool, (fstype == "f2fs") ? "f2fs-tools" : "e2fsprogs", fstype)); + return 95; + } + + let ret = lvm("lvextend", "-l", size_ext, res[0].lv_full_name); if (ret.retval != 0) return ret.retval; - uvol_uci_remove(vol_name); - uvol_uci_commit(vol_name); + if (fstype == "f2fs") + return system(sprintf("resize.f2fs /dev/%s", dev)); + + return system(sprintf("resize2fs /dev/%s", dev)); +} + +// Reap volumes marked for deferred deletion (dd_ prefix). A volume can only be +// reaped once its backing device has no holder; blockd signals that moment with +// a mount.umount notification (autofs idle-expiry), which triggers 'uvol reap'. +// Still-held volumes are left for the next signal (or the boot sweep). +function lvm_reap() { + for (let dd in lvs_deleting(vg_name)) { + let dev = getdev(dd); + if (dd.lv_active) { + let r = lvm("lvchange", "-a", "n", dd.lv_full_name); + if (r.retval != 0) + continue; + } + if (dev) + unregister(dev); + lvm("lvremove", "-y", dd.lv_full_name); + } return 0; } +function lvm_remove(vol_name) { + if (!vol_name || !vg_name) + return 22; + + let res = lvs(vg_name, vol_name); + if (!res[0]) + return 2; + + // mark for deletion: rename to the dd_ state (works whether the volume is + // active or not, and hides it from list/status). reap removes it now if the + // device is already free, otherwise blockd's mount.umount triggers reap once + // the holder releases it. + let ret = lvm("lvrename", vg_name, res[0].lv_name, sprintf("dd_%s", vol_name)); + if (ret.retval != 0) + return ret.retval; + + return lvm_reap(); +} + function lvm_dd(in_fd, out_fd, vol_size) { let rem = vol_size; let buf; @@ -369,7 +505,7 @@ function lvm_dd(in_fd, out_fd, vol_size) { return rem; } -function lvm_write(vol_name, vol_size) { +function lvm_write(vol_name, vol_size, verify) { if (!vol_name || !vg_name) return 22; @@ -382,70 +518,74 @@ function lvm_write(vol_name, vol_size) { if (vol_size > lv.lv_size) return 27; - if (wildcard(lv.lv_path, "/dev/*/wo_*")) { - let ret = lvm("lvchange", "-p", "rw", lv.lv_full_name); - if (ret.retval != 0) - return ret.retval; + if (!wildcard(lv.lv_path, "/dev/*/wo_*")) + return 22; + + let ret = lvm("lvchange", "-p", "rw", lv.lv_full_name); + if (ret.retval != 0) + return ret.retval; - let ret = lvm("lvchange", "-a", "y", lv.lv_full_name); - if (ret.retval != 0) - return ret.retval; + let ret = lvm("lvchange", "-a", "y", lv.lv_full_name); + if (ret.retval != 0) + return ret.retval; - let volfile = fs.open(lv.lv_path, "w"); - let ret = lvm_dd(fs.stdin, volfile, vol_size); - volfile.close(); - if (ret < 0) { - printf("more %d bytes data than given size!\n", -ret); - } + let volfile = fs.open(lv.lv_path, "w"); + let rem = lvm_dd(fs.stdin, volfile, vol_size); + volfile.close(); + if (rem < 0) { + printf("more %d bytes data than given size!\n", -rem); + } - if (ret > 0) { - printf("reading finished %d bytes before given size!\n", ret); - } + if (rem > 0) { + printf("reading finished %d bytes before given size!\n", rem); + } - uvol_uci_add(vol_name, sprintf("/dev/%s", getdev(lv)), "ro"); + if (verify && !verify(lv.lv_path)) { + lvm("lvchange", "-a", "n", lv.lv_full_name); + lvm("lvchange", "-p", "r", lv.lv_full_name); + return 74; + } - let ret = lvm("lvchange", "-a", "n", lv.lv_full_name); - if (ret.retval != 0) - return ret.retval; + let ret = lvm("lvchange", "-a", "n", lv.lv_full_name); + if (ret.retval != 0) + return ret.retval; - let ret = lvm("lvchange", "-p", "r", lv.lv_full_name); - if (ret.retval != 0) - return ret.retval; + let ret = lvm("lvchange", "-p", "r", lv.lv_full_name); + if (ret.retval != 0) + return ret.retval; - let ret = lvm("lvrename", vg_name, sprintf("wo_%s", vol_name), sprintf("ro_%s", vol_name)); - if (ret.retval != 0) - return ret.retval; + let ret = lvm("lvrename", vg_name, sprintf("wo_%s", vol_name), sprintf("ro_%s", vol_name)); + if (ret.retval != 0) + return ret.retval; - } else { - return 22; - } return 0; } function lvm_detect() { - let temp_up = []; - let inactive_lv = lvs(vg_name, null, "lv_skip_activation!=0"); - for (let lv in inactive_lv) { - lvm("lvchange", "-k", "n", lv.lv_full_name); - lvm("lvchange", "-a", "y", lv.lv_full_name); - push(temp_up, lv.lv_full_name); - } - sleep(1000); - uvol_uci_init(); for (let lv in lvs(vg_name)) { - let vol_name = substr(lv.lv_name, 3); - let vol_mode = substr(lv.lv_name, 0, 2); - uvol_uci_add(vol_name, sprintf("/dev/%s", getdev(lv)), vol_mode); - } - uvol_uci_commit(); - for (let lv_full_name in temp_up) { - lvm("lvchange", "-a", "n", lv_full_name); - lvm("lvchange", "-k", "y", lv_full_name); + if (!lv.lv_active) + continue; + + let mode = substr(lv.lv_name, 0, 2); + if (mode != "ro" && mode != "rw") + continue; + + register(substr(lv.lv_name, 3), getdev(lv), mode == "ro"); } return 0; } function lvm_boot() { + // clear crash/power-loss leftovers before activating + lvm_reap(); + lvm_purge_incomplete(); + for (let lv in lvs(vg_name, null, "lv_skip_activation=0")) { + let mode = substr(lv.lv_name, 0, 2); + if (mode != "ro" && mode != "rw") + continue; + + lvm_up(substr(lv.lv_name, 3)); + } return 0; } @@ -464,5 +604,7 @@ backend.device = lvm_device; backend.up = lvm_up; backend.down = lvm_down; backend.create = lvm_create; +backend.resize = lvm_resize; backend.remove = lvm_remove; +backend.reap = lvm_reap; backend.write = lvm_write; diff --git a/utils/uvol/files/mount.uc b/utils/uvol/files/mount.uc new file mode 100644 index 000000000..d65e792f3 --- /dev/null +++ b/utils/uvol/files/mount.uc @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// blockd mount registration for uvol +// (c) 2022 Daniel Golle +// +// Register volumes with blockd over ubus instead of writing /etc/config/fstab, +// so volume state stays on the self-describing backing store and never leaks +// into the firmware rootfs. + +let mount_fs = require("fs"); +let mount_ubus = require("ubus"); + +let uvol_target = function(vol_name) { + return sprintf("/tmp/run/uvol/%s", vol_name); +}; + +uvol_mount = { + register: function(vol_name, dev_name, read_only) { + if (!dev_name) + return 22; + if (substr(vol_name, 0, 1) == "." && vol_name != ".meta") + return 1; + + let target = uvol_target(vol_name); + let st = mount_fs.lstat(target); + if (st && st.type == "link") + mount_fs.unlink(target); + else if (st && st.type == "directory") + mount_fs.rmdir(target); + + let data = { + device: dev_name, + target: target, + autofs: 1, + }; + // declare the volume's read-only-ness to blockd, which carries it + // to block as a mount option; block never guesses from the fs type. + if (read_only) + data.options = "ro"; + else + data.check_fs = 1; + + mount_ubus.call({ + object: "block", + method: "hotplug", + data: data, + }); + return mount_ubus.error() ? -1 : 0; + }, + + unregister: function(dev_name) { + if (!dev_name) + return 22; + + mount_ubus.call({ + object: "block", + method: "hotplug", + data: { + device: dev_name, + remove: 1, + }, + }); + return mount_ubus.error() ? -1 : 0; + } +}; diff --git a/utils/uvol/files/ubi.uc b/utils/uvol/files/ubi.uc index 8b46112c7..69edd8c29 100644 --- a/utils/uvol/files/ubi.uc +++ b/utils/uvol/files/ubi.uc @@ -50,10 +50,6 @@ function mkubifs(vol_dev) { return 0; } -function block_hotplug(action, devname) { - return system(sprintf("ACTION=%s DEVNAME=%s /sbin/block hotplug", action, devname)); -} - function ubi_init(ctx) { cursor = ctx.cursor; fs = ctx.fs; @@ -76,10 +72,8 @@ function ubi_init(ctx) { ebsize = read_file(sprintf("%s/eraseblock_size", ubidevpath)); - uvol_uci_add = ctx.uci_add; - uvol_uci_commit = ctx.uci_commit; - uvol_uci_remove = ctx.uci_remove; - uvol_uci_init = ctx.uci_init; + register = ctx.register; + unregister = ctx.unregister; return true; } @@ -137,10 +131,6 @@ function ubi_device(vol_name) { } function ubi_create(vol_name, vol_size, vol_mode) { - let vol_dev = ubi_get_dev(vol_name); - if (vol_dev) - return 17; - let mode; if (vol_mode == "ro" || vol_mode == "wo") mode = "wo"; @@ -152,6 +142,13 @@ function ubi_create(vol_name, vol_size, vol_mode) { let vol_size = +vol_size; if (vol_size <= 0) return 22; + + ubi_purge_incomplete(vol_name, vol_size); + + let vol_dev = ubi_get_dev(vol_name); + if (vol_dev) + return 17; + let ret = system(sprintf("ubimkvol /dev/%s -N \"uvol-%s-%s\" -s %d", ubidev, mode, vol_name, vol_size)); if (ret != 0) return ret; @@ -171,8 +168,6 @@ function ubi_create(vol_name, vol_size, vol_mode) { if (ret != 0) return ret; - uvol_uci_add(vol_name, sprintf("/dev/%s", vol_dev), "rw"); - let ret = system(sprintf("ubirename /dev/%s \"uvol-wp-%s\" \"uvol-wd-%s\"", ubidev, vol_name, vol_name)); if (ret != 0) return ret; @@ -180,27 +175,113 @@ function ubi_create(vol_name, vol_size, vol_mode) { return 0; } -function ubi_remove(vol_name) { +// Grow a rw volume; UBIFS uses the new size directly. Shrink is refused. +function ubi_resize(vol_name, vol_size) { + vol_size = +vol_size; + if (vol_size <= 0) + return 22; + let vol_dev = ubi_get_dev(vol_name); if (!vol_dev) return 2; - let vol_mode = vol_get_mode(vol_dev); - if (vol_mode == "rw" || vol_mode == "ro") - return 16; + if (vol_get_mode(vol_dev) != "rw") + return 1; - let volnum = split(vol_dev, "_")[1]; + let leb = +read_file(sprintf("/sys/class/ubi/%s/usable_eb_size", vol_dev)); + let cur_lebs = +read_file(sprintf("/sys/class/ubi/%s/reserved_ebs", vol_dev)); + let req_lebs = vol_size / leb; + if (vol_size % leb) + ++req_lebs; - let ret = system(sprintf("ubirmvol /dev/%s -n %d", ubidev, volnum)); - if (ret != 0) - return ret; + if (req_lebs == cur_lebs) + return 0; + if (req_lebs < cur_lebs) + return 22; - uvol_uci_remove(vol_name); - uvol_uci_commit(vol_name); + return system(sprintf("ubirsvol /dev/%s -N \"uvol-rw-%s\" -s %d", ubidev, vol_name, vol_size)); +} + +function ubi_get_deleting() { + let ret = []; + for (vol_dir in fs.glob(sprintf("/sys/class/ubi/%s_*", ubidev))) { + let vol_ubiname = read_file(sprintf("%s/name", vol_dir)); + if (!wildcard(vol_ubiname, "uvol-dd-*")) + continue; + + push(ret, fs.basename(vol_dir)); + } + return ret; +} + +function ubi_get_incomplete(vol_name) { + let ret = []; + let pat = vol_name ? sprintf("uvol-w[op]-%s", vol_name) : "uvol-w[op]-*"; + for (vol_dir in fs.glob(sprintf("/sys/class/ubi/%s_*", ubidev))) { + let vol_ubiname = read_file(sprintf("%s/name", vol_dir)); + if (!wildcard(vol_ubiname, pat)) + continue; + push(ret, { + dev: fs.basename(vol_dir), + lebs: +read_file(sprintf("%s/reserved_ebs", vol_dir)), + leb: +read_file(sprintf("%s/usable_eb_size", vol_dir)), + }); + } + return ret; +} +// purge incomplete (wo/wp) leftovers; with match_size, only those whose +// reservation matches that size (null purges all) +function ubi_purge_incomplete(vol_name, match_size) { + for (let v in ubi_get_incomplete(vol_name)) { + if (match_size != null) { + if (!v.leb || !v.lebs) + continue; + let want = match_size / v.leb; + if (match_size % v.leb) + want++; + if (v.lebs != want) + continue; + } + let volnum = split(v.dev, "_")[1]; + system(sprintf("ubirmvol /dev/%s -n %d 2>/dev/null", ubidev, volnum)); + } return 0; } +function ubi_reap() { + for (let vol_dev in ubi_get_deleting()) { + let volnum = split(vol_dev, "_")[1]; + let blkdev = sprintf("ubiblock%s", substr(vol_dev, 3)); + let isblock = fs.access(sprintf("/dev/%s", blkdev), "r"); + + if (isblock && system(sprintf("ubiblock --remove /dev/%s 2>/dev/null", vol_dev)) != 0) + continue; + + if (!isblock) + system(sprintf("umount /dev/%s 2>/dev/null", vol_dev)); + + if (system(sprintf("ubirmvol /dev/%s -n %d 2>/dev/null", ubidev, volnum)) != 0) + continue; + + unregister(isblock ? blkdev : vol_dev); + } + return 0; +} + +function ubi_remove(vol_name) { + let vol_dev = ubi_get_dev(vol_name); + if (!vol_dev) + return 2; + + let vol_ubiname = read_file(sprintf("/sys/class/ubi/%s/name", vol_dev)); + let ret = system(sprintf("ubirename /dev/%s \"%s\" \"uvol-dd-%s\"", ubidev, vol_ubiname, vol_name)); + if (ret != 0) + return ret; + + return ubi_reap(); +} + function ubi_up(vol_name) { let vol_dev = ubi_get_dev(vol_name); if (!vol_dev) @@ -214,19 +295,22 @@ function ubi_up(vol_name) { else if (vol_mode == "wp") return 16; - uvol_uci_commit(vol_name); if (vol_mode == "rd") { let ret = system(sprintf("ubirename /dev/%s \"uvol-rd-%s\" \"uvol-ro-%s\"", ubidev, vol_name, vol_name)); if (ret != 0) return ret; - return system(sprintf("ubiblock --create /dev/%s", vol_dev)); + ret = system(sprintf("ubiblock --create /dev/%s", vol_dev)); + if (ret != 0) + return ret; + + return register(vol_name, sprintf("ubiblock%s", substr(vol_dev, 3)), true); } else if (vol_mode == "wd") { let ret = system(sprintf("ubirename /dev/%s \"uvol-wd-%s\" \"uvol-rw-%s\"", ubidev, vol_name, vol_name)); if (ret != 0) return ret; - return block_hotplug("add", vol_dev); + return register(vol_name, vol_dev, false); } return 0; } @@ -244,14 +328,15 @@ function ubi_down(vol_name) { else if (vol_mode == "wp") return 16; else if (vol_mode == "ro") { + unregister(sprintf("ubiblock%s", substr(vol_dev, 3))); system(sprintf("umount /dev/ubiblock%s 2>&1 >/dev/null", substr(vol_dev, 3))); system(sprintf("ubiblock --remove /dev/%s", vol_dev)); let ret = system(sprintf("ubirename /dev/%s \"uvol-ro-%s\" \"uvol-rd-%s\"", ubidev, vol_name, vol_name)); return ret; } else if (vol_mode == "rw") { + unregister(vol_dev); system(sprintf("umount /dev/%s 2>&1 >/dev/null", vol_dev)); let ret = system(sprintf("ubirename /dev/%s \"uvol-rw-%s\" \"uvol-wd-%s\"", ubidev, vol_name, vol_name)); - block_hotplug("remove", vol_dev); return ret; } return 0; @@ -279,64 +364,38 @@ function ubi_list(search_name) { return volumes; } -function ubi_detect() { - let tmpdev = []; +function ubi_register_active() { for (vol_dir in fs.glob(sprintf("/sys/class/ubi/%s_*", ubidev))) { let vol_ubiname = read_file(sprintf("%s/name", vol_dir)); - - if (!wildcard(vol_ubiname, "uvol-r[od]-*")) - continue; - - let vol_name = substr(vol_ubiname, 8); - let vol_mode = substr(vol_ubiname, 5, 2); - let vol_dev = fs.basename(vol_dir); - - ret = system(sprintf("ubiblock --create /dev/%s", vol_dev)); - if (ret) - continue; - - if (vol_mode == "rd") - push(tmpdev, vol_dev); - } - - uvol_uci_init(); - - for (vol_dir in fs.glob(sprintf("/sys/class/ubi/%s_*", ubidev))) { - let vol_ubiname = read_file(sprintf("%s/name", vol_dir)); - if (!wildcard(vol_ubiname, "uvol-[rw][wod]-*")) + if (!wildcard(vol_ubiname, "uvol-r[ow]-*")) continue; let vol_dev = fs.basename(vol_dir); let vol_name = substr(vol_ubiname, 8); let vol_mode = substr(vol_ubiname, 5, 2); - if (vol_mode == "ro" || vol_mode == "rd") - uvol_uci_add(vol_name, sprintf("/dev/ubiblock%s", substr(vol_dev, 3)), "ro"); - else if (vol_mode == "rw" || vol_mode == "wd") - uvol_uci_add(vol_name, sprintf("/dev/%s", vol_dev), "rw"); + if (vol_mode == "ro") { + system(sprintf("ubiblock --create /dev/%s", vol_dev)); + register(vol_name, sprintf("ubiblock%s", substr(vol_dev, 3)), true); + } else { + register(vol_name, vol_dev, false); + } } - - uvol_uci_commit(); - - for (vol_dev in tmpdev) - system(sprintf("ubiblock --remove /dev/%s", vol_dev)); - return 0; } -function ubi_boot() { - for (vol_dir in fs.glob(sprintf("/sys/class/ubi/%s_*", ubidev))) { - let vol_dev = fs.basename(vol_dir); - let vol_ubiname = read_file(sprintf("%s/name", vol_dir)); - - if (!wildcard(vol_ubiname, "uvol-ro-*")) - continue; +function ubi_detect() { + return ubi_register_active(); +} - system(sprintf("ubiblock --create /dev/%s", vol_dev)); - } +function ubi_boot() { + // clear crash/power-loss leftovers before activating + ubi_reap(); + ubi_purge_incomplete(); + return ubi_register_active(); } -function ubi_write(vol_name, write_size) { +function ubi_write(vol_name, write_size, verify) { let vol_dev = ubi_get_dev(vol_name); if (!vol_dev) return 2; @@ -349,13 +408,13 @@ function ubi_write(vol_name, write_size) { if (vol_mode != "wo") return 22; - let ret = system(sprintf("ubiupdatevol -s %s /dev/%s -", write_size, vol_dev)); + let ret = system(sprintf("ubiupdatevol -s %d /dev/%s -", write_size, vol_dev)); if (ret) return ret; - system(sprintf("ubiblock --create /dev/%s", vol_dev)); - uvol_uci_add(vol_name, sprintf("/dev/ubiblock%s", substr(vol_dev, 3)), "ro"); - system(sprintf("ubiblock --remove /dev/%s", vol_dev)); + if (verify && !verify(sprintf("/dev/%s", vol_dev))) + return 74; + system(sprintf("ubirename /dev/%s \"uvol-wo-%s\" \"uvol-rd-%s\"", ubidev, vol_name, vol_name)); return 0; @@ -376,5 +435,7 @@ backend.device = ubi_device; backend.up = ubi_up; backend.down = ubi_down; backend.create = ubi_create; +backend.resize = ubi_resize; backend.remove = ubi_remove; +backend.reap = ubi_reap; backend.write = ubi_write; diff --git a/utils/uvol/files/uci.uc b/utils/uvol/files/uci.uc deleted file mode 100644 index be3bae136..000000000 --- a/utils/uvol/files/uci.uc +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// UCI tools for uvol -// (c) 2022 Daniel Golle - -let uci_spooldir = "/var/spool/uvol"; -let init_spooldir = function(void) { - parentdir = fs.stat(fs.dirname(uci_spooldir)); - if (!parentdir || parentdir.type != "directory") - fs.mkdir(fs.dirname(uci_spooldir), 0755); - fs.mkdir(uci_spooldir, 0700); -}; - -uvol_uci = { - uvol_uci_add: function(vol_name, dev_name, mode) { - try { - let autofs = false; - let uuid; - let target; - if (mode == "ro") - autofs = true; - - let uciname = replace(vol_name, /[-.]/g, "_"); - uciname = replace(uciname, /!([:alnum:]_)/g, ""); - let bdinfo_p = fs.popen("/sbin/block info"); - let bdinfo_l; - while (bdinfo_l = bdinfo_p.read("line")) { - if (substr(bdinfo_l, 0, length(dev_name) + 1) != dev_name + ":") - continue; - let bdinfo_e = split(bdinfo_l, " "); - shift(bdinfo_e); - for (let bdinfo_a in bdinfo_e) { - let bdinfo_v = split(bdinfo_a, "="); - if (bdinfo_v[0] && bdinfo_v[0] == "UUID") { - uuid = trim(bdinfo_v[1], "\""); - break; - } - } - break; - } - - if (!uuid) - return 22; - - if (uciname == "_meta") - target = "/tmp/run/uvol/.meta"; - else if (substr(uciname, 0, 1) == "_") - return 1; - else - target = sprintf("/tmp/run/uvol/%s", vol_name); - - init_spooldir(); - let remspool = sprintf("%s/remove-%s", uci_spooldir, uciname); - if (fs.stat(remspool)) - fs.unlink(remspool); - - let addobj = {}; - addobj.name=uciname; - addobj.uuid=uuid; - addobj.target=target; - addobj.options=mode; - addobj.autofs=autofs; - addobj.enabled=true; - - let spoolfile = fs.open(sprintf("%s/add-%s", uci_spooldir, uciname), "w"); - spoolfile.write(addobj); - spoolfile.close(); - } catch(e) { - printf("adding UCI section to spool failed"); - return -1; - } - return 0; - }, - - uvol_uci_remove: function(vol_name) { - let uciname = replace(vol_name, /[-.]/g, "_"); - uciname = replace(uciname, /!([:alnum:]_)/g, ""); - - let addspool = sprintf("%s/add-%s", uci_spooldir, uciname); - if (fs.stat(addspool)) { - fs.unlink(addspool); - return 0; - } - init_spooldir(); - let spoolfile = fs.open(sprintf("%s/remove-%s", uci_spooldir, uciname), "w"); - spoolfile.write(uciname); - spoolfile.close(); - return 0; - }, - - uvol_uci_commit: function(vol_name) { - try { - let uciname = null; - if (vol_name) { - uciname = replace(vol_name, /[-.]/g, "_"); - uciname = replace(uciname, /!([:alnum:]_)/g, ""); - } - - for (let file in fs.glob(sprintf("%s/*-%s", uci_spooldir, uciname?uciname:"*"))) { - let action = split(fs.basename(file), "-")[0]; - let spoolfd = fs.open(file, "r"); - let spoolstr = spoolfd.read("all"); - spoolfd.close(); - fs.unlink(file); - if (action == "remove") { - cursor.delete("fstab", spoolstr); - } else if (action == "add") { - let spoolobj = json(spoolstr); - cursor.set("fstab", spoolobj.name, "mount"); - for (key in keys(spoolobj)) { - if (key == "name") - continue; - - cursor.set("fstab", spoolobj.name, key, spoolobj[key]); - } - } - } - cursor.commit(); - } catch(e) { - printf("committing UCI spool failed"); - return -1; - } - return 0; - }, - - uvol_uci_init: function () { - cursor.load("fstab"); - let f = cursor.get("fstab", "@uvol[0]", "initialized"); - if (f == 1) - return 0; - - cursor.add("fstab", "uvol"); - cursor.set("fstab", "@uvol[-1]", "initialized", true); - cursor.commit(); - cursor.unload("fstab"); - return 0; - } -}; diff --git a/utils/uvol/files/uvol b/utils/uvol/files/uvol index 2e2ea2928..1b06bcdf7 100644 --- a/utils/uvol/files/uvol +++ b/utils/uvol/files/uvol @@ -3,10 +3,13 @@ // uvol - storage volume manager for OpenWrt // (c) 2022 Daniel Golle -let help_output = "uvol storage volume manager +let uvol_version = "@PKG_VERSION@"; + +let help_output = `uvol ${uvol_version} - storage volume manager syntax: uvol command ... commands: + version show version and exit boot get active volumes ready (called on boot) free show number of bytes available total show total number of bytes @@ -25,7 +28,20 @@ commands: 1 - volume is not ready for use 2 - volume doesn't exist write volname size write to volume from stdin, size in bytes -"; +`; + +let cmd = shift(ARGV); + +// answer before probing for a backend, so both work on a system without storage +if (cmd == "version" || cmd == "--version" || cmd == "-v" || cmd == "-V") { + printf("uvol %s\n", uvol_version); + return 0; +} + +if (!cmd || cmd == "-h" || cmd == "--help" || cmd == "help") { + printf("%s", help_output); + return cmd?0:22; +} let fs = require("fs"); let uci = require("uci"); @@ -34,11 +50,9 @@ let cursor = uci ? uci.cursor() : null; let ctx = {}; ctx.cursor = cursor; ctx.fs = fs; -include("/usr/lib/uvol/uci.uc"); -ctx.uci_add = uvol_uci.uvol_uci_add; -ctx.uci_remove = uvol_uci.uvol_uci_remove; -ctx.uci_commit = uvol_uci.uvol_uci_commit; -ctx.uci_init = uvol_uci.uvol_uci_init; +include("/usr/lib/uvol/mount.uc"); +ctx.register = uvol_mount.register; +ctx.unregister = uvol_mount.unregister; let backend = null; let tried_backends = []; @@ -64,27 +78,53 @@ if (!backend) { exit(2); } -// The below code is needed as older versions of ucode pass the complete cmdline via ARGV -// Once we can rely in more recent ucode the while loop can be replaced by simply -// let cmd = shift(ARGV); -let skip = null; -let cmd = null; -let skip_argv = ["/usr/bin/ucode", "-R", "/usr/sbin/uvol"]; -while (skip = shift(ARGV)) { - if (skip != shift(skip_argv)) { - cmd = skip; - break; - } +if (!(cmd in keys(backend))) { + printf("command %s not found\n", cmd); + return 22; } -if (!cmd || cmd == "-h" || cmd == "help") { - printf("%s", help_output); - return cmd?0:22; +// serialise state-mutating commands device-wide; released on process exit. +// create and write additionally serialise per volume, as create may reclaim +// an incomplete volume of the same name and size while a writer is still +// streaming to it. The device-wide lock is always taken before the +// per-volume lock; write takes only the per-volume one so a slow stream +// does not stall operations on other volumes. +let uvol_mutating = [ "boot", "detect", "create", "up", "down", "remove", "reap", "resize" ]; +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 (!(cmd in keys(backend))) { - printf("command %s not found\n", cmd); - return 22; +let meta_init = function() { + let sz = backend.size(".meta"); + if (type(sz) != "string" || +sz <= 0) { + let metasz = +backend.total() / 10240; + if (metasz < 4194304) + metasz = 4194304; + if (metasz > +backend.free()) + return; + if (backend.create(".meta", sprintf("%d", metasz), "rw") != 0) + return; + } + backend.up(".meta"); + fs.mkdir("/tmp/run/uvol/.meta/apk", 0755); +}; + +if (cmd == "boot") { + backend.boot(); + meta_init(); + exit(0); } let json_output = false; @@ -110,6 +150,69 @@ let legacy_output = function(var) { return out; }; +// Content-addressed volumes are named "-"; the name carries the +// expected digest of its content. Validation lives here in the generic layer so +// backends stay plain storage. Add an entry to ca_algos to support a new digest. +let ca_algos = { + sha256: { cmd: "sha256sum", len: 64 }, +}; + +let ca_parse = function(name) { + let m = match(name, /^([0-9a-z]+)-([0-9a-f]+)$/); + if (!m) + return null; + let a = ca_algos[m[1]]; + if (!a || length(m[2]) != a.len) + return null; + return { cmd: a.cmd, hash: m[2] }; +}; + +let ca_digest = function(cmd, size, path) { + let src = path ? sprintf("head -c %d %s", size, path) : sprintf("head -c %d", size); + let f = fs.popen(sprintf("%s | %s", src, cmd), "r"); + if (!f) + return null; + let line = f.read("line"); + f.close(); + return split(trim(line ?? ""), " ")[0]; +}; + +// complete means the volume exists and has left the write-only state: +// ready (0) or merely down (1); incomplete volumes report 22 or 16 +let ca_complete = function(name) { + let st = backend.status(name); + return st == 0 || st == 1; +}; + +let ca_mismatch = function(name) { + printf("content hash mismatch for %s\n", name); + exit(74); +}; + +if (cmd == "create" && ca_parse(ARGV[0])) { + let ret = backend.create(...ARGV); + exit(ret == 17 ? 0 : ret); +} + +if (cmd == "write") { + let ca = ca_parse(ARGV[0]); + if (ca) { + let size = +ARGV[1]; + if (ca_complete(ARGV[0])) { + if (ca_digest(ca.cmd, size, null) != ca.hash) + ca_mismatch(ARGV[0]); + exit(0); + } + // the backend calls verify() on the raw volume before finalising it; + // 74 (EBADMSG) reports a rejected payload, the volume stays incomplete + let verify = (path) => ca_digest(ca.cmd, size, path) == ca.hash; + let ret = backend.write(ARGV[0], size, verify); + if (ret == 74) + ca_mismatch(ARGV[0]); + exit(ret); + } +} + if (type(backend[cmd]) == "function") { let ret = backend[cmd](...ARGV); if (type(ret) == "int") { diff --git a/utils/uvol/files/uvol.defaults b/utils/uvol/files/uvol.defaults deleted file mode 100644 index 927f8c115..000000000 --- a/utils/uvol/files/uvol.defaults +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh - -uvol_init() { - local metasz freesz totalsz - uvol detect - metasz="$(uvol size .meta 2>/dev/null)" - if [ "$metasz" ]; then - [ "$((metasz))" -gt 0 ] && return - fi - totalsz="$(uvol total)" - freesz="$(uvol free)" - metasz="$((totalsz / 10240))" - [ "$metasz" -lt 4194304 ] && metasz=4194304 - [ "$metasz" -gt "$freesz" ] && return - uvol create .meta "$metasz" rw - uvol up .meta - grep -q "uvol/.meta" /proc/mounts || return - mkdir -p "/var/run/uvol/.meta/apk" -} - -uci -q get fstab.@uvol[0].initialized >/dev/null || uvol_init diff --git a/utils/uvol/files/uvol.init b/utils/uvol/files/uvol.init index d75187368..123b97cae 100644 --- a/utils/uvol/files/uvol.init +++ b/utils/uvol/files/uvol.init @@ -6,16 +6,16 @@ NAME=uvol PROG=/usr/sbin/uvol start_service() { - [ "${__BOOT_UVOL}" = "1" ] && return 0 - procd_open_instance "$NAME" - procd_set_param command "$PROG" boot - procd_close_instance -} + local ready -boot() { - __BOOT_UVOL=1 start + ready=$(ubus call block status 2>/dev/null | jsonfilter -e '@.ready') + [ "$ready" = "true" ] && "$PROG" boot } service_triggers() { - procd_add_raw_trigger "mount.ready" 200 /etc/init.d/uvol start + procd_add_raw_trigger "mount.ready" 1000 "$PROG" boot + + # blockd emits mount.umount when an idle volume's device becomes free; + # reap any volume that was marked for deferred deletion while in use + procd_add_raw_trigger "mount.umount" 1000 "$PROG" reap }