зеркало из https://github.com/Azure/ARO-RP.git
355 строки
7.6 KiB
Go
355 строки
7.6 KiB
Go
package zfs
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/google/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().String()
|
|
joinedArgs := strings.Join(cmd.Args, " ")
|
|
|
|
logger.Log([]string{"ID:" + id, "START", joinedArgs})
|
|
if err := cmd.Run(); err != nil {
|
|
return nil, &Error{
|
|
Err: err,
|
|
Debug: strings.Join([]string{cmd.Path, joinedArgs[1:]}, " "),
|
|
Stderr: stderr.String(),
|
|
}
|
|
}
|
|
logger.Log([]string{"ID:" + id, "FINISH"})
|
|
|
|
// 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 (d *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(&d.Name, line[0])
|
|
setString(&d.Origin, line[1])
|
|
|
|
if err = setUint(&d.Used, line[2]); err != nil {
|
|
return err
|
|
}
|
|
if err = setUint(&d.Avail, line[3]); err != nil {
|
|
return err
|
|
}
|
|
|
|
setString(&d.Mountpoint, line[4])
|
|
setString(&d.Compression, line[5])
|
|
setString(&d.Type, line[6])
|
|
|
|
if err = setUint(&d.Volsize, line[7]); err != nil {
|
|
return err
|
|
}
|
|
if err = setUint(&d.Quota, line[8]); err != nil {
|
|
return err
|
|
}
|
|
if err = setUint(&d.Referenced, line[9]); err != nil {
|
|
return err
|
|
}
|
|
|
|
if runtime.GOOS == "solaris" {
|
|
return nil
|
|
}
|
|
|
|
if err = setUint(&d.Written, line[10]); err != nil {
|
|
return err
|
|
}
|
|
if err = setUint(&d.Logicalused, line[11]); err != nil {
|
|
return err
|
|
}
|
|
return setUint(&d.Usedbydataset, line[12])
|
|
}
|
|
|
|
/*
|
|
* 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: %w", 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) // nolint:ifshort // llen *is* actually used
|
|
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: %w", 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: %w", err)
|
|
}
|
|
case Modified:
|
|
if llen == 4 {
|
|
referenceCount, err = parseReferenceCount(line[3])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse reference count: %w", err)
|
|
}
|
|
}
|
|
default:
|
|
newPath = ""
|
|
}
|
|
|
|
return &InodeChange{
|
|
Change: changeType,
|
|
Type: inodeType,
|
|
Path: path,
|
|
NewPath: newPath,
|
|
ReferenceCountChange: referenceCount,
|
|
}, nil
|
|
}
|
|
|
|
// example input for parseInodeChanges
|
|
// 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: %w, 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 := zfsOutput(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
|
|
i := strings.Index(val, "%")
|
|
if i < 0 {
|
|
i = len(val)
|
|
}
|
|
err = setUint(&z.Fragmentation, val[:i])
|
|
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
|
|
}
|