firehol/netdata

View on GitHub
src/go/plugin/go.d/modules/zfspool/collect_zpool_list_vdev.go

Summary

Maintainability
B
4 hrs
Test Coverage
// SPDX-License-Identifier: GPL-3.0-or-later

package zfspool

import (
    "bufio"
    "bytes"
    "fmt"
    "strings"
)

type vdevEntry struct {
    name   string
    vdev   string // The full path of the vdev within the zpool hierarchy.
    health string

    // Represents the nesting level of the vdev within the zpool hierarchy, based on indentation.
    // A level of -1 indicates the root vdev (the pool itself).
    level int
}

func (z *ZFSPool) collectZpoolListVdev(mx map[string]int64) error {
    seen := make(map[string]bool)

    for pool := range z.seenZpools {
        bs, err := z.exec.listWithVdev(pool)
        if err != nil {
            return err
        }

        vdevs, err := parseZpoolListVdevOutput(bs)
        if err != nil {
            return fmt.Errorf("bad zpool list vdev output (pool '%s'): %v", pool, err)
        }

        for _, vdev := range vdevs {
            if vdev.health == "" || vdev.health == "-" {
                continue
            }

            seen[vdev.vdev] = true
            if !z.seenVdevs[vdev.vdev] {
                z.seenVdevs[vdev.vdev] = true
                z.addVdevCharts(pool, vdev.vdev)
            }

            px := fmt.Sprintf("vdev_%s_", vdev.vdev)

            for _, s := range zpoolHealthStates {
                mx[px+"health_state_"+s] = 0
            }
            mx[px+"health_state_"+vdev.health] = 1
        }
    }

    for name := range z.seenVdevs {
        if !seen[name] {
            z.removeVdevCharts(name)
            delete(z.seenVdevs, name)
        }
    }

    return nil
}

func parseZpoolListVdevOutput(bs []byte) ([]vdevEntry, error) {
    var headers []string
    var vdevs []vdevEntry
    sc := bufio.NewScanner(bytes.NewReader(bs))

    for sc.Scan() {
        line := sc.Text()
        if line == "" {
            continue
        }

        if len(headers) == 0 {
            if !strings.HasPrefix(line, "NAME") {
                return nil, fmt.Errorf("missing headers (line '%s')", line)
            }
            headers = strings.Fields(line)
            continue
        }

        values := strings.Fields(line)
        if len(values) == 0 || len(values) > len(headers) {
            return nil, fmt.Errorf("unexpected columns: headers(%d)  values(%d) (line '%s')", len(headers), len(values), line)
        }

        vdev := vdevEntry{
            level: len(line) - len(strings.TrimLeft(line, " ")),
        }

        for i, v := range values {
            switch strings.ToLower(headers[i]) {
            case "name":
                vdev.name = v
            case "health":
                vdev.health = strings.ToLower(v)
            }
        }

        if vdev.name != "" {
            if len(vdevs) == 0 {
                vdev.level = -1 // Pool
            }
            vdevs = append(vdevs, vdev)
        }
    }

    // set parent/child relationships
    for i := range vdevs {
        v := &vdevs[i]

        switch i {
        case 0:
            v.vdev = v.name
        default:
            // find parent with a lower level
            for j := i - 1; j >= 0; j-- {
                if vdevs[j].level < v.level {
                    v.vdev = fmt.Sprintf("%s/%s", vdevs[j].vdev, v.name)
                    break
                }
            }
            if v.vdev == "" {
                return nil, fmt.Errorf("no parent for vdev '%s'", v.name)
            }
        }
    }

    // first is Pool
    if len(vdevs) < 2 {
        return nil, fmt.Errorf("no vdevs found")
    }

    return vdevs[1:], nil
}