Skip to content

Commit

Permalink
feat: better error handling and warning propagation (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
jakobmoellerdev authored Aug 7, 2024
1 parent 859e373 commit 62cf99c
Show file tree
Hide file tree
Showing 19 changed files with 315 additions and 129 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Display Go version
run: go version
- name: Run tests
run: sudo go test ./...
run: sudo go test -v ./... -test.count=5
- name: Run simple example
run: sudo go run examples/simple/main.go
- name: Run activation example
Expand Down
27 changes: 19 additions & 8 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ import (
)

func TestLVs(t *testing.T) {
SkipOrFailTestIfNotRoot(t)
ctx := WithCustomEnvironment(context.Background(), map[string]string{})
t.Parallel()
slog.SetDefault(slog.New(NewContextPropagatingSlogHandler(NewTestingHandler(t))))
slog.SetLogLoggerLevel(slog.LevelDebug)

SkipOrFailTestIfNotRoot(t)
for i, tc := range []test{
{
LoopDevices: []Size{
Expand Down Expand Up @@ -79,9 +80,12 @@ func TestLVs(t *testing.T) {
},
} {
t.Run(fmt.Sprintf("[%v]%s", i, tc.String()), func(t *testing.T) {
SkipOrFailTestIfNotRoot(t)
t.Parallel()
ctx := WithCustomEnvironment(context.Background(), map[string]string{})
ctx, cancel := context.WithCancel(ctx)
defer cancel()

SkipOrFailTestIfNotRoot(t)
clnt := GetTestClient(ctx)
infra := tc.SetupDevicesAndVolumeGroup(t)

Expand Down Expand Up @@ -118,12 +122,19 @@ func TestLVs(t *testing.T) {
t.Fatalf("Expected volume group %s, got %s", infra.volumeGroup.Name, vg.Name)
}

pvs, err := clnt.PVs(ctx, infra.volumeGroup.Name)
if err != nil {
t.Fatal(err)
var pvs []*PhysicalVolume
success := false
for attempt := 0; attempt < 3; attempt++ {
if pvs, err = clnt.PVs(ctx, infra.volumeGroup.Name); err != nil {
t.Logf("failed to get physical volumes: %s", err)
}
if len(pvs) != len(infra.loopDevices) {
t.Logf("%s expected %d physical volumes, got %d", t.Name(), len(infra.loopDevices), len(pvs))
}
success = true
}
if len(pvs) != len(infra.loopDevices) {
t.Fatalf("Expected %d physical volumes, got %d", len(infra.loopDevices), len(pvs))
if !success {
t.Fatalf("failed to get physical volumes: %s", err)
}

for _, pv := range pvs {
Expand Down
10 changes: 6 additions & 4 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func Test_DecodeConfig(t *testing.T) {
}

func Test_EncodeDecode(t *testing.T) {
t.Parallel()
SkipOrFailTestIfNotRoot(t)
slog.SetDefault(slog.New(NewContextPropagatingSlogHandler(NewTestingHandler(t))))
slog.SetLogLoggerLevel(slog.LevelDebug)
Expand All @@ -145,7 +146,7 @@ func Test_EncodeDecode(t *testing.T) {
t.Fatalf("profile dir is empty even though that was not expected")
}

profileName := "lvm2go-test"
profileName := "lvm2go-test-encode-decode"

testFile, err := os.OpenFile(filepath.Join(c.Config.ProfileDir, fmt.Sprintf("%s.profile", profileName)), os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
Expand All @@ -167,7 +168,7 @@ func Test_EncodeDecode(t *testing.T) {
c = &structConfig{}
if err := clnt.ReadAndDecodeConfig(ctx, c, ConfigTypeFull, Profile(profileName)); err == nil {
t.Fatalf("expected error due no customizable profile")
} else if !IsLVMErrConfigurationSectionNotCustomizableByProfile(err) {
} else if !IsConfigurationSectionNotCustomizableByProfile(err) {
t.Fatalf("expected error due no customizable profile, but got %v", err)
}
}
Expand Down Expand Up @@ -252,13 +253,14 @@ func TestGetProfilePath(t *testing.T) {
}

func TestProfile(t *testing.T) {
t.Parallel()
SkipOrFailTestIfNotRoot(t)
slog.SetDefault(slog.New(NewContextPropagatingSlogHandler(NewTestingHandler(t))))
slog.SetLogLoggerLevel(slog.LevelDebug)
ctx := context.Background()
clnt := GetTestClient(ctx)

profile := Profile("lvm2go-test")
profile := Profile("lvm2go-test-profile")

type structConfig struct {
Config struct {
Expand All @@ -283,7 +285,7 @@ func TestProfile(t *testing.T) {
}

err = clnt.ReadAndDecodeConfig(ctx, c, ConfigTypeFull, profile)
if !IsLVMErrConfigurationSectionNotCustomizableByProfile(err) {
if !IsConfigurationSectionNotCustomizableByProfile(err) {
t.Fatalf("expected error due no customizable profile, but got %v", err)
}
}
Expand Down
27 changes: 7 additions & 20 deletions exit_code_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
package lvm2go

import (
"bytes"
"errors"
"fmt"
)

// AsExitCodeError returns the ExitCodeError from the error if it exists and a bool indicating if is an ExitCodeError or not.
Expand All @@ -35,42 +33,31 @@ func AsExitCodeError(err error) (ExitCodeError, bool) {
type ExitCodeError interface {
error
ExitCode() int
Unwrap() error
}

// NewExitCodeError returns a new ExitCodeError with the provided error and stderr output.
func NewExitCodeError(err error, stderr []byte) ExitCodeError {
return &exitCodeErr{
err: err,
stderr: stderr,
func NewExitCodeError(err error) ExitCodeError {
if err == nil {
return nil
}
return &exitCodeErr{error: err}
}

// exitCodeErr is an implementation of ExitCodeError storing the original error and the stderr output of the lvmBinaryPath command.
// It also provides a POSIX exit code that can be used to determine the type of error from LVM.
type exitCodeErr struct {
err error
stderr []byte
}

func (e *exitCodeErr) Error() string {
if e.stderr != nil {
return fmt.Sprintf("%v: %v", e.err, string(bytes.TrimSpace(e.stderr)))
}
return e.err.Error()
error
}

func (e *exitCodeErr) Unwrap() error {
return e.err
}
var _ ExitCodeError = &exitCodeErr{}

func (e *exitCodeErr) ExitCode() int {
type exitError interface {
ExitCode() int
error
}
var err exitError
if errors.As(e.err, &err) {
if errors.As(e.error, &err) {
return err.ExitCode()
}
return -1
Expand Down
18 changes: 7 additions & 11 deletions losetup.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,24 +285,20 @@ func (dev *loopbackDevice) SetBackingFile(file string) error {
return ErrDeviceAlreadyClosed
}

ctx, cancel := context.WithTimeout(context.Background(), dev.commandTimeout)
defer cancel()

if err := dev.setFile(file); err != nil {
return err
}

if _, err := os.Stat(dev.file); err == nil {
return fmt.Errorf("backing file %s already exists", dev.file)
} else if !os.IsNotExist(err) {
return fmt.Errorf("failed to check for backing file existence %s: %w", dev.file, err)
fd, err := os.OpenFile(dev.file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("failed to open or create backing file %s: %w", dev.file, err)
}
defer fd.Close()

args := []string{fmt.Sprintf("--size=%v", uint64(dev.size.Val)), dev.file}
out, err := exec.CommandContext(ctx, "truncate", args...).CombinedOutput()
if err != nil {
return fmt.Errorf("failed to truncate backing file: %w: %s", err, string(out))
if err := fd.Truncate(int64(dev.size.Val)); err != nil {
return fmt.Errorf("failed to truncate backing file %s to size %v: %w", dev.file, dev.size.Val, err)
}

return nil
}

Expand Down
94 changes: 59 additions & 35 deletions lvm_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@
package lvm2go

import (
"fmt"
"regexp"
"slices"
)

var (
// NotFoundPattern is a regular expression that matches the error message when a volume group or logical volume is not found.
// The volume group might not be present or the logical volume might not be present in the volume group.
NotFoundPattern = regexp.MustCompile(`Volume group "(.*?)" not found|Failed to find logical volume "(.*?)"`)

// NotFoundPatterns are regular expressions that matches the error message when a device, volume group or logical volume is not found.

volumeGroupNotFoundPattern = `Volume group "(.*?)" not found`
VolumeGroupNotFoundPattern = regexp.MustCompile(volumeGroupNotFoundPattern)
logicalVolumeNotFoundPattern = `Failed to find logical volume "(.*?)"`
LogicalVolumeNotFoundPattern = regexp.MustCompile(logicalVolumeNotFoundPattern)
deviceNotFoundPattern = `Couldn't find device with uuid (.{6}-.{4}-.{4}-.{4}-.{4}-.{4}-.{6})`
DeviceNotFoundPattern = regexp.MustCompile(deviceNotFoundPattern)
NotFoundPattern = regexp.MustCompile(fmt.Sprintf(`%s|%s|%s`, volumeGroupNotFoundPattern, logicalVolumeNotFoundPattern, deviceNotFoundPattern))

// NoSuchCommandPattern is a regular expression that matches the error message when a command is not found.
NoSuchCommandPattern = regexp.MustCompile(`no such command`)
Expand Down Expand Up @@ -66,71 +73,88 @@ var (
// Example:
//
// func IsLVMCustomError(err error) bool {
// return IsLVMError(err, regexp.MustCompile(`custom error pattern`), 5)
// return IsLVMError(err, regexp.MustCompile(`custom error pattern`))
// }
func IsLVMError(err error, pattern *regexp.Regexp, validExitCodes ...int) bool {
func IsLVMError(err error, pattern *regexp.Regexp) bool {
if err == nil {
return false
}
lvmErr, ok := AsExitCodeError(err)
if !ok || !slices.Contains(validExitCodes, lvmErr.ExitCode()) {
return false

if stdErr, ok := AsLVMStdErr(err); ok {
for _, line := range stdErr.Lines(true) {
if pattern.Match(line) {
return true
}
}
}
return pattern.Match([]byte(lvmErr.Error()))

return false
}

func IsNotFound(err error) bool {
return IsLVMError(err, NotFoundPattern)
}

func IsVolumeGroupNotFound(err error) bool {
return IsLVMError(err, VolumeGroupNotFoundPattern)
}

func IsLogicalVolumeNotFound(err error) bool {
return IsLVMError(err, LogicalVolumeNotFoundPattern)
}

func IsLVMErrNotFound(err error) bool {
return IsLVMError(err, NotFoundPattern, 5)
func IsDeviceNotFound(err error) bool {
return IsLVMError(err, DeviceNotFoundPattern)
}

func IsLVMErrNoSuchCommand(err error) bool {
return IsLVMError(err, NoSuchCommandPattern, 2)
func IsNoSuchCommand(err error) bool {
return IsLVMError(err, NoSuchCommandPattern)
}

func IsLVMErrMaximumLogicalVolumesReached(err error) bool {
return IsLVMError(err, MaximumNumberOfLogicalVolumesPattern, 5)
func IsMaximumLogicalVolumesReached(err error) bool {
return IsLVMError(err, MaximumNumberOfLogicalVolumesPattern)
}

func IsLVMErrMaximumPhysicalVolumesReached(err error) bool {
return IsLVMError(err, MaximumNumberOfPhysicalVolumesPattern, 5)
func IsMaximumPhysicalVolumesReached(err error) bool {
return IsLVMError(err, MaximumNumberOfPhysicalVolumesPattern)
}

func IsLVMErrVGImmutableDueToMissingPVs(err error) bool {
return IsLVMError(err, CannotChangeVGWhilePVsAreMissingPattern, 5)
func IsVGImmutableDueToMissingPVs(err error) bool {
return IsLVMError(err, CannotChangeVGWhilePVsAreMissingPattern)
}

func IsLVMCouldNotFindDeviceWithUUID(err error) bool {
return IsLVMError(err, CouldNotFindDeviceWithUUIDPattern, 5)
func IsCouldNotFindDeviceWithUUID(err error) bool {
return IsLVMError(err, CouldNotFindDeviceWithUUIDPattern)
}

func IsLVMErrVGMissingPVs(err error) bool {
return IsLVMError(err, VGMissingPVsPattern, 5, 3)
func IsVGMissingPVs(err error) bool {
return IsLVMError(err, VGMissingPVsPattern)
}

func LVMErrVGMissingPVsDetails(err error) (vg string, pv string, lastWrittenTo string, ok bool) {
func VGMissingPVsDetails(err error) (vg string, pv string, lastWrittenTo string, ok bool) {
submatches := VGMissingPVsPattern.FindStringSubmatch(err.Error())
if submatches == nil {
return "", "", "", false
}
return submatches[1], submatches[2], submatches[3], true
}

func IsLVMPartialLVNeedsRepairOrRemove(err error) bool {
return IsLVMError(err, PartialLVNeedsRepairOrRemovePattern, 5)
func IsPartialLVNeedsRepairOrRemove(err error) bool {
return IsLVMError(err, PartialLVNeedsRepairOrRemovePattern)
}

func IsLVMErrThereAreStillPartialLVs(err error) bool {
return IsLVMError(err, ThereAreStillPartialLVsPattern, 5)
func IsThereAreStillPartialLVs(err error) bool {
return IsLVMError(err, ThereAreStillPartialLVsPattern)
}

func IsLVMErrNoDataToMove(err error) bool {
return IsLVMError(err, NoDataToMovePattern, 5)
func IsNoDataToMove(err error) bool {
return IsLVMError(err, NoDataToMovePattern)
}

func IsLVMNoFreeExtents(err error) bool {
return IsLVMError(err, NoFreeExtentsPattern, 5)
func IsNoFreeExtents(err error) bool {
return IsLVMError(err, NoFreeExtentsPattern)
}

func IsLVMErrConfigurationSectionNotCustomizableByProfile(err error) bool {
return IsLVMError(err, ConfigurationSectionNotCustomizableByProfilePattern, 5)
func IsConfigurationSectionNotCustomizableByProfile(err error) bool {
return IsLVMError(err, ConfigurationSectionNotCustomizableByProfilePattern)
}
2 changes: 1 addition & 1 deletion lvmdevices_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (c *client) DevCheck(ctx context.Context, opts ...DevCheckOption) error {

return c.RunRaw(
ctx,
NoOpRawOutputProcessor(false),
NoOpRawOutputProcessor(),
append([]string{"lvmdevices", "--check"}, args.GetRaw()...)...,
)
}
Expand Down
2 changes: 1 addition & 1 deletion lvmdevices_modify.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func (c *client) DevModify(ctx context.Context, opts ...DevModifyOption) error {

return c.RunRaw(
ctx,
NoOpRawOutputProcessor(false),
NoOpRawOutputProcessor(),
append([]string{"lvmdevices"}, args.GetRaw()...)...,
)
}
Expand Down
2 changes: 1 addition & 1 deletion lvmdevices_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (c *client) DevUpdate(ctx context.Context, opts ...DevUpdateOption) error {

return c.RunRaw(
ctx,
NoOpRawOutputProcessor(false),
NoOpRawOutputProcessor(),
append([]string{"lvmdevices", "--update"}, args.GetRaw()...)...,
)
}
Expand Down
2 changes: 1 addition & 1 deletion lvs.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (c *client) LVs(ctx context.Context, opts ...LVsOption) ([]*LogicalVolume,

err = c.RunLVMInto(ctx, res, append(args, argsFromOpts.GetRaw()...)...)

if IsLVMErrNotFound(err) {
if IsNotFound(err) {
return nil, nil
}

Expand Down
Loading

0 comments on commit 62cf99c

Please sign in to comment.