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>
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
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
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))
}
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;
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;
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;
}
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;
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) {
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";
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)
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)
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;
return rem;
}
-function lvm_write(vol_name, vol_size) {
+function lvm_write(vol_name, vol_size, verify) {
if (!vol_name || !vg_name)
return 22;
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;
}
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;
--- /dev/null
+// 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;
+ }
+};
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;
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;
}
}
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";
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;
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;
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)
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;
}
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;
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;
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;
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;
+++ /dev/null
-// 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;
- }
-};
// 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
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");
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 = [];
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;
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") {
+++ /dev/null
-#!/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
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
}