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 "<algo>-<hexdigest>" 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@9b11fa4088.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
This commit is contained in:
Daniel Golle committed 2026-07-25 23:53:57 +01:00
1 parent a9fc86bec5
commit cb60600aec
8 files changed
+574 -362

No files matched your search

+6 -6
View File
@@ -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 <daniel@makrotopia.org>
@@ -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))
+230 -88
View File
@@ -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;
}
return 0
if (up)
return register(vol_name, getdev(lv), substr(lv.lv_name, 0, 2) == "ro");
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,6 +381,98 @@ function lvm_create(vol_name, vol_size, vol_mode) {
return 0;
}
// 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 (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;
}
// 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;
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;
@@ -344,16 +481,15 @@ function lvm_remove(vol_name) {
if (!res[0])
return 2;
if (res[0].lv_active)
return 16;
let ret = lvm("lvremove", "-y", res[0].lv_full_name);
// 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;
uvol_uci_remove(vol_name);
uvol_uci_commit(vol_name);
return 0;
return lvm_reap();
}
function lvm_dd(in_fd, out_fd, vol_size) {
@@ -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;
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);
}
if (ret > 0) {
printf("reading finished %d bytes before given size!\n", ret);
}
uvol_uci_add(vol_name, sprintf("/dev/%s", getdev(lv)), "ro");
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("lvrename", vg_name, sprintf("wo_%s", vol_name), sprintf("ro_%s", vol_name));
if (ret.retval != 0)
return ret.retval;
} else {
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 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 (rem > 0) {
printf("reading finished %d bytes before given size!\n", rem);
}
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", "-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;
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;
+64
View File
@@ -0,0 +1,64 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// blockd mount registration for uvol
// (c) 2022 Daniel Golle <daniel@makrotopia.org>
//
// 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;
}
};
+138 -77
View File
@@ -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,25 +175,111 @@ function ubi_create(vol_name, vol_size, vol_mode) {
return 0;
}
// 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;
if (vol_get_mode(vol_dev) != "rw")
return 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;
if (req_lebs == cur_lebs)
return 0;
if (req_lebs < cur_lebs)
return 22;
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_mode = vol_get_mode(vol_dev);
if (vol_mode == "rw" || vol_mode == "ro")
return 16;
let volnum = split(vol_dev, "_")[1];
let ret = system(sprintf("ubirmvol /dev/%s -n %d", ubidev, volnum));
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;
uvol_uci_remove(vol_name);
uvol_uci_commit(vol_name);
return 0;
return ubi_reap();
}
function ubi_up(vol_name) {
@@ -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;
system(sprintf("ubiblock --create /dev/%s", vol_dev));
}
function ubi_detect() {
return ubi_register_active();
}
function ubi_write(vol_name, write_size) {
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, 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;
-137
View File
@@ -1,137 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// UCI tools for uvol
// (c) 2022 Daniel Golle <daniel@makrotopia.org>
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;
}
};
+128 -25
View File
@@ -3,10 +3,13 @@
// uvol - storage volume manager for OpenWrt
// (c) 2022 Daniel Golle <daniel@makrotopia.org>
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,29 +78,55 @@ 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 || cmd == "-h" || cmd == "help") {
printf("%s", help_output);
return cmd?0:22;
}
if (!(cmd in keys(backend))) {
printf("command %s not found\n", cmd);
return 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");
}
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;
if (ARGV[0] == "-j") {
json_output = true;
@@ -110,6 +150,69 @@ let legacy_output = function(var) {
return out;
};
// Content-addressed volumes are named "<algo>-<hexdigest>"; 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") {
-21
View File
@@ -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
+8 -8
View File
@@ -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
}