From f5050b11e6f8269224c7fa7edb88a67ee981cc0a Mon Sep 17 00:00:00 2001 From: Peter Fern Date: Sat, 14 Aug 2021 10:56:12 +1000 Subject: [PATCH] feat(zfs): Add local ZFS CLI parsing Preparing for removal of go-zfs dependency. --- zfs/dataset.go | 72 ++++++++++++++++++++++++++++++++++++++++++++ zfs/pool.go | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++ zfs/zfs.go | 50 +++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 zfs/dataset.go create mode 100644 zfs/pool.go create mode 100644 zfs/zfs.go diff --git a/zfs/dataset.go b/zfs/dataset.go new file mode 100644 index 0000000..f27d803 --- /dev/null +++ b/zfs/dataset.go @@ -0,0 +1,72 @@ +package zfs + +import ( + "strings" +) + +// DatasetKind enum of supported dataset types +type DatasetKind string + +const ( + // DatasetFilesystem enum entry + DatasetFilesystem DatasetKind = `filesystem` + // DatasetVolume enum entry + DatasetVolume DatasetKind = `volume` + // DatasetSnapshot enum entry + DatasetSnapshot DatasetKind = `snapshot` +) + +// Dataset holds the properties for an individual dataset +type Dataset struct { + Pool string + Name string + Properties map[string]string +} + +// DatasetProperties returns the requested properties for all datasets in the given pool +func DatasetProperties(pool string, kind DatasetKind, properties ...string) ([]Dataset, error) { + handler := newDatasetHandler() + if err := execute(pool, handler, `zfs`, `get`, `-Hprt`, string(kind), `-o`, `name,property,value`, strings.Join(properties, `,`)); err != nil { + return nil, err + } + return handler.datasets(), nil +} + +// datasetHandler handles parsing of the data returned from the CLI into Dataset structs +type datasetHandler struct { + store map[string]Dataset +} + +// processLine implements the handler interface +func (h *datasetHandler) processLine(pool string, line []string) error { + if len(line) != 3 { + return ErrInvalidOutput + } + if _, ok := h.store[line[0]]; !ok { + h.store[line[0]] = newDataset(pool, line[0]) + } + h.store[line[0]].Properties[line[1]] = line[2] + return nil +} + +func (h *datasetHandler) datasets() []Dataset { + result := make([]Dataset, len(h.store)) + i := 0 + for _, dataset := range h.store { + result[i] = dataset + i++ + } + return result +} + +func newDataset(pool string, name string) Dataset { + return Dataset{ + Pool: pool, + Name: name, + Properties: make(map[string]string), + } +} + +func newDatasetHandler() *datasetHandler { + return &datasetHandler{store: make(map[string]Dataset)} +} diff --git a/zfs/pool.go b/zfs/pool.go new file mode 100644 index 0000000..f3c446e --- /dev/null +++ b/zfs/pool.go @@ -0,0 +1,81 @@ +package zfs + +import ( + "bufio" + "os/exec" + "strings" +) + +// PoolStatus enum contains status text +type PoolStatus string + +const ( + // PoolOnline enum entry + PoolOnline PoolStatus = `ONLINE` + // PoolDegraded enum entry + PoolDegraded PoolStatus = `DEGRADED` + // PoolFaulted enum entry + PoolFaulted PoolStatus = `FAULTED` + // PoolOffline enum entry + PoolOffline PoolStatus = `OFFLINE` + // PoolUnavail enum entry + PoolUnavail PoolStatus = `UNAVAIL` + // PoolRemoved enum entry + PoolRemoved PoolStatus = `REMOVED` +) + +// Pool holds the properties for an individual pool +type Pool struct { + Name string + Properties map[string]string +} + +// processLine implements the handler interface +func (p Pool) processLine(pool string, line []string) error { + if len(line) != 3 || line[0] != pool { + return ErrInvalidOutput + } + p.Properties[line[1]] = line[2] + + return nil +} + +// PoolNames returns a list of available pool names +func PoolNames() ([]string, error) { + pools := make([]string, 0) + cmd := exec.Command(`zpool`, `list`, `-Ho`, `name`) + out, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + scanner := bufio.NewScanner(out) + + if err = cmd.Start(); err != nil { + return nil, err + } + + for scanner.Scan() { + pools = append(pools, scanner.Text()) + } + if err = cmd.Wait(); err != nil { + return nil, err + } + + return pools, nil +} + +// PoolProperties returns the requested properties for the given pool +func PoolProperties(pool string, properties ...string) (Pool, error) { + handler := newPool(pool) + if err := execute(pool, handler, `zpool`, `get`, `-Hpo`, `name,property,value`, strings.Join(properties, `,`)); err != nil { + return handler, err + } + return handler, nil +} + +func newPool(name string) Pool { + return Pool{ + Name: name, + Properties: make(map[string]string), + } +} diff --git a/zfs/zfs.go b/zfs/zfs.go new file mode 100644 index 0000000..514ce41 --- /dev/null +++ b/zfs/zfs.go @@ -0,0 +1,50 @@ +package zfs + +import ( + "encoding/csv" + "errors" + "io" + "os/exec" +) + +var ( + // ErrInvalidOutput is returned on unparseable CLI output + ErrInvalidOutput = errors.New(`Invalid output executing command`) +) + +type handler interface { + processLine(pool string, line []string) error +} + +func execute(pool string, h handler, cmd string, args ...string) error { + c := exec.Command(cmd, append(args, pool)...) + out, err := c.StdoutPipe() + if err != nil { + return err + } + + r := csv.NewReader(out) + r.Comma = '\t' + r.LazyQuotes = true + r.ReuseRecord = true + r.FieldsPerRecord = 3 + + if err = c.Start(); err != nil { + return err + } + + for { + line, err := r.Read() + if err == io.EOF { + break + } + if err != nil { + return err + } + if err = h.processLine(pool, line); err != nil { + return err + } + } + + return c.Wait() +}