# 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.