malp/bin/malpd
2026-04-13 23:04:21 -04:00

181 lines
4.2 KiB
Ruby
Executable File

#!/usr/bin/env ruby
require "socket"
require "fileutils"
require "json"
SOCKET_PATH = "/run/malpd/malpd.sock"
def human_capacity(bytes)
units = ["B", "KB", "MB", "GB", "TB", "PB"]
i = 0
size = bytes.to_f
while size >= 1000 && i < units.length - 1
size /= 1000
i += 1
end
precision = i <= 3 ? 0 : 1
"%g %s" % [size.round(precision), units[i]]
end
def drive_type(name)
return "nvme" if name.start_with?("nvme")
rotational = File.read("/sys/block/#{name}/queue/rotational").strip
rotational == "1" ? "hdd" : "ssd"
end
def read_smart(name)
out = `smartctl -j -A -H /dev/#{name} 2>/dev/null`
return nil if out.empty?
data = JSON.parse(out) rescue nil
return nil unless data
result = {}
passed = data.dig("smart_status", "passed")
result["passed"] = passed unless passed.nil?
if (t = data.dig("temperature", "current"))
result["temperature"] = t
end
if (poh = data.dig("power_on_time", "hours"))
result["power_on_hours"] = poh
end
if name.start_with?("nvme")
log = data["nvme_smart_health_information_log"] || {}
result["percentage_used"] = log["percentage_used"] if log.key?("percentage_used")
result["media_errors"] = log["media_errors"] if log.key?("media_errors")
result["critical_warning"] = log["critical_warning"] if log.key?("critical_warning")
else
attrs = data.dig("ata_smart_attributes", "table") || []
attrs.each do |a|
raw = a.dig("raw", "value")
next if raw.nil?
case a["id"]
when 5 then result["reallocated_sectors"] = raw
when 10 then result["spin_retry"] = raw
when 197 then result["pending_sectors"] = raw
when 199 then result["crc_errors"] = raw
end
end
end
result
end
def read_filesystems
out = `findmnt -J -b -o SOURCE,TARGET,FSTYPE,SIZE,USED 2>/dev/null`
return [] if out.empty?
data = JSON.parse(out) rescue nil
return [] unless data
skip_fstypes = %w[tmpfs devtmpfs efivarfs]
result = []
seen = {}
stack = Array(data["filesystems"]).dup
until stack.empty?
node = stack.shift
stack.concat(node["children"]) if node["children"]
source = node["source"].to_s
fstype = node["fstype"].to_s
target = node["target"].to_s
next unless source.start_with?("/dev/")
next if skip_fstypes.include?(fstype)
size = node["size"].to_i
next if size <= 0
key = [source, target]
next if seen[key]
seen[key] = true
result << {
"type" => "filesystem",
"source" => source,
"mount" => target,
"fstype" => fstype,
"size" => size,
"used" => node["used"].to_i
}
end
result
end
def handle_info(conn)
info = []
Dir.glob("/sys/block/*").each do |block_path|
name = File.basename(block_path)
next unless name.match?(/^(sd[a-z]+|nvme\d+n\d+)$/)
device_path = File.realpath("#{block_path}/device") rescue next
next if device_path.include?("/usb")
model = (File.read("#{block_path}/device/model").strip rescue "Unknown")
bytes = File.read("#{block_path}/size").strip.to_i * 512
entry = {
"type" => "drive",
"drivetype" => drive_type(name),
"name" => name,
"model" => model,
"capacity" => human_capacity(bytes)
}
if (smart = read_smart(name))
entry["smart"] = smart
end
info << entry
end
info.concat(read_filesystems)
if conn
conn.puts JSON.generate(info)
else
puts JSON.pretty_generate(info)
end
end
if ARGV.include?("-t")
handle_info(nil)
exit
end
if Process.uid != 0
$stderr.puts "malpd must be run as root"
exit(1)
end
if ENV["LISTEN_FDS"]
server = UNIXServer.for_fd(3)
else
FileUtils.mkdir_p(File.dirname(SOCKET_PATH))
FileUtils.rm_f(SOCKET_PATH)
server = UNIXServer.new(SOCKET_PATH)
File.chmod(0660, SOCKET_PATH)
FileUtils.chown(nil, "apache", SOCKET_PATH)
end
Signal.trap("TERM") { server.close }
Signal.trap("INT") { server.close }
loop do
conn = server.accept
request = conn.gets&.chomp
begin
case request
when nil
# empty request
when "info"
handle_info(conn)
else
conn.puts "unknown request: #{request}"
end
rescue => e
$stderr.puts "error handling request: #{e.message}"
ensure
conn.close
end
rescue IOError
break
end