From bea7e548f8779a09f0f49236b9d3262cf901bfb4 Mon Sep 17 00:00:00 2001 From: iBug Date: Thu, 4 Apr 2024 02:20:48 +0800 Subject: [PATCH] Add parser for PVE storage --- Makefile | 5 +- cmd/iolimit/iolimit.go | 8 ++- go.mod | 6 +- go.sum | 12 +++- pkg/pve/pct.go | 53 +++++++++++++++++- pkg/pve/storage.go | 121 ++++++++++++++++++++++++++++++++++++++++ pkg/pve/storage_test.go | 57 +++++++++++++++++++ 7 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 pkg/pve/storage.go create mode 100644 pkg/pve/storage_test.go diff --git a/Makefile b/Makefile index 9512b62..58190c6 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,15 @@ BIN := vct VERSION := $(shell git describe --tags --always --dirty) -.PHONY: all $(BIN) clean +.PHONY: all $(BIN) test clean all: $(BIN) $(BIN): go build -ldflags='-s -w -X main.version=$(VERSION)' +test: + go test -v ./... + clean: rm -f $(BIN) diff --git a/cmd/iolimit/iolimit.go b/cmd/iolimit/iolimit.go index 111e897..2303ab6 100644 --- a/cmd/iolimit/iolimit.go +++ b/cmd/iolimit/iolimit.go @@ -34,5 +34,11 @@ func MakeCmd() *cobra.Command { } func iolimitMain(ctid string, iops cgroup.IOPS) error { - return cgroup.SetIOPSForLXC(ctid, cgroup.IOPSLine{IOPS: iops}) + // TODO: Find major:minor + iopsline := cgroup.IOPSLine{ + Major: 0, + Minor: 0, + IOPS: iops, + } + return cgroup.SetIOPSForLXC(ctid, iopsline) } diff --git a/go.mod b/go.mod index 287ef9e..061bdff 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,14 @@ go 1.20 require ( github.com/ryanuber/columnize v2.1.2+incompatible github.com/spf13/cobra v1.8.0 - golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 + github.com/stretchr/testify v1.9.0 + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ceaf173..9c3aa01 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk= github.com/ryanuber/columnize v2.1.2+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -8,7 +12,11 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= -golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/pve/pct.go b/pkg/pve/pct.go index d154b80..ed98e37 100644 --- a/pkg/pve/pct.go +++ b/pkg/pve/pct.go @@ -1,8 +1,18 @@ package pve -import "os/exec" +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" + "strings" +) -const PctPath = "/usr/sbin/pct" +const ( + EtcPve = "/etc/pve" + PctPath = "/usr/sbin/pct" +) func PctCmd(args ...string) *exec.Cmd { return exec.Command(PctPath, args...) @@ -11,3 +21,42 @@ func PctCmd(args ...string) *exec.Cmd { func StopCmd(vmid string) *exec.Cmd { return PctCmd("stop", vmid) } + +func parseConfig(r io.Reader) map[string]string { + config := make(map[string]string) + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if comment, ok := strings.CutPrefix(line, "#"); ok { + config["#"] += comment + "\n" + continue + } + fields := strings.Fields(line) + if len(fields) != 2 { + // Invalid line? + continue + } + config[fields[0]] = fields[1] + } + return config +} + +func GetConfig(typ, vmid string) (map[string]string, error) { + if typ == "qemu" { + typ = "qemu-server" + } + f, err := os.Open(fmt.Sprintf("%s/%s/%s.cfg", EtcPve, typ, vmid)) + if err != nil { + return nil, err + } + defer f.Close() + return parseConfig(f), nil +} + +func GetLXCConfig(vmid string) (map[string]string, error) { + return GetConfig("lxc", vmid) +} + +func GetQemuConfig(vmid string) (map[string]string, error) { + return GetConfig("qemu", vmid) +} diff --git a/pkg/pve/storage.go b/pkg/pve/storage.go new file mode 100644 index 0000000..6912845 --- /dev/null +++ b/pkg/pve/storage.go @@ -0,0 +1,121 @@ +package pve + +import ( + "bufio" + "fmt" + "io" + "io/fs" + "os" + "slices" + "strings" + "syscall" +) + +const StorageConf = "/etc/pve/storage.cfg" + +type PVEStorage struct { + Type string + Name string + Attr map[string]string +} + +func newPVEStorage() PVEStorage { + return PVEStorage{ + Attr: make(map[string]string), + } +} + +func parseStorage(r io.Reader) []PVEStorage { + items := make([]PVEStorage, 0) + item := newPVEStorage() + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "#") { + continue + } + fields := strings.Fields(line) + if len(fields) != 2 { + // Invalid line? + continue + } + if strings.HasSuffix(fields[0], ":") { + if item.Type != "" { + items = append(items, item) + item = newPVEStorage() + } + item.Type = strings.TrimSuffix(fields[0], ":") + item.Name = fields[1] + } else { + item.Attr[fields[0]] = fields[1] + } + } + if item.Type != "" { + items = append(items, item) + } + return items +} + +func GetStorage() ([]PVEStorage, error) { + f, err := os.Open(StorageConf) + if os.IsNotExist(err) { + return nil, nil + } else if err != nil { + return nil, err + } + defer f.Close() + return parseStorage(f), nil +} + +func devMajorMinor(device uint64) (major, minor uint64) { + major = (device >> 8) & 0xfff + minor = (device & 0xff) | ((device >> 12) & 0xfff00) + return +} + +func getBlockDevForDir(dir string) (uint64, uint64, error) { + fileInfo, err := os.Lstat(dir) + if err != nil { + return 0, 0, err + } + stat, ok := fileInfo.Sys().(*syscall.Stat_t) + if !ok { + return 0, 0, fmt.Errorf("failed to get backing device for %s: %w", dir, err) + } + major, minor := devMajorMinor(stat.Dev) + return major, minor, nil +} + +func getBlockDevForLVM(vgname, lvname string) (uint64, uint64, error) { + fileInfo, err := os.Stat(fmt.Sprintf("/dev/%s/%s", vgname, lvname)) + if err != nil { + return 0, 0, err + } + stat, ok := fileInfo.Sys().(*syscall.Stat_t) + if !ok { + return 0, 0, fmt.Errorf("failed to get device number for %s/%s: %w", vgname, lvname, err) + } + major, minor := devMajorMinor(stat.Rdev) + return major, minor, nil +} + +// Finds the block device for the given storage and name. +// The "aux" parameter is used to help determine the type of the storage. +func GetBlockDevForStorage(storage, name string, aux []PVEStorage) (uint64, uint64, error) { + i := slices.IndexFunc(aux, func(s PVEStorage) bool { + return s.Name == name + }) + if i == -1 { + return 0, 0, fs.ErrNotExist + } + info := aux[i] + switch info.Type { + case "dir": + return getBlockDevForDir(storage) + case "lvm", "lvmthin": + vgname := info.Attr["vgname"] + return getBlockDevForLVM(vgname, name) + default: + return 0, 0, fmt.Errorf("unsupported storage type %s", info.Type) + } +} diff --git a/pkg/pve/storage_test.go b/pkg/pve/storage_test.go new file mode 100644 index 0000000..c60b874 --- /dev/null +++ b/pkg/pve/storage_test.go @@ -0,0 +1,57 @@ +package pve + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +const testStorageCfg = `dir: local + path /var/lib/vz + content vztmpl,iso,images + +lvmthin: local-lvm + thinpool data + vgname pve + content rootdir,images + +dir: nfs-template + path /mnt/vz + content vztmpl,iso,images + shared 1 +` + +var testStorage = []PVEStorage{ + { + Type: "dir", + Name: "local", + Attr: map[string]string{ + "path": "/var/lib/vz", + "content": "vztmpl,iso,images", + }, + }, + { + Type: "lvmthin", + Name: "local-lvm", + Attr: map[string]string{ + "thinpool": "data", + "vgname": "pve", + "content": "rootdir,images", + }, + }, + { + Type: "dir", + Name: "nfs-template", + Attr: map[string]string{ + "path": "/mnt/vz", + "content": "vztmpl,iso,images", + "shared": "1", + }, + }, +} + +func TestParseStorage(t *testing.T) { + as := assert.New(t) + as.Equal(testStorage, parseStorage(strings.NewReader(testStorageCfg))) +}