Analyze block devices

This commit is contained in:
Andrew Reed
2021-02-11 04:15:27 +00:00
parent e2c0c722ae
commit 0bcd5183f5
9 changed files with 526 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostPreflight
metadata:
name: block
spec:
collectors:
- blockDevices: {}
analyzers:
- blockDevices:
outcomes:
- pass:
when: ".* == 1"
message: One available block device
- pass:
when: ".* > 1"
message: Multiple available block devices
- fail:
message: No available block devices

View File

@@ -90,6 +90,13 @@ func HostAnalyze(hostAnalyzer *troubleshootv1beta2.HostAnalyze, getFile getColle
}
return []*AnalyzeResult{result}, nil
}
if hostAnalyzer.BlockDevices != nil {
result, err := analyzeHostBlockDevices(hostAnalyzer.BlockDevices, getFile)
if err != nil {
return nil, err
}
return []*AnalyzeResult{result}, nil
}
return nil, errors.New("invalid analyzer")
}

View File

@@ -0,0 +1,183 @@
package analyzer
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
func analyzeHostBlockDevices(hostAnalyzer *troubleshootv1beta2.BlockDevicesAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
contents, err := getCollectedFileContents("system/block_devices.json")
if err != nil {
return nil, errors.Wrap(err, "failed to get collected file")
}
var devices []collect.BlockDeviceInfo
if err := json.Unmarshal(contents, &devices); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal block devices info")
}
result := AnalyzeResult{}
title := hostAnalyzer.CheckName
if title == "" {
title = "Block Devices"
}
result.Title = title
for _, outcome := range hostAnalyzer.Outcomes {
if outcome.Fail != nil {
if outcome.Fail.When == "" {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI
return &result, nil
}
isMatch, err := compareHostBlockDevicesConditionalToActual(outcome.Fail.When, devices)
if err != nil {
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When)
}
if isMatch {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI
return &result, nil
}
} else if outcome.Warn != nil {
if outcome.Warn.When == "" {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
return &result, nil
}
isMatch, err := compareHostBlockDevicesConditionalToActual(outcome.Warn.When, devices)
if err != nil {
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When)
}
if isMatch {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
return &result, nil
}
} else if outcome.Pass != nil {
if outcome.Pass.When == "" {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
return &result, nil
}
isMatch, err := compareHostBlockDevicesConditionalToActual(outcome.Pass.When, devices)
if err != nil {
return nil, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When)
}
if isMatch {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
return &result, nil
}
}
}
return &result, nil
}
// <regexp> <op> <count>
// example: sdb > 0
func compareHostBlockDevicesConditionalToActual(conditional string, devices []collect.BlockDeviceInfo) (res bool, err error) {
parts := strings.Split(conditional, " ")
if len(parts) != 3 {
return false, fmt.Errorf("Expected exactly 3 parts, got %d", len(parts))
}
rx, err := regexp.Compile(parts[0])
if err != nil {
return false, errors.Wrapf(err, "failed to compile regex %q", parts[0])
}
count := countEligibleBlockDevices(rx, devices)
desiredInt, err := strconv.Atoi(parts[2])
if err != nil {
return false, errors.Wrapf(err, "failed to parse desired quantity %q", parts[2])
}
switch parts[1] {
case ">":
return count > desiredInt, nil
case ">=":
return count >= desiredInt, nil
case "<":
return count < desiredInt, nil
case "<=":
return count <= desiredInt, nil
case "=", "==", "===":
return count == desiredInt, nil
}
return false, fmt.Errorf("Unexpected operator %q", parts[1])
}
func countEligibleBlockDevices(rx *regexp.Regexp, devices []collect.BlockDeviceInfo) int {
count := 0
for _, device := range devices {
if isEligibleBlockDevice(rx, device, devices) {
count++
}
}
return count
}
func isEligibleBlockDevice(rx *regexp.Regexp, device collect.BlockDeviceInfo, devices []collect.BlockDeviceInfo) bool {
if !rx.MatchString(device.Name) {
return false
}
if device.Type != "disk" {
return false
}
if device.Mountpoint != "" {
return false
}
if device.FilesystemType != "" {
return false
}
if device.ReadOnly {
return false
}
if device.Removable {
return false
}
for _, d := range devices {
if d.ParentKernelName == device.KernelName {
return false
}
}
return true
}

View File

@@ -0,0 +1,175 @@
package analyzer
import (
"encoding/json"
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAnalyzeBlockDevices(t *testing.T) {
tests := []struct {
name string
devices []collect.BlockDeviceInfo
hostAnalyzer *troubleshootv1beta2.BlockDevicesAnalyze
result *AnalyzeResult
expectErr bool
}{
{
name: "sdb == 1, pass when there is an empty /dev/sdb",
devices: []collect.BlockDeviceInfo{
{
Name: "sdb",
KernelName: "sdb",
Type: "disk",
Major: 8,
Serial: "disk1",
},
},
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "sdb == 1",
Message: "Block device available",
},
},
},
},
result: &AnalyzeResult{
Title: "Block Devices",
IsPass: true,
Message: "Block device available",
},
},
{
name: "sdb == 1, fail when partitioned",
devices: []collect.BlockDeviceInfo{
{
Name: "sdb",
KernelName: "sdb",
Type: "disk",
Major: 8,
Serial: "disk1",
},
{
Name: "sdb1",
KernelName: "sdb1",
ParentKernelName: "sdb",
Type: "part",
Major: 8,
Minor: 1,
},
},
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "sdb == 1",
Message: "Block device available",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "No block device available",
},
},
},
},
result: &AnalyzeResult{
Title: "Block Devices",
IsFail: true,
Message: "No block device available",
},
},
{
name: "sdb == 1, fail when it has a filesystem",
devices: []collect.BlockDeviceInfo{
{
Name: "sdb",
KernelName: "sdb",
Type: "disk",
Major: 8,
Serial: "disk1",
FilesystemType: "ext4",
},
},
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "sdb == 1",
Message: "Block device available",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "No block device available",
},
},
},
},
result: &AnalyzeResult{
Title: "Block Devices",
IsFail: true,
Message: "No block device available",
},
},
{
name: ".* > 0, fail when only loop devices are found",
devices: []collect.BlockDeviceInfo{
{
Name: "loop0",
KernelName: "loop0",
Type: "loop",
Major: 7,
},
},
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: ".* > 0",
Message: "Block device available",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "No block device available",
},
},
},
},
result: &AnalyzeResult{
Title: "Block Devices",
IsFail: true,
Message: "No block device available",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
b, err := json.Marshal(test.devices)
if err != nil {
t.Fatal(err)
}
getCollectedFileContents := func(filename string) ([]byte, error) {
return b, nil
}
result, err := analyzeHostBlockDevices(test.hostAnalyzer, getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
req.NoError(err)
}
assert.Equal(t, test.result, result)
})
}
}

View File

@@ -39,6 +39,11 @@ type TimeAnalyze struct {
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type BlockDevicesAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type HostAnalyze struct {
CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"`
//
@@ -53,4 +58,6 @@ type HostAnalyze struct {
HTTP *HTTPAnalyze `json:"http" yaml:"http"`
Time *TimeAnalyze `json:"time" yaml:"time"`
BlockDevices *BlockDevicesAnalyze `json:"blockDevices" yaml:"blockDevices"`
}

View File

@@ -63,6 +63,10 @@ type HostTime struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
}
type HostBlockDevices struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
}
type HostCollect struct {
CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"`
Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"`
@@ -74,6 +78,7 @@ type HostCollect struct {
DiskUsage *DiskUsage `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"`
HTTP *HostHTTP `json:"http,omitempty" yaml:"http,omitempty"`
Time *HostTime `json:"time,omitempty" yaml:"time,omitempty"`
BlockDevices *HostBlockDevices `json:"blockDevices,omitempty" yaml:"blockDevices,omitempty"`
}
func (c *HostCollect) GetName() string {

View File

@@ -287,6 +287,33 @@ func (in *AnalyzerStatus) DeepCopy() *AnalyzerStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BlockDevicesAnalyze) DeepCopyInto(out *BlockDevicesAnalyze) {
*out = *in
out.AnalyzeMeta = in.AnalyzeMeta
if in.Outcomes != nil {
in, out := &in.Outcomes, &out.Outcomes
*out = make([]*Outcome, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(Outcome)
(*in).DeepCopyInto(*out)
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BlockDevicesAnalyze.
func (in *BlockDevicesAnalyze) DeepCopy() *BlockDevicesAnalyze {
if in == nil {
return nil
}
out := new(BlockDevicesAnalyze)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CPU) DeepCopyInto(out *CPU) {
*out = *in
@@ -1108,6 +1135,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
*out = new(TimeAnalyze)
(*in).DeepCopyInto(*out)
}
if in.BlockDevices != nil {
in, out := &in.BlockDevices, &out.BlockDevices
*out = new(BlockDevicesAnalyze)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze.
@@ -1120,6 +1152,22 @@ func (in *HostAnalyze) DeepCopy() *HostAnalyze {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostBlockDevices) DeepCopyInto(out *HostBlockDevices) {
*out = *in
out.HostCollectorMeta = in.HostCollectorMeta
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostBlockDevices.
func (in *HostBlockDevices) DeepCopy() *HostBlockDevices {
if in == nil {
return nil
}
out := new(HostBlockDevices)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostCollect) DeepCopyInto(out *HostCollect) {
*out = *in
@@ -1173,6 +1221,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) {
*out = new(HostTime)
**out = **in
}
if in.BlockDevices != nil {
in, out := &in.BlockDevices, &out.BlockDevices
*out = new(HostBlockDevices)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect.

View File

@@ -0,0 +1,76 @@
package collect
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"os/exec"
"github.com/pkg/errors"
)
type BlockDeviceInfo struct {
Name string `json:"name"`
KernelName string `json:"kernel_name"`
ParentKernelName string `json:"parent_kernel_name"`
Type string `json:"type"`
Major int `json:"major"`
Minor int `json:"minor"`
Size uint64 `json:"size"`
FilesystemType string `json:"filesystem_type"`
Mountpoint string `json:"mountpoint"`
Serial string `json:"serial"`
ReadOnly bool `json:"read_only"`
Removable bool `json:"removable"`
}
const lsblkColumns = "NAME,KNAME,PKNAME,TYPE,MAJ:MIN,SIZE,FSTYPE,MOUNTPOINT,SERIAL,RO,RM"
const lsblkFormat = `NAME=%q KNAME=%q PKNAME=%q TYPE=%q MAJ:MIN="%d:%d" SIZE="%d" FSTYPE=%q MOUNTPOINT=%q SERIAL=%q RO="%d" RM="%d0"`
func HostBlockDevices(c *HostCollector) (map[string][]byte, error) {
var devices []BlockDeviceInfo
cmd := exec.Command("lsblk", "--noheadings", "--bytes", "--pairs", "-o", lsblkColumns)
stdout, err := cmd.Output()
if err != nil {
return nil, errors.Wrapf(err, "failed to execute lsblk")
}
buf := bytes.NewBuffer(stdout)
scanner := bufio.NewScanner(buf)
for scanner.Scan() {
bdi := BlockDeviceInfo{}
var ro int
var rm int
fmt.Sscanf(
scanner.Text(),
lsblkFormat,
&bdi.Name,
&bdi.KernelName,
&bdi.ParentKernelName,
&bdi.Type,
&bdi.Major,
&bdi.Minor,
&bdi.Size,
&bdi.FilesystemType,
&bdi.Mountpoint,
&bdi.Serial,
&ro,
&rm,
)
bdi.ReadOnly = ro == 1
bdi.Removable = rm == 1
devices = append(devices, bdi)
}
b, err := json.Marshal(devices)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal block device info")
}
return map[string][]byte{
"system/block_devices.json": b,
}, nil
}

View File

@@ -32,6 +32,8 @@ func (c *HostCollector) RunCollectorSync() (result map[string][]byte, err error)
result, err = HostHTTP(c)
} else if c.Collect.Time != nil {
result, err = HostTime(c)
} else if c.Collect.BlockDevices != nil {
result, err = HostBlockDevices(c)
} else {
err = errors.New("no spec found to run")
return