#!/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 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 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