# lua-sysmount A lightweight, high-performance, and secure Lua binding for the native FreeBSD `getfsstat(2)` system call. This module allows Lua scripts to query active storage mounts and filesystem types (such as UFS, ZFS, msdosfs) directly from the kernel memory space. By operating entirely through C bindings, it completely eliminates the need to spawn external shell processes or parse text output from utilities like `mount(8)`, ensuring maximum security and execution speed. ## Features - **100% Native**: Fetches live data directly from FreeBSD kernel memory structures. - **No Shell Spawning**: Avoids risky and slow `io.popen("/sbin/mount")` calls. - **Extended Metadata**: Returns filesystem types (`ufs`, `zfs`, etc.) alongside device names and mount points. - **FreeBSD Ports Framework Ready**: Fully supports `FLAVORS` and complies with modern FreeBSD packaging standards. --- ## Installation ### Method 1: Build & Install via FreeBSD Ports Framework (Recommended) This method automatically handles Lua `FLAVORS` and registers the module in the system package database (`pkg`). 1. Copy the port directory to your local ports tree: ```bash cp -R port/sysutils/lua-sysmount /usr/ports/sysutils/ ``` 2. Build and install for your default Lua version (e.g., Lua 5.4): ```bash cd /usr/ports/sysutils/lua-sysmount sudo make install clean ``` 3. Alternatively, target a specific Lua flavor (e.g., Lua 5.3): ```bash sudo make FLAVOR=lua53 install clean ``` ### Method 2: Manual Standalone Build If you don't want to use the ports tree, you can compile the `.so` module directly in any directory. It will use `pkg-config` to discover your active Lua setup. ```bash # Clone the repository and navigate to the source directory git clone https://github.com cd lua-sysmount/src # Compile and install globally make sudo make install make clean ``` --- ## Usage Examples ### 1. Basic Module Verification You can test the native binding directly from your interactive Lua shell: ```lua $ lua54 Lua 5.4.8 Copyright (C) 1994-2025 Lua.org, PUC-Rio > local sysmount = require("sysmount") > local mounts = sysmount.getfsstat() > > -- Print the first mount point entry > print(mounts[1].dev, "->", mounts[1].mnt, "[" .. mounts[1].type .. "]") /dev/ada0p2 -> / [ufs] ``` ### 2. Full Storage Topography Script Below is a production-ready example combining `sysctl` (via `lua-sysctl` port) for disk architecture, your custom `sysmount` for actual mount points, and safe `smartctl` execution for drive temperatures. Save this as `disks.lua`: ```lua #!/usr/bin/env lua local sysctl = require("sysctl") local sysmount = require("sysmount") -- 1. Fetch GEOM storage layout from the kernel local function get_geom_xml() return sysctl.get("kern.geom.confxml") end -- 2. Fetch live mount points via our custom native C module local function get_mounts() local mounts = {} local mnt_list = sysmount.getfsstat() or {} for _, entry in ipairs(mnt_list) do if entry.dev and entry.mnt then mounts[entry.dev] = { path = entry.mnt, fstype = entry.type } -- Cache short name (e.g., ada0p2) without /dev/ prefix local short_dev = entry.dev:match("/dev/(.+)") if short_dev then mounts[short_dev] = { path = entry.mnt, fstype = entry.type } end end end return mounts end -- 3. Safely query S.M.A.R.T. temperature (Requires root privileges) local function get_disk_temp(disk_name) if not disk_name:match("^%w+$") then return "—" end local f = io.popen("/usr/local/sbin/smartctl -a /dev/" .. disk_name .. " 2>/dev/null") if not f then return "—" end local temp = nil for line in f:lines() do if line:match("^%s*194%s") or line:match("^%s*190%s") then local val = line:match("%s+(%d+)%s*$") or line:match("%s+(%d+)%s+%(.-%)%s*$") if val then temp = val; break end end end f:close() return temp and (temp .. " °C") or "—" end -- 4. Parse GEOM XML layout local function parse_geom_xml(xml_data) local disks = {} local raw_partitions = {} local clean_xml = xml_data:gsub("%s*\n%s*", "") for class_attr, class_node in clean_xml:gmatch("(.-)") do local class_name = class_node:match("(.-)") if class_name == "DISK" or class_name == "PART" then for geom_attr, geom_node in class_node:gmatch("(.-)") do for prov_attr, provider_node in geom_node:gmatch("(.-)") do local p_name = provider_node:match("(.-)") local p_size = provider_node:match("(.-)") if p_name then local size_gb = tonumber(p_size) and string.format("%.2f GB", p_size / 1024^3) or "unknown" if class_name == "DISK" then local p_descr = provider_node:match("(.-)") or "Generic Drive" disks[p_name] = { name = p_name, size = size_gb, model = p_descr, partitions = {} } elseif class_name == "PART" then table.insert(raw_partitions, { name = p_name, size = size_gb }) end end end end end end for _, part in ipairs(raw_partitions) do for disk_name, disk_info in pairs(disks) do if part.name:sub(1, #disk_name) == disk_name and part.name ~= disk_name then table.insert(disk_info.partitions, part) break end end end return disks end -- Main Output Render local function main() local xml_data = get_geom_xml() local mounts = get_mounts() local devices = parse_geom_xml(xml_data) print(string.format("%-14s %-12s %-8s %-8s %-30s %s", "Device", "Size", "Temp.", "FS", "Model / Description", "Mount Point")) print(string.rep("-", 115)) local sorted_disks = {} for k in pairs(devices) do table.insert(sorted_disks, k) end table.sort(sorted_disks) for _, disk_name in ipairs(sorted_disks) do local disk = devices[disk_name] local disk_mnt_info = mounts[disk.name] local disk_path = disk_mnt_info and disk_mnt_info.path or "" local disk_fs = disk_mnt_info and disk_mnt_info.fstype or "—" local disk_temp = get_disk_temp(disk.name) print(string.format("📦 %-11s %-12s %-8s %-8s %-30s %s", disk.name, disk.size, disk_temp, disk_fs, disk.model, disk_path)) table.sort(disk.partitions, function(a, b) return a.name < b.name end) for _, part in ipairs(disk.partitions) do local part_mnt_info = mounts[part.name] if part_mnt_info then print(string.format(" ├── 💾 %-6s %-12s %-8s %-8s %-30s %s", part.name, part.size, "—", part_mnt_info.fstype, "—", part_mnt_info.path)) end end end end main() ``` #### Running Without Root Privileges (Recommended Secure Setup) By default, `smartctl` requires elevated access to read disk nodes in `/dev/`. However, you can securely run this script as a normal user by delegating device permissions via FreeBSD `devfs` rules. 1. Add the following ruleset to your `/etc/devfs.rules` (create the file if it doesn't exist) to grant read/write access to the `operator` group for SATA and NVMe drives: ```ini [localrules=10] add path 'ada*' mode 0660 group operator add path 'da*' mode 0660 group operator add path 'nvd*' mode 0660 group operator ``` 2. Enable the ruleset in `/etc/rc.conf`: ```sh sysrc devfs_system_ruleset="localrules" ``` 3. Restart the `devfs` service to apply changes immediately: ```sh sudo service devfs restart ``` 4. Ensure your active user belongs to the `operator` group: ```sh # Add user to the group if needed sudo pw groupmod operator -m your_username ``` Now you can run the topology script safely without `sudo`: ```bash chmod +x disks.lua ./disks.lua ``` ### Expected Output Structure: ```text Device Size Temp. FS Model / Description Mount Point ------------------------------------------------------------------------------------------------------------------- 📦 ada0 465.76 GB 32 °C — Samsung SSD 860 EVO 500GB ├── 💾 ada0p2 63.75 GB — ufs — / ├── 💾 ada0p4 256.00 GB — ufs — /usr 📦 ada1 1863.02 GB 38 °C — ST2000NM000A-2J2100 ├── 💾 ada1p1 1863.00 GB — ufs — /home 📦 ada2 223.57 GB 30 °C — INTEL SSDSC2BB240G7 ``` ## License This project is licensed under the BSD 2-Clause License.