1 changed files with 219 additions and 1 deletions
@ -1,3 +1,221 @@ |
|||
# lua-sysmount |
|||
|
|||
A lightweight, high-performance, and secure Lua binding for the native FreeBSD getfsstat(2) system call. |
|||
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("<class(.-)>(.-)</class>") do |
|||
local class_name = class_node:match("<name>(.-)</name>") |
|||
if class_name == "DISK" or class_name == "PART" then |
|||
for geom_attr, geom_node in class_node:gmatch("<geom(.-)>(.-)</geom>") do |
|||
for prov_attr, provider_node in geom_node:gmatch("<provider(.-)>(.-)</provider>") do |
|||
local p_name = provider_node:match("<name>(.-)</name>") |
|||
local p_size = provider_node:match("<mediasize>(.-)</mediasize>") |
|||
|
|||
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("<descr>(.-)</descr>") 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() |
|||
``` |
|||
|
|||
Run the script with **root privileges** to ensure S.M.A.R.T. temperatures are displayed: |
|||
```bash |
|||
sudo chmod +x disks.lua |
|||
sudo ./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. |
|||
|
|||
Reference in new issue