| package zfs |
| |
| import ( |
| "bytes" |
| "errors" |
| "fmt" |
| "io" |
| "os/exec" |
| "regexp" |
| "runtime" |
| "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 { |
| var err error |
| |
| if len(line) != len(dsPropList) { |
| return errors.New("Output does not match what is expected on this platform") |
| } |
| setString(&ds.Name, line[0]) |
| setString(&ds.Origin, line[1]) |
| |
| if err = setUint(&ds.Used, line[2]); err != nil { |
| return err |
| } |
| if err = setUint(&ds.Avail, line[3]); err != nil { |
| return err |
| } |
| |
| setString(&ds.Mountpoint, line[4]) |
| setString(&ds.Compression, line[5]) |
| setString(&ds.Type, line[6]) |
| |
| if err = setUint(&ds.Volsize, line[7]); err != nil { |
| return err |
| } |
| if err = setUint(&ds.Quota, line[8]); err != nil { |
| return err |
| } |
| |
| if runtime.GOOS == "solaris" { |
| return nil |
| } |
| |
| if err = setUint(&ds.Written, line[9]); err != nil { |
| return err |
| } |
| if err = setUint(&ds.Logicalused, line[10]); err != nil { |
| return err |
| } |
| if err = setUint(&ds.Usedbydataset, line[11]); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| /* |
| * 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{"list", "-rHp", "-t", t, "-o", dsPropListOptions} |
| |
| 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 "name": |
| setString(&z.Name, val) |
| 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) |
| case "fragmentation": |
| // Trim trailing "%" before parsing uint |
| err = setUint(&z.Fragmentation, val[:len(val)-1]) |
| case "readonly": |
| z.ReadOnly = val == "on" |
| case "freeing": |
| err = setUint(&z.Freeing, val) |
| case "leaked": |
| err = setUint(&z.Leaked, val) |
| case "dedupratio": |
| // Trim trailing "x" before parsing float64 |
| z.DedupRatio, err = strconv.ParseFloat(val[:len(val)-1], 64) |
| } |
| return err |
| } |