first draft

master
Denis Knauf 2023-10-13 11:28:46 +02:00
commit d623efde64
4 changed files with 350 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.sw[pomnqrst]
*.gem
Gemfile.lock
.rake_tasks

5
Gemfile Normal file
View File

@ -0,0 +1,5 @@
source 'https://rubygems.org'
gem 'ruby-lxc', git: 'https://git.denkn.at/deac/ruby-lxc'
gem 'prometheus-client'
gem 'puma'
gem 'rack'

14
config.ru Normal file
View File

@ -0,0 +1,14 @@
require 'rack'
require_relative 'pvect-exporter'
require 'prometheus/client/formats/text'
run lambda {|env|
req = Rack::Request.new env
case req.path
when "/metrics"
collector = PveCtCollector.new
[200, {"Content-Type" => "text/plain"}, [Prometheus::Client::Formats::Text.marshal( collector.collect)]]
else
[404, {"Content-Type" => "text/plain"}, ["Not found\nYou want to try /metrics?\n"]]
end
}

327
pvect-exporter.rb Normal file
View File

@ -0,0 +1,327 @@
#!/usr/bin/env ruby
require 'pathname'
require 'fiddle'
require 'fiddle/import'
require 'json'
require 'lxc'
require 'prometheus/client'
=begin
class LXC::CT
def initialize name
extend Fiddle::Importer
dlload Fiddle.dlopen( nil)
extern 'lxc_container *lxc_container_new( const char* name, const char* config_path)'
def initialize name, config = nil
lxc_container_new name, config
end
end
end
=end
class File
module Statfs
Magics = {}
#=begin
Line = <<~EOF
ADFS 0xadf5
AFFS 0xadff
AFS 0x5346414F
AUTOFS 0x0187
CODA 0x73757245
CRAMFS 0x28cd3d45
CRAMFS_WEND 0x453dcd28
DEBUGFS 0x64626720
SECURITYFS 0x73636673
SELINUX 0xf97cff8c
SMACK 0x43415d53
RAMFS 0x858458f6
TMPFS 0x01021994
HUGETLBFS 0x958458f6
SQUASHFS 0x73717368
ECRYPTFS 0xf15f
EFS 0x414A53
EROFS_V1 0xE0F5E1E2
EXT2 0xEF53
EXT3 0xEF53
XENFS 0xabba1974
EXT4 0xEF53
BTRFS 0x9123683E
NILFS 0x3434
F2FS 0xF2F52010
HPFS 0xf995e849
ISOFS 0x9660
JFFS2 0x72b6
XFS 0x58465342
PSTOREFS 0x6165676C
EFIVARFS 0xde5e81e4
HOSTFS 0x00c0ffee
OVERLAYFS 0x794c7630
MINIX 0x137F
MINIX2 0x138F
MINIX2 0x2468
MINIX22 0x2478
MINIX3 0x4d5a
MSDOS 0x4d44
NCP 0x564c
NFS 0x6969
OCFS2 0x7461636f
OPENPROM 0x9fa1
QNX4 0x002f
QNX6 0x68191122
AFS_FS 0x6B414653
REISERFS 0x52654973
REISERFS "ReIsErFs"
REISER2FS "ReIsEr2Fs"
REISER2FS_JR "ReIsEr3Fs"
SMB 0x517B
CGROUP 0x27e0eb
CGROUP2 0x63677270
RDTGROUP 0x7655821
STACK_END 0x57AC6E9D
TRACEFS 0x74726163
V9FS 0x01021997
BDEVFS 0x62646576
DAXFS 0x64646178
BINFMTFS 0x42494e4d
DEVPTS 0x1cd1
BINDERFS 0x6c6f6f70
FUTEXFS 0xBAD1DEA
PIPEFS 0x50495045
PROC 0x9fa0
SOCKFS 0x534F434B
SYSFS 0x62656572
USBDEVICE 0x9fa2
MTD_INODE_FS 0x11307854
ANON_INODE_FS 0x09041934
BTRFS_TEST 0x73727279
NSFS 0x6e736673
BPF_FS 0xcafe4a11
AAFS 0x5a3c69f0
ZONEFS 0x5a4f4653
UDF 0x15013346
BALLOON_KVM 0x13661366
ZSMALLOC 0x58295829
DMA_BUF 0x444d4142
DEVMEM 0x454d444d
Z3FOLD 0x33
PPC_CMM 0xc7571590
EOF
Line.each_line do |l|
n, v = l.split(/\s+/)[0..1]
v =
case v
when /^0x(\h+)$/ then $1.to_i 16
when /^"([^"]+)"$/ then $1
else raise "Unknown value: #$1"
end
Magics[v] = n.downcase.to_sym
end
#=end
extend Fiddle::Importer
dlload Fiddle.dlopen( nil)
Struct_statfs = struct <<~EOS
long type,
long bsize,
unsigned long blocks,
unsigned long bfree,
unsigned long bavail,
unsigned long files,
unsigned long ffree,
int fsid[2],
long namelen,
long frsize,
long flags,
long spare[4]
EOS
class Struct_statfs
def fstypename
Magics[type]
end
def to_h
{type: type, fstypename: fstypename, bsize: bsize, blocks: blocks, bfree: bfree, bavail: bavail, files: files, ffree: ffree, fsid: fsid, namelen: namelen, frsize: frsize, flags: flags, spare: spare}
end
end
StatfsFun = Fiddle::Function.new handler['statfs'], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT
#extern 'int statfs( const char *__file, struct statfs *__buf)'
def self.new file
buf = Struct_statfs.malloc
val = StatfsFun.call file, buf
raise SystemCallError.new( "statfs(#{file.inspect})", Fiddle.last_error) unless val == 0
buf
end
end
end
class NS
#extend FFI::Library
#ffi_lib 'c'
#attach_function :setns_without_exception_handling, :setns, %i[int int], :int
extend Fiddle::Importer
dlload Fiddle.dlopen( nil)
SetNS_Function = Fiddle::Function.new handler['setns'], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT
class <<self
def setns fd, t
#r = setns_without_exception_handling fd.to_i, t.to_i
#SystemCallError.new "setns(#{fd.to_i})", FFI::LastError.error if -1 == r
r = SetNS_Function.call fd.to_i, t.to_i
SystemCallError.new "setns(#{fd.to_i})", Fiddle.last_error if -1 == r
r
end
def _open_nsfd pid, type
File.open "/proc/%d/ns/%s" % [pid, type]
end
def open pid, *types, &exe
ns = new pid, *types
if block_given?
begin yield ns
ensure ns.close
end
else ns
end
end
def change pid, *types, &exe
open pid, *types do |ns|
ns.change &exe
end
end
end
attr_reader :pid, :fds, :types
def initialize pid, *types
@pid, @fds = pid, {}
types.map {|t| @fds[t] = self.class._open_nsfd pid, t }
end
def close
@fds.each {|t, fd| fd.close }
end
def change &block
@owns = @fds.map {|t, _f| Own[t] }
begin
@fds.each {|type, fd| self.class.setns fd, 0 }
yield
ensure
@owns.each {|fd| self.class.setns fd, 0 }
end
end
Own = Hash.new {|h, ns| h[ns] = _open_nsfd $$, ns }
end
class MountInfo
attr_reader :major, :minor, :mp, :dev, :type, :opts
# 4979 4977 0:213 / /proc rw,nosuid,nodev,noexec,relatime shared:2416 - proc proc rw
def initialize *args
i = args.find_index {|x| '-' == x }
args2 = args[(i+1)..-1]
ma, mi = args[2].split ':'
@major, @minor, @mp, @dev, @type, @opts =
ma.to_i, mi.to_i, args[4], args2[1], args2[0], args2[2].split( ',')
end
class << self
def parse line
new *line.split( ' ')
end
def of pid
Pathname.new( "/proc/#{pid}/mountinfo").readlines.map {|l| parse l }
end
end
end
=begin
LxcStartExe = Pathname.new "/usr/bin/lxc-start"
def get_running_ct_pids &block
return to_enum( __method__) unless block_given?
cts = []
Pathname.new( "/proc").each_child do |fn|
next if /\D/ =~ fn.basename.to_s
exepath = fn + 'exe'
next unless exepath.exist? and LxcStartExe == exepath.readlink
pid = fn.basename.to_s.to_i
cts.push pid
end
Pathname.new( "/proc").each_child do |fn|
next if /\D/ =~ fn.basename.to_s
statuspath = fn + 'status'
ppid = statuspath.read.match( /^PPid:\s+(\d+)$/)[1].to_i
next unless cts.include? ppid
yield fn.basename.to_s.to_i
end
end
pids = get_running_ct_pids
=end
class PveCtCollector
def fetch
LXC.list_containers.map {|n|LXC::Container.new n}.select( &:running?).each do |ct|
pid = ct.init_pid
mis = MountInfo.of pid
NS.change pid, :pid, :mnt do
mis.each do |mi|
next if %w[devpts devtmpfs fuse.lxcfs].include? mi.type
statfs = File::Statfs.new mi.mp
yield ct, mi.mp, statfs
end
end
end
end
def initialize registry = nil
@prometheus ||= Prometheus::Client::Registry.new
@info = Prometheus::Client::Gauge.new :pvect_filesystem_info, labels: %i[id mountpoint name fstype], docstring: 'CT Info'
@totalb = Prometheus::Client::Gauge.new :pvect_filesystem_total_bytes, labels: %i[id mountpoint], docstring: 'Total bytes in filesystem'
@freeb = Prometheus::Client::Gauge.new :pvect_filesystem_free_bytes, labels: %i[id mountpoint], docstring: 'Free bytes in filesystem'
@availb = Prometheus::Client::Gauge.new :pvect_filesystem_available_bytes, labels: %i[id mountpoint], docstring: 'Free bytes available to unprivileged user'
@totalf = Prometheus::Client::Gauge.new :pvect_filesystem_total_files, labels: %i[id mountpoint], docstring: 'Total files in filesystem'
@freef = Prometheus::Client::Gauge.new :pvect_filesystem_free_files, labels: %i[id mountpoint], docstring: 'Free files in filesystem'
[@info, @totalb, @freeb, @availb, @totalf, @freef].each {|g| @prometheus.register g }
end
def collect
rd, wr = IO.pipe
pid = fork do
rd.close
fetch do |ct, mp, st|
next unless st.fstypename and 0 < st.blocks
wr.puts st.to_h.merge( mp: mp, id: ct.name, name: ct.config_item( 'lxc.uts.name')).to_json
end
wr.close
exit 0
end
wr.close
rd.each_line do |l|
st = OpenStruct.new JSON.parse( l)
labels = {id: st.id, mountpoint: st.mp}
@info.set 1, labels: labels.merge( name: st.name, fstype: st.fstypename)
@totalb.set st.bsize*st.blocks, labels: labels
@freeb.set st.bsize*st.bfree, labels: labels
@availb.set st.bsize*st.bavail, labels: labels
@totalf.set st.files, labels: labels
@freef.set st.ffree, labels: labels
end
rd.close
Process.wait pid
@prometheus
end
end