Skip to content

Commit

Permalink
feat(zfs): Add local ZFS CLI parsing
Browse files Browse the repository at this point in the history
Preparing for removal of go-zfs dependency.
  • Loading branch information
pdf committed Aug 14, 2021
1 parent d3a45ef commit f5050b1
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 0 deletions.
72 changes: 72 additions & 0 deletions zfs/dataset.go
Original file line number Diff line number Diff line change
@@ -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)}
}
81 changes: 81 additions & 0 deletions zfs/pool.go
Original file line number Diff line number Diff line change
@@ -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),
}
}
50 changes: 50 additions & 0 deletions zfs/zfs.go
Original file line number Diff line number Diff line change
@@ -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()
}

0 comments on commit f5050b1

Please sign in to comment.