Add virtual machines list
This commit is contained in:
parent
23008b4997
commit
5773347bda
@ -182,8 +182,9 @@
|
||||
@media (max-width: 500px) { .grid { grid-template-columns: 1fr; } }
|
||||
|
||||
/* span modifiers */
|
||||
.span-2 { grid-column: span 2; }
|
||||
.span-3 { grid-column: span 3; }
|
||||
.span-2 { grid-column: span 2; }
|
||||
.span-3 { grid-column: span 3; }
|
||||
.span-all { grid-column: 1 / -1; }
|
||||
|
||||
.card:hover { border-color: #2d3748; }
|
||||
|
||||
|
||||
93
bin/malpd
93
bin/malpd
@ -115,6 +115,98 @@ def read_filesystems
|
||||
result
|
||||
end
|
||||
|
||||
def parse_mem(s)
|
||||
return 0 unless s
|
||||
n, unit = s.split
|
||||
mult = case unit
|
||||
when "KiB" then 1024
|
||||
when "MiB" then 1024 ** 2
|
||||
when "GiB" then 1024 ** 3
|
||||
when "TiB" then 1024 ** 4
|
||||
else 1
|
||||
end
|
||||
n.to_i * mult
|
||||
end
|
||||
|
||||
def read_vm_dominfo(name)
|
||||
out = `virsh --connect qemu:///system dominfo #{Shellwords.escape(name)} 2>/dev/null`
|
||||
info = {}
|
||||
out.each_line do |line|
|
||||
key, value = line.split(":", 2).map { |s| s&.strip }
|
||||
next unless key && value
|
||||
case key
|
||||
when "State" then info["state"] = value
|
||||
when "CPU(s)" then info["vcpus"] = value.to_i
|
||||
when "Max memory" then info["memory"] = parse_mem(value)
|
||||
when "Used memory" then info["used_memory"] = parse_mem(value)
|
||||
when "Autostart" then info["autostart"] = (value == "enable")
|
||||
end
|
||||
end
|
||||
info
|
||||
end
|
||||
|
||||
def read_vm_disks(name)
|
||||
out = `virsh --connect qemu:///system domblklist #{Shellwords.escape(name)} --details 2>/dev/null`
|
||||
targets = []
|
||||
out.each_line do |line|
|
||||
parts = line.strip.split(/\s+/)
|
||||
next if parts.size < 3
|
||||
next if parts[0] == "Type" || parts[0].start_with?("-")
|
||||
next unless parts[1] == "disk"
|
||||
targets << parts[2]
|
||||
end
|
||||
total_cap = 0
|
||||
total_alloc = 0
|
||||
targets.each do |tgt|
|
||||
blk = `virsh --connect qemu:///system domblkinfo #{Shellwords.escape(name)} #{Shellwords.escape(tgt)} --bytes 2>/dev/null`
|
||||
blk.each_line do |l|
|
||||
k, v = l.split(":", 2).map { |s| s&.strip }
|
||||
next unless k && v
|
||||
total_cap += v.to_i if k == "Capacity"
|
||||
total_alloc += v.to_i if k == "Allocation"
|
||||
end
|
||||
end
|
||||
{ "disk_total" => total_cap, "disk_used" => total_alloc }
|
||||
end
|
||||
|
||||
def read_vm_os(name)
|
||||
xml = `virsh --connect qemu:///system dumpxml #{Shellwords.escape(name)} 2>/dev/null`
|
||||
return nil if xml.empty?
|
||||
if xml =~ %r{<libosinfo:os\s+id="([^"]+)"}
|
||||
path = $1.sub(%r{^https?://[^/]+/}, "")
|
||||
parts = path.split("/").reject(&:empty?)
|
||||
return parts.map { |p| p.sub(/^\w/, &:upcase) }.join(" ") unless parts.empty?
|
||||
end
|
||||
if xml =~ %r{<title>([^<]+)</title>}
|
||||
return $1.strip
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def read_virtual_machines
|
||||
list_out = `virsh --connect qemu:///system list --all --name 2>/dev/null`
|
||||
return [] unless $?.success?
|
||||
names = list_out.split("\n").map(&:strip).reject(&:empty?)
|
||||
return [] if names.empty?
|
||||
|
||||
names.map do |name|
|
||||
dom = read_vm_dominfo(name)
|
||||
disk = read_vm_disks(name)
|
||||
{
|
||||
"type" => "vm",
|
||||
"name" => name,
|
||||
"state" => dom["state"],
|
||||
"vcpus" => dom["vcpus"],
|
||||
"memory" => dom["memory"],
|
||||
"used_memory" => dom["used_memory"],
|
||||
"autostart" => dom["autostart"],
|
||||
"os" => read_vm_os(name),
|
||||
"disk_total" => disk["disk_total"],
|
||||
"disk_used" => disk["disk_used"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(conn)
|
||||
info = []
|
||||
|
||||
@ -142,6 +234,7 @@ def handle_info(conn)
|
||||
end
|
||||
|
||||
info.concat(read_filesystems)
|
||||
info.concat(read_virtual_machines)
|
||||
|
||||
if conn
|
||||
conn.puts JSON.generate(info)
|
||||
|
||||
@ -109,6 +109,87 @@ if cgi.params.key?("content")
|
||||
exit
|
||||
end
|
||||
|
||||
drives = info.select { |e| e["type"] == "drive" }
|
||||
filesystems = info.select { |e| e["type"] == "filesystem" }
|
||||
vms = info.select { |e| e["type"] == "vm" }
|
||||
|
||||
if vms.any?
|
||||
running = vms.count { |v| v["state"].to_s == "running" }
|
||||
total = vms.size
|
||||
hdr_cls = running == total ? "ok" : running == 0 ? "bad" : "warn"
|
||||
|
||||
html << %(<div class="span-3">)
|
||||
html << %(<div class="section-label">Virtual Machines</div>)
|
||||
html << <<~HTML
|
||||
<div class="card #{hdr_cls}">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Libvirt</span>
|
||||
<span class="badge #{hdr_cls}">#{running} / #{total} Running</span>
|
||||
</div>
|
||||
<div class="subitems">
|
||||
<div style="display:grid;grid-template-columns:1fr 110px 1.2fr 100px;gap:0.5rem;padding:0 0.6rem;font-size:0.68rem;color:#334155;text-transform:uppercase;letter-spacing:0.06em;font-weight:600;">
|
||||
<span>VM</span>
|
||||
<span style="text-align:right;">Resources</span>
|
||||
<span>Disk</span>
|
||||
<span style="text-align:right;">State</span>
|
||||
</div>
|
||||
HTML
|
||||
|
||||
vms.each do |vm|
|
||||
name = CGI.escapeHTML(vm["name"].to_s)
|
||||
os = vm["os"] ? CGI.escapeHTML(vm["os"]) : nil
|
||||
state = vm["state"].to_s
|
||||
state_cls = case state
|
||||
when "running" then "ok"
|
||||
when "paused", "in shutdown", "pmsuspended" then "warn"
|
||||
when "shut off", "crashed" then "bad"
|
||||
else "warn"
|
||||
end
|
||||
state_label = CGI.escapeHTML(state.split.map(&:capitalize).join(" "))
|
||||
|
||||
vcpus = vm["vcpus"].to_i
|
||||
mem_b = vm["memory"].to_i
|
||||
res = vcpus > 0 || mem_b > 0 ? "#{vcpus} CPU#{vcpus > 1 ? "s" : ""} · #{human_capacity(mem_b)}" : "—"
|
||||
|
||||
cap = vm["disk_total"].to_i
|
||||
used = vm["disk_used"].to_i
|
||||
dpct_f = cap > 0 ? (used * 100.0 / cap) : 0.0
|
||||
dpct = dpct_f.round
|
||||
disk_cls = dpct_f >= 90 ? "bad" : dpct_f >= 75 ? "warn" : "ok"
|
||||
|
||||
disk_html = if cap > 0
|
||||
%(<div>
|
||||
<div style="font-size:0.72rem;color:#94a3b8;">#{dpct}% · #{CGI.escapeHTML(human_capacity(used))} / #{CGI.escapeHTML(human_capacity(cap))}</div>
|
||||
<div class="bar-track" style="margin-top:0.25rem;"><div class="bar-fill #{disk_cls}" style="width:#{dpct}%"></div></div>
|
||||
</div>)
|
||||
else
|
||||
%(<div><span style="font-size:0.72rem;color:#475569;">—</span></div>)
|
||||
end
|
||||
|
||||
os_html = os ? %(<div style="font-size:0.67rem;color:#475569;">#{os}</div>) : ""
|
||||
|
||||
html << <<~HTML
|
||||
<div class="subitem" style="flex-direction:column;align-items:stretch;gap:0.35rem;">
|
||||
<div style="display:grid;grid-template-columns:1fr 110px 1.2fr 100px;gap:0.5rem;align-items:center;">
|
||||
<div class="subitem-left">
|
||||
<div class="dot #{state_cls}"></div>
|
||||
<div>
|
||||
<div class="subitem-name">#{name}</div>
|
||||
#{os_html}
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:right;"><span class="subitem-value ok">#{CGI.escapeHTML(res)}</span></div>
|
||||
#{disk_html}
|
||||
<div style="text-align:right;"><span class="subitem-value #{state_cls}">#{state_label}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
HTML
|
||||
end
|
||||
|
||||
html << %(</div></div>)
|
||||
html << %(</div>)
|
||||
end
|
||||
|
||||
thousands = ->(n) { n.to_s.gsub(/(\d)(?=(\d{3})+$)/, '\1,') }
|
||||
|
||||
drive_stats = lambda do |dt, smart|
|
||||
@ -157,9 +238,6 @@ if cgi.params.key?("content")
|
||||
stats
|
||||
end
|
||||
|
||||
drives = info.select { |e| e["type"] == "drive" }
|
||||
filesystems = info.select { |e| e["type"] == "filesystem" }
|
||||
|
||||
if drives.any?
|
||||
html << %(<div class="span-2">)
|
||||
html << %(<div class="section-label">Storage Drives</div>)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user