package zfs import ( "bytes" "fmt" "io" "os/exec" "regexp" "strconv" "strings" "github.com/pborman/uuid" ) type command struct { Command string Stdin io.Reader Stdout io.Writer } func (c *command) Run(arg ...string) ([][]string, error) { cmd := exec.Command(c.Command, arg...) var stdout, stderr bytes.Buffer if c.Stdout == nil { cmd.Stdout = &stdout } else { cmd.Stdout = c.Stdout } if c.Stdin != nil { cmd.Stdin = c.Stdin } cmd.Stderr = &stderr id := uuid.New() joinedArgs := strings.Join(cmd.Args, " ") logger.Log([]string{"ID:" + id, "START", joinedArgs}) err := cmd.Run() logger.Log([]string{"ID:" + id, "FINISH"}) if err != nil { return nil, &Error{ Err: err, Debug: strings.Join([]string{cmd.Path, joinedArgs}, " "), Stderr: stderr.String(), } } // assume if you passed in something for stdout, that you know what to do with it if c.Stdout != nil { return nil, nil } lines := strings.Split(stdout.String(), "\n") //last line is always blank lines = lines[0 : len(lines)-1] output := make([][]string, len(lines)) for i, l := range lines { output[i] = strings.Fields(l) } return output, nil } func setString(field *string, value string) { v := "" if value != "-" { v = value } *field = v } func setUint(field *uint64, value string) error { var v uint64 if value != "-" { var err error v, err = strconv.ParseUint(value, 10, 64) if err != nil { return err } } *field = v return nil } func (ds *Dataset) parseLine(line []string) error { prop := line[1] val := line[2] var err error switch prop { case "available": err = setUint(&ds.Avail, val) case "compression": setString(&ds.Compression, val) case "mountpoint": setString(&ds.Mountpoint, val) case "quota": err = setUint(&ds.Quota, val) case "type": setString(&ds.Type, val) case "origin": setString(&ds.Origin, val) case "used": err = setUint(&ds.Used, val) case "volsize": err = setUint(&ds.Volsize, val) case "written": err = setUint(&ds.Written, val) case "logicalused": err = setUint(&ds.Logicalused, val) } return err } /* * from zfs diff`s escape function: * * Prints a file name out a character at a time. If the character is * not in the range of what we consider "printable" ASCII, display it * as an escaped 3-digit octal value. ASCII values less than a space * are all control characters and we declare the upper end as the * DELete character. This also is the last 7-bit ASCII character. * We choose to treat all 8-bit ASCII as not printable for this * application. */ func unescapeFilepath(path string) (string, error) { buf := make([]byte, 0, len(path)) llen := len(path) for i := 0; i < llen; { if path[i] == '\\' { if llen < i+4 { return "", fmt.Errorf("Invalid octal code: too short") } octalCode := path[(i + 1):(i + 4)] val, err := strconv.ParseUint(octalCode, 8, 8) if err != nil { return "", fmt.Errorf("Invalid octal code: %v", err) } buf = append(buf, byte(val)) i += 4 } else { buf = append(buf, path[i]) i++ } } return string(buf), nil } var changeTypeMap = map[string]ChangeType{ "-": Removed, "+": Created, "M": Modified, "R": Renamed, } var inodeTypeMap = map[string]InodeType{ "B": BlockDevice, "C": CharacterDevice, "/": Directory, ">": Door, "|": NamedPipe, "@": SymbolicLink, "P": EventPort, "=": Socket, "F": File, } // matches (+1) or (-1) var referenceCountRegex = regexp.MustCompile("\\(([+-]\\d+?)\\)") func parseReferenceCount(field string) (int, error) { matches := referenceCountRegex.FindStringSubmatch(field) if matches == nil { return 0, fmt.Errorf("Regexp does not match") } return strconv.Atoi(matches[1]) } func parseInodeChange(line []string) (*InodeChange, error) { llen := len(line) if llen < 1 { return nil, fmt.Errorf("Empty line passed") } changeType := changeTypeMap[line[0]] if changeType == 0 { return nil, fmt.Errorf("Unknown change type '%s'", line[0]) } switch changeType { case Renamed: if llen != 4 { return nil, fmt.Errorf("Mismatching number of fields: expect 4, got: %d", llen) } case Modified: if llen != 4 && llen != 3 { return nil, fmt.Errorf("Mismatching number of fields: expect 3..4, got: %d", llen) } default: if llen != 3 { return nil, fmt.Errorf("Mismatching number of fields: expect 3, got: %d", llen) } } inodeType := inodeTypeMap[line[1]] if inodeType == 0 { return nil, fmt.Errorf("Unknown inode type '%s'", line[1]) } path, err := unescapeFilepath(line[2]) if err != nil { return nil, fmt.Errorf("Failed to parse filename: %v", err) } var newPath string var referenceCount int switch changeType { case Renamed: newPath, err = unescapeFilepath(line[3]) if err != nil { return nil, fmt.Errorf("Failed to parse filename: %v", err) } case Modified: if llen == 4 { referenceCount, err = parseReferenceCount(line[3]) if err != nil { return nil, fmt.Errorf("Failed to parse reference count: %v", err) } } default: newPath = "" } return &InodeChange{ Change: changeType, Type: inodeType, Path: path, NewPath: newPath, ReferenceCountChange: referenceCount, }, nil } // example input //M / /testpool/bar/ //+ F /testpool/bar/hello.txt //M / /testpool/bar/hello.txt (+1) //M / /testpool/bar/hello-hardlink func parseInodeChanges(lines [][]string) ([]*InodeChange, error) { changes := make([]*InodeChange, len(lines)) for i, line := range lines { c, err := parseInodeChange(line) if err != nil { return nil, fmt.Errorf("Failed to parse line %d of zfs diff: %v, got: '%s'", i, err, line) } changes[i] = c } return changes, nil } func listByType(t, filter string) ([]*Dataset, error) { args := []string{"get", "-rHp", "-t", t, "all"} if filter != "" { args = append(args, filter) } out, err := zfs(args...) if err != nil { return nil, err } var datasets []*Dataset name := "" var ds *Dataset for _, line := range out { if name != line[0] { name = line[0] ds = &Dataset{Name: name} datasets = append(datasets, ds) } if err := ds.parseLine(line); err != nil { return nil, err } } return datasets, nil } func propsSlice(properties map[string]string) []string { args := make([]string, 0, len(properties)*3) for k, v := range properties { args = append(args, "-o") args = append(args, fmt.Sprintf("%s=%s", k, v)) } return args } func (z *Zpool) parseLine(line []string) error { prop := line[1] val := line[2] var err error switch prop { case "health": setString(&z.Health, val) case "allocated": err = setUint(&z.Allocated, val) case "size": err = setUint(&z.Size, val) case "free": err = setUint(&z.Free, val) } return err }