mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-04-15 07:16:34 +00:00
Analyze block devices
This commit is contained in:
18
examples/preflight/host-block-devices.yaml
Normal file
18
examples/preflight/host-block-devices.yaml
Normal 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
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
183
pkg/analyze/host_block_devices.go
Normal file
183
pkg/analyze/host_block_devices.go
Normal 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
|
||||
}
|
||||
175
pkg/analyze/host_block_devices_test.go
Normal file
175
pkg/analyze/host_block_devices_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
76
pkg/collect/host_block_device.go
Normal file
76
pkg/collect/host_block_device.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user