From b3a3405e4026577e5b17e563ab50440be1899b2f Mon Sep 17 00:00:00 2001 From: jakobmoellerdev Date: Sun, 28 Jul 2024 20:59:53 +0200 Subject: [PATCH] feat: vgchange and pointer returns --- args.go | 2 ++ client.go | 31 +++++++++++++++++++-- client_test.go | 10 +------ config_test.go | 1 + json_convert_helper.go | 4 ++- logical_volume.go | 20 ++++++++++++-- lv_attr_test.go | 1 + lvchange.go | 2 +- lvextend_test.go | 1 + lvmdevices_test.go | 1 + lvrename_test.go | 1 + lvs.go | 38 ++++++++++++++++++++++++-- physical_volume.go | 3 ++ pvs.go | 22 +++++++-------- size_test.go | 1 + tags.go | 7 +++++ util_test.go | 8 +++++- vgchange.go | 34 +++++++++++++++++++++-- vgchange_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++ vgextend_test.go | 13 ++------- vgrename_test.go | 1 + vgs.go | 28 +++++++++++++++++-- volume_group.go | 13 ++++++++- 23 files changed, 257 insertions(+), 47 deletions(-) create mode 100644 vgchange_test.go diff --git a/args.go b/args.go index cc9cbfa..1e7e7c5 100644 --- a/args.go +++ b/args.go @@ -33,7 +33,9 @@ const ( ArgsTypePVs ArgsType = "pvs" ArgsTypeVGs ArgsType = "vgs" ArgsTypeLVCreate ArgsType = "lvcreate" + ArgsTypeLVChange ArgsType = "lvchange" ArgsTypeVGCreate ArgsType = "vgcreate" + ArgsTypeVGChange ArgsType = "vgchange" ArgsTypeLVRename ArgsType = "lvrename" ) diff --git a/client.go b/client.go index fd1e7f9..e870c62 100644 --- a/client.go +++ b/client.go @@ -2,6 +2,12 @@ package lvm2go import ( "context" + "errors" +) + +var ( + ErrVolumeGroupNotFound = errors.New("volume group not found") + ErrLogicalVolumeNotFound = errors.New("logical volume not found") ) type client struct{} @@ -41,6 +47,15 @@ type MetaClient interface { // VolumeGroupClient is a client that provides operations on lvm2 volume groups. type VolumeGroupClient interface { + // VG returns a volume group that matches the given options. + // + // If no VolumeGroupName is defined, ErrVolumeGroupNameRequired is returned. + // If no volume group is found, ErrVolumeGroupNotFound is returned. + // + // It is equivalent to calling VGs with the same options and returning the first volume group in the list. + // see VGs for more information. + VG(ctx context.Context, opts ...VGsOption) (*VolumeGroup, error) + // VGs return a list of volume groups that match the given options. // // If no volume groups are found, an empty slice is returned. @@ -48,7 +63,7 @@ type VolumeGroupClient interface { // the slice may be shorter than the total number of volume groups. // // See man lvm vgs for more information. - VGs(ctx context.Context, opts ...VGsOption) ([]VolumeGroup, error) + VGs(ctx context.Context, opts ...VGsOption) ([]*VolumeGroup, error) // VGCreate creates a new volume group with the given options. // @@ -83,6 +98,16 @@ type VolumeGroupClient interface { // LogicalVolumeClient is a client that provides operations on lvm2 logical volumes. type LogicalVolumeClient interface { + // LV returns a logical volume that matches the given options. + // + // If no LogicalVolumeName is defined, ErrLogicalVolumeNameRequired is returned. + // If no VolumeGroupName is defined, ErrVolumeGroupNameRequired is returned. + // If no logical volume is found in the volume group, ErrLogicalVolumeNotFound is returned. + // + // It is equivalent to calling LVs with the same options and returning the first logical volume in the list. + // see LVs for more information. + LV(ctx context.Context, opts ...LVsOption) (*LogicalVolume, error) + // LVs return a list of logical volumes that match the given options. // // If no logical volumes are found, an empty slice is returned. @@ -90,7 +115,7 @@ type LogicalVolumeClient interface { // the slice may be shorter than the total number of logical volumes. // // See man lvm lvs for more information. - LVs(ctx context.Context, opts ...LVsOption) ([]LogicalVolume, error) + LVs(ctx context.Context, opts ...LVsOption) ([]*LogicalVolume, error) // LVCreate creates a new logical volume with the given options. // @@ -137,7 +162,7 @@ type PhysicalVolumeClient interface { // the slice may be shorter than the total number of physical volumes. // // See man lvm pvs for more information. - PVs(ctx context.Context, opts ...PVsOption) ([]PhysicalVolume, error) + PVs(ctx context.Context, opts ...PVsOption) ([]*PhysicalVolume, error) // PVCreate creates a new physical volume with the given options. // diff --git a/client_test.go b/client_test.go index e65724a..c8cb7aa 100644 --- a/client_test.go +++ b/client_test.go @@ -74,10 +74,6 @@ func TestLVs(t *testing.T) { t.Fatal(err) } - if len(lvs) != len(tc.Volumes) { - t.Fatalf("Expected %d logical volumes, got %d", len(tc.Volumes), len(lvs)) - } - for _, expected := range infra.lvs { found := false for _, lv := range lvs { @@ -97,14 +93,10 @@ func TestLVs(t *testing.T) { } } - vgs, err := clnt.VGs(ctx, infra.volumeGroup.Name) + vg, err := clnt.VG(ctx, infra.volumeGroup.Name) if err != nil { t.Fatal(err) } - if len(vgs) != 1 { - t.Fatalf("Expected 1 volume group, got %d", len(vgs)) - } - vg := vgs[0] if vg.Name != infra.volumeGroup.Name { t.Fatalf("Expected volume group %s, got %s", infra.volumeGroup.Name, vg.Name) diff --git a/config_test.go b/config_test.go index 7b88774..56c5ff0 100644 --- a/config_test.go +++ b/config_test.go @@ -9,6 +9,7 @@ import ( ) func Test_RawConfig(t *testing.T) { + t.Parallel() FailTestIfNotRoot(t) slog.SetDefault(slog.New(NewContextPropagatingSlogHandler(NewTestingHandler(t)))) slog.SetLogLoggerLevel(slog.LevelDebug) diff --git a/json_convert_helper.go b/json_convert_helper.go index b36aca3..75ca7bb 100644 --- a/json_convert_helper.go +++ b/json_convert_helper.go @@ -17,7 +17,9 @@ func unmarshalAndConvertToStrings(raw map[string]json.RawMessage, key string, fi return err } - *fieldPtr = strings.Split(str, ",") + if len(str) > 0 { + *fieldPtr = strings.Split(str, ",") + } return nil } diff --git a/logical_volume.go b/logical_volume.go index b677bff..74657c3 100644 --- a/logical_volume.go +++ b/logical_volume.go @@ -18,7 +18,7 @@ type LogicalVolume struct { Major int64 `json:"lv_kernel_major"` Minor int64 `json:"lv_kernel_minor"` - Tags string `json:"lv_tags"` + Tags Tags `json:"lv_tags"` Attr LVAttributes `json:"lv_attr"` Size Size `json:"lv_size"` @@ -43,7 +43,6 @@ func (lv *LogicalVolume) UnmarshalJSON(data []byte) error { "lv_name": (*string)(&lv.Name), "lv_full_name": &lv.FullName, "lv_path": &lv.Path, - "lv_tags": &lv.Tags, "origin": &lv.Origin, "pool_lv": &lv.PoolLogicalVolume, "vg_name": (*string)(&lv.VolumeGroupName), @@ -55,6 +54,14 @@ func (lv *LogicalVolume) UnmarshalJSON(data []byte) error { } } + for key, fieldPtr := range map[string]*Tags{ + "lv_tags": &lv.Tags, + } { + if err := unmarshalAndConvertToStrings(raw, key, (*[]string)(fieldPtr)); err != nil { + return err + } + } + for key, fieldPtr := range map[string]*int64{ "lv_kernel_major": &lv.Major, "lv_kernel_minor": &lv.Minor, @@ -119,6 +126,10 @@ func (opt LogicalVolumeName) ApplyToLVChangeOptions(opts *LVChangeOptions) { opts.LogicalVolumeName = opt } +func (opt LogicalVolumeName) ApplyToLVsOptions(opts *LVsOptions) { + opts.LogicalVolumeName = opt +} + type FQLogicalVolumeName struct { VolumeGroupName LogicalVolumeName @@ -147,11 +158,16 @@ func (opt *FQLogicalVolumeName) ApplyToLVResizeOptions(opts *LVResizeOptions) { func (opt *FQLogicalVolumeName) ApplyToLVReduceOptions(opts *LVReduceOptions) { opts.VolumeGroupName, opts.LogicalVolumeName = opt.VolumeGroupName, opt.LogicalVolumeName } + func (opt *FQLogicalVolumeName) ApplyToLVRenameOptions(opts *LVRenameOptions) { opts.VolumeGroupName = opt.VolumeGroupName opts.SetOldOrNew(opt.LogicalVolumeName) } +func (opt *FQLogicalVolumeName) ApplyToLVsOptions(opts *LVsOptions) { + opts.VolumeGroupName, opts.LogicalVolumeName = opt.VolumeGroupName, opt.LogicalVolumeName +} + func (opt *FQLogicalVolumeName) Split() (VolumeGroupName, LogicalVolumeName) { return opt.VolumeGroupName, opt.LogicalVolumeName } diff --git a/lv_attr_test.go b/lv_attr_test.go index a62907a..050392b 100644 --- a/lv_attr_test.go +++ b/lv_attr_test.go @@ -8,6 +8,7 @@ import ( ) func TestLVAttributes(t *testing.T) { + t.Parallel() type args struct { raw string } diff --git a/lvchange.go b/lvchange.go index 3fcf4e6..fbd3897 100644 --- a/lvchange.go +++ b/lvchange.go @@ -52,7 +52,7 @@ func (c *client) LVChange(ctx context.Context, opts ...LVChangeOption) error { } func (list LVChangeOptionsList) AsArgs() (Arguments, error) { - args := NewArgs(ArgsTypeGeneric) + args := NewArgs(ArgsTypeLVChange) options := LVChangeOptions{} for _, opt := range list { opt.ApplyToLVChangeOptions(&options) diff --git a/lvextend_test.go b/lvextend_test.go index 942e837..1bd63d4 100644 --- a/lvextend_test.go +++ b/lvextend_test.go @@ -8,6 +8,7 @@ import ( ) func TestLVExtend(t *testing.T) { + t.Parallel() FailTestIfNotRoot(t) clnt := NewClient() diff --git a/lvmdevices_test.go b/lvmdevices_test.go index 9ec5996..49ac0da 100644 --- a/lvmdevices_test.go +++ b/lvmdevices_test.go @@ -12,6 +12,7 @@ import ( ) func TestLVMDevices(t *testing.T) { + t.Parallel() FailTestIfNotRoot(t) _, err := exec.LookPath("lvmdevices") diff --git a/lvrename_test.go b/lvrename_test.go index e08b56c..3fc4c9b 100644 --- a/lvrename_test.go +++ b/lvrename_test.go @@ -9,6 +9,7 @@ import ( ) func TestLVRename(t *testing.T) { + t.Parallel() FailTestIfNotRoot(t) clnt := NewClient() diff --git a/lvs.go b/lvs.go index 94c109f..448742c 100644 --- a/lvs.go +++ b/lvs.go @@ -7,6 +7,7 @@ import ( type ( LVsOptions struct { VolumeGroupName + LogicalVolumeName Tags Select @@ -27,10 +28,10 @@ var ( // LVs returns a list of logical volumes that match the given options. // If no logical volumes are found, nil is returned. // It is really just a wrapper around the `lvs --reportformat json` command. -func (c *client) LVs(ctx context.Context, opts ...LVsOption) ([]LogicalVolume, error) { +func (c *client) LVs(ctx context.Context, opts ...LVsOption) ([]*LogicalVolume, error) { type lvReport struct { Report []struct { - LV []LogicalVolume `json:"lv"` + LV []*LogicalVolume `json:"lv"` } `json:"report"` } @@ -67,6 +68,39 @@ func (c *client) LVs(ctx context.Context, opts ...LVsOption) ([]LogicalVolume, e return lvs, nil } +func (c *client) LV(ctx context.Context, opts ...LVsOption) (*LogicalVolume, error) { + foundVG := false + foundLV := false + for _, opt := range opts { + if _, ok := opt.(VolumeGroupName); ok { + foundVG = true + } + if _, ok := opt.(LogicalVolumeName); ok { + foundLV = true + } + if foundVG && foundLV { + break + } + } + if !foundVG { + return nil, ErrVolumeGroupNameRequired + } + if !foundLV { + return nil, ErrLogicalVolumeNameRequired + } + + lvs, err := c.LVs(ctx, opts...) + if err != nil { + return nil, err + } + + if len(lvs) == 0 { + return nil, ErrLogicalVolumeNotFound + } + + return lvs[0], nil +} + func (opts *LVsOptions) ApplyToArgs(args Arguments) error { for _, arg := range []Argument{ opts.VolumeGroupName, diff --git a/physical_volume.go b/physical_volume.go index 3762e2d..026e53e 100644 --- a/physical_volume.go +++ b/physical_volume.go @@ -2,8 +2,11 @@ package lvm2go import ( "encoding/json" + "errors" ) +var ErrPhysicalVolumeNameRequired = errors.New("PhysicalVolumeName is required for a fully qualified physical volume") + type PhysicalVolume struct { UUID string `json:"pv_uuid"` Name PhysicalVolumeName `json:"pv_name"` diff --git a/pvs.go b/pvs.go index ebb704f..7aacbfe 100644 --- a/pvs.go +++ b/pvs.go @@ -27,10 +27,10 @@ var ( // PVs returns a list of logical volumes that match the given options. // If no logical volumes are found, nil is returned. // It is really just a wrapper around the `lvs --reportformat json` command. -func (c *client) PVs(ctx context.Context, opts ...PVsOption) ([]PhysicalVolume, error) { +func (c *client) PVs(ctx context.Context, opts ...PVsOption) ([]*PhysicalVolume, error) { type lvReport struct { Report []struct { - PV []PhysicalVolume `json:"pv"` + PV []*PhysicalVolume `json:"pv"` } `json:"report"` } @@ -68,16 +68,16 @@ func (c *client) PVs(ctx context.Context, opts ...PVsOption) ([]PhysicalVolume, } func (opts *PVsOptions) ApplyToArgs(args Arguments) error { - if err := opts.VolumeGroupName.ApplyToArgs(args); err != nil { - return err - } - - if err := opts.CommonOptions.ApplyToArgs(args); err != nil { - return err - } - if err := opts.ColumnOptions.ApplyToArgs(args); err != nil { - return err + for _, arg := range []Argument{ + opts.VolumeGroupName, + opts.Tags, + opts.CommonOptions, + opts.ColumnOptions, + } { + if err := arg.ApplyToArgs(args); err != nil { + return err + } } return nil diff --git a/size_test.go b/size_test.go index 34f5e12..def3ea5 100644 --- a/size_test.go +++ b/size_test.go @@ -86,6 +86,7 @@ func init() { } func Test_Size(t *testing.T) { + t.Parallel() for _, tc := range DefaultSizeTestCases { t.Run(tc.InputToParse, func(t *testing.T) { actual, err := ParseSize(tc.InputToParse) diff --git a/tags.go b/tags.go index 4018806..0d5794c 100644 --- a/tags.go +++ b/tags.go @@ -19,6 +19,9 @@ func (opt Tags) ApplyToLVCreateOptions(opts *LVCreateOptions) { func (opt Tags) ApplyToVGRemoveOptions(opts *VGRemoveOptions) { opts.Tags = opt } +func (opt Tags) ApplyToVGChangeOptions(opts *VGChangeOptions) { + opts.Tags = opt +} func (opt Tags) ApplyToLVRemoveOptions(opts *LVRemoveOptions) { opts.Tags = opt } @@ -29,6 +32,10 @@ func (opt Tags) ApplyToArgs(args Arguments) error { } switch args.GetType() { + case ArgsTypeLVChange: + fallthrough + case ArgsTypeVGChange: + fallthrough case ArgsTypeVGCreate: fallthrough case ArgsTypeLVCreate: diff --git a/util_test.go b/util_test.go index b301753..84b83cc 100644 --- a/util_test.go +++ b/util_test.go @@ -102,8 +102,15 @@ func (t LoopbackDevices) PhysicalVolumeNames() PhysicalVolumeNames { } +// testLoopbackCreationSync is a mutex to synchronize the creation of loopback devices in tests +// so that they don't interfere with each other by requesting the same free loopback device +var testLoopbackCreationSync = sync.Mutex{} + func MakeTestLoopbackDevice(t *testing.T, size Size) LoopbackDevice { + t.Helper() ctx := context.Background() + testLoopbackCreationSync.Lock() + defer testLoopbackCreationSync.Unlock() backingFilePath := filepath.Join(t.TempDir(), fmt.Sprintf("%s.img", NewNonDeterministicTestID(t))) @@ -132,7 +139,6 @@ func MakeTestLoopbackDevice(t *testing.T, size Size) LoopbackDevice { } logger = logger.With("loop", loop) logger.DebugContext(ctx, "created test loopback device successfully") - t.Cleanup(func() { logger.DebugContext(ctx, "cleaning up test loopback device") if err := loop.Close(); err != nil { diff --git a/vgchange.go b/vgchange.go index 7b11a75..6944ee9 100644 --- a/vgchange.go +++ b/vgchange.go @@ -2,7 +2,6 @@ package lvm2go import ( "context" - "errors" "fmt" ) @@ -37,9 +36,38 @@ func (c *client) VGChange(ctx context.Context, opts ...VGChangeOption) error { } func (list VGChangeOptionsList) AsArgs() (Arguments, error) { - return nil, fmt.Errorf("not implemented: %w", errors.ErrUnsupported) + args := NewArgs(ArgsTypeVGCreate) + options := VGChangeOptions{} + for _, opt := range list { + opt.ApplyToVGChangeOptions(&options) + } + if err := options.ApplyToArgs(args); err != nil { + return nil, err + } + return args, nil } func (opts *VGChangeOptions) ApplyToArgs(args Arguments) error { - return fmt.Errorf("not implemented: %w", errors.ErrUnsupported) + if opts.VolumeGroupName == "" { + return fmt.Errorf("VolumeGroupName is required for creation of a volume group") + } + + for _, opt := range []Argument{ + opts.VolumeGroupName, + opts.Tags, + opts.DelTags, + opts.AutoActivation, + opts.CommonOptions, + } { + if err := opt.ApplyToArgs(args); err != nil { + return err + + } + } + + return nil +} + +func (opts *VGChangeOptions) ApplyToVGChangeOptions(new *VGChangeOptions) { + *new = *opts } diff --git a/vgchange_test.go b/vgchange_test.go new file mode 100644 index 0000000..bfe5344 --- /dev/null +++ b/vgchange_test.go @@ -0,0 +1,62 @@ +package lvm2go_test + +import ( + "context" + "slices" + "testing" + + . "github.com/jakobmoellerdev/lvm2go" +) + +func TestVGChange(t *testing.T) { + FailTestIfNotRoot(t) + + clnt := NewClient() + ctx := context.Background() + + test := test{ + LoopDevices: []Size{ + MustParseSize("10M"), + }, + Volumes: []TestLogicalVolume{{ + Options: LVCreateOptionList{ + MustParseExtents("100%FREE"), + }, + }}, + } + + infra := test.SetupDevicesAndVolumeGroup(t) + + testTags := Tags{"test"} + + getVGTags := func() Tags { + vg, err := clnt.VG(ctx, infra.volumeGroup.Name) + if err != nil { + t.Fatal(err) + } + return vg.Tags + } + + if err := clnt.VGChange(ctx, infra.volumeGroup.Name, testTags); err != nil { + t.Fatal(err) + } + + if tags := getVGTags(); len(tags) == 0 { + t.Fatalf("expected tags, got %v", tags) + } else { + for _, testTag := range testTags { + if !slices.Contains(tags, testTag) { + t.Fatalf("expected tag %s, got %v", testTag, tags) + } + } + } + + if err := clnt.VGChange(ctx, infra.volumeGroup.Name, DelTags(testTags)); err != nil { + t.Fatal(err) + } + + if tags := getVGTags(); len(tags) != 0 { + t.Fatalf("expected 0 tags, got %d", len(tags)) + } + +} diff --git a/vgextend_test.go b/vgextend_test.go index 5161e69..8ce543b 100644 --- a/vgextend_test.go +++ b/vgextend_test.go @@ -8,6 +8,7 @@ import ( ) func TestVGExtend(t *testing.T) { + t.Parallel() FailTestIfNotRoot(t) clnt := NewClient() @@ -35,21 +36,11 @@ func TestVGExtend(t *testing.T) { t.Fatal(err) } - vgs, err := clnt.VGs(ctx) + vg, err := clnt.VG(ctx, infra.volumeGroup.Name) if err != nil { t.Fatal(err) } - if len(vgs) != 1 { - t.Fatalf("expected 1 volume group, got %d", len(vgs)) - } - - if vgs[0].Name != infra.volumeGroup.Name { - t.Fatalf("expected volume group %s, got %s", infra.volumeGroup.Name, vgs[0].Name) - } - - vg := vgs[0] - if vg.PvCount != 3 { t.Fatalf("expected 3 physical volumes, got %d", vg.PvCount) } diff --git a/vgrename_test.go b/vgrename_test.go index 0da21a7..f0002aa 100644 --- a/vgrename_test.go +++ b/vgrename_test.go @@ -8,6 +8,7 @@ import ( ) func TestVGRename(t *testing.T) { + t.Parallel() FailTestIfNotRoot(t) clnt := NewClient() diff --git a/vgs.go b/vgs.go index 47cb5b9..793862c 100644 --- a/vgs.go +++ b/vgs.go @@ -24,10 +24,10 @@ var ( _ Argument = (*VGsOptions)(nil) ) -func (c *client) VGs(ctx context.Context, opts ...VGsOption) ([]VolumeGroup, error) { +func (c *client) VGs(ctx context.Context, opts ...VGsOption) ([]*VolumeGroup, error) { type vgReport struct { Report []struct { - VG []VolumeGroup `json:"vg"` + VG []*VolumeGroup `json:"vg"` } `json:"report"` } res := new(vgReport) @@ -61,6 +61,30 @@ func (c *client) VGs(ctx context.Context, opts ...VGsOption) ([]VolumeGroup, err return res.Report[0].VG, nil } +func (c *client) VG(ctx context.Context, opts ...VGsOption) (*VolumeGroup, error) { + found := false + for _, opt := range opts { + if _, ok := opt.(VolumeGroupName); ok { + found = true + break + } + } + if !found { + return nil, ErrVolumeGroupNameRequired + } + + vgs, err := c.VGs(ctx, opts...) + if err != nil { + return nil, err + } + + if len(vgs) == 0 { + return nil, ErrVolumeGroupNotFound + } + + return vgs[0], nil +} + func (opts *VGsOptions) ApplyToArgs(args Arguments) error { if err := opts.VolumeGroupName.ApplyToArgs(args); err != nil { return err diff --git a/volume_group.go b/volume_group.go index 01e2bc5..bfc2fe7 100644 --- a/volume_group.go +++ b/volume_group.go @@ -11,6 +11,7 @@ type VolumeGroup struct { LockType string `json:"vg_lock_type"` LockArgs string `json:"vg_lock_args"` VGAttributes string `json:"vg_attr"` + Tags Tags `json:"vg_tags"` ExtentSize Size `json:"vg_extent_size"` ExtentCount int64 `json:"vg_extent_count"` @@ -49,6 +50,14 @@ func (vg *VolumeGroup) UnmarshalJSON(data []byte) error { } } + for key, fieldPtr := range map[string]*Tags{ + "vg_tags": &vg.Tags, + } { + if err := unmarshalAndConvertToStrings(raw, key, (*[]string)(fieldPtr)); err != nil { + return err + } + } + for key, fieldPtr := range map[string]*int64{ "vg_extent_count": &vg.ExtentCount, "pv_count": &vg.PvCount, @@ -115,10 +124,12 @@ func (opt VolumeGroupName) ApplyToVGExtendOptions(opts *VGExtendOptions) { func (opt VolumeGroupName) ApplyToVGRenameOptions(opts *VGRenameOptions) { opts.SetOldOrNew(opt) } +func (opt VolumeGroupName) ApplyToVGChangeOptions(opts *VGChangeOptions) { + opts.VolumeGroupName = opt +} func (opt VolumeGroupName) ApplyToLVRemoveOptions(opts *LVRemoveOptions) { opts.VolumeGroupName = opt } - func (opt VolumeGroupName) ApplyToLVResizeOptions(opts *LVResizeOptions) { opts.VolumeGroupName = opt }