From 5bf6fc7e70ac885062eefc67da0f8da2003ca7ea Mon Sep 17 00:00:00 2001 From: Anton Sergunov Date: Fri, 28 Feb 2025 14:28:08 +0600 Subject: [PATCH] [controller] Data wipe unit tests (#128) Signed-off-by: Anton Sergunov --- images/agent/src/go.mod | 1 + images/agent/src/go.sum | 2 + .../src/internal/controller/llv/reconciler.go | 2 +- images/agent/src/internal/mock_utils/mock.go | 953 ++++++++++++++++++ .../agent/src/internal/utils/block_device.go | 116 +++ .../src/internal/utils/block_device_test.go | 98 ++ images/agent/src/internal/utils/commands.go | 11 +- images/agent/src/internal/utils/syscall.go | 88 ++ images/agent/src/internal/utils/utils_test.go | 13 + .../src/internal/utils/volume_cleanup_ce.go | 2 +- .../internal/utils/volume_cleanup_ce_test.go | 39 + .../src/internal/utils/volume_cleanup_ee.go | 165 +-- .../internal/utils/volume_cleanup_ee_test.go | 407 ++++++++ 13 files changed, 1758 insertions(+), 139 deletions(-) create mode 100644 images/agent/src/internal/mock_utils/mock.go create mode 100644 images/agent/src/internal/utils/block_device.go create mode 100644 images/agent/src/internal/utils/block_device_test.go create mode 100644 images/agent/src/internal/utils/syscall.go create mode 100644 images/agent/src/internal/utils/utils_test.go create mode 100644 images/agent/src/internal/utils/volume_cleanup_ce_test.go create mode 100644 images/agent/src/internal/utils/volume_cleanup_ee_test.go diff --git a/images/agent/src/go.mod b/images/agent/src/go.mod index ceb3fd0b..f6839caf 100644 --- a/images/agent/src/go.mod +++ b/images/agent/src/go.mod @@ -62,6 +62,7 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect + go.uber.org/mock v0.5.0 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/oauth2 v0.26.0 // indirect golang.org/x/sync v0.11.0 // indirect diff --git a/images/agent/src/go.sum b/images/agent/src/go.sum index f7b05204..75954be4 100644 --- a/images/agent/src/go.sum +++ b/images/agent/src/go.sum @@ -105,6 +105,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= diff --git a/images/agent/src/internal/controller/llv/reconciler.go b/images/agent/src/internal/controller/llv/reconciler.go index 478e2ef7..3367b106 100644 --- a/images/agent/src/internal/controller/llv/reconciler.go +++ b/images/agent/src/internal/controller/llv/reconciler.go @@ -636,7 +636,7 @@ func (r *Reconciler) deleteLVIfNeeded(ctx context.Context, vgName string, llv *v } prevFailedMethod = &method r.log.Debug(fmt.Sprintf("[deleteLVIfNeeded] running cleanup for LV %s in VG %s with method %s", lvName, vgName, method)) - err = utils.VolumeCleanup(ctx, r.log, vgName, lvName, method) + err = utils.VolumeCleanup(ctx, r.log, utils.OsDeviceOpener(), vgName, lvName, method) if err != nil { r.log.Error(err, fmt.Sprintf("[deleteLVIfNeeded] unable to clean up LV %s in VG %s with method %s", lvName, vgName, method)) return true, err diff --git a/images/agent/src/internal/mock_utils/mock.go b/images/agent/src/internal/mock_utils/mock.go new file mode 100644 index 00000000..e3921855 --- /dev/null +++ b/images/agent/src/internal/mock_utils/mock.go @@ -0,0 +1,953 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: agent/internal/utils (interfaces: SysCall,BlockDevice,File,FileOpener,BlockDeviceOpener) +// +// Generated by this command: +// +// mockgen -write_generate_directive -typed agent/internal/utils SysCall,BlockDevice,File,FileOpener,BlockDeviceOpener +// + +// Package mock_utils is a generated GoMock package. +package mock_utils + +import ( + utils "agent/internal/utils" + fs "io/fs" + reflect "reflect" + syscall "syscall" + + gomock "go.uber.org/mock/gomock" + unix "golang.org/x/sys/unix" +) + +//go:generate mockgen -write_generate_directive -typed agent/internal/utils SysCall,BlockDevice,File,FileOpener,BlockDeviceOpener + +// MockSysCall is a mock of SysCall interface. +type MockSysCall struct { + ctrl *gomock.Controller + recorder *MockSysCallMockRecorder + isgomock struct{} +} + +// MockSysCallMockRecorder is the mock recorder for MockSysCall. +type MockSysCallMockRecorder struct { + mock *MockSysCall +} + +// NewMockSysCall creates a new mock instance. +func NewMockSysCall(ctrl *gomock.Controller) *MockSysCall { + mock := &MockSysCall{ctrl: ctrl} + mock.recorder = &MockSysCallMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSysCall) EXPECT() *MockSysCallMockRecorder { + return m.recorder +} + +// Blkdiscard mocks base method. +func (m *MockSysCall) Blkdiscard(fd uintptr, start, count uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Blkdiscard", fd, start, count) + ret0, _ := ret[0].(error) + return ret0 +} + +// Blkdiscard indicates an expected call of Blkdiscard. +func (mr *MockSysCallMockRecorder) Blkdiscard(fd, start, count any) *MockSysCallBlkdiscardCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Blkdiscard", reflect.TypeOf((*MockSysCall)(nil).Blkdiscard), fd, start, count) + return &MockSysCallBlkdiscardCall{Call: call} +} + +// MockSysCallBlkdiscardCall wrap *gomock.Call +type MockSysCallBlkdiscardCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockSysCallBlkdiscardCall) Return(arg0 error) *MockSysCallBlkdiscardCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockSysCallBlkdiscardCall) Do(f func(uintptr, uint64, uint64) error) *MockSysCallBlkdiscardCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockSysCallBlkdiscardCall) DoAndReturn(f func(uintptr, uint64, uint64) error) *MockSysCallBlkdiscardCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Fstat mocks base method. +func (m *MockSysCall) Fstat(fd int, stat *unix.Stat_t) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Fstat", fd, stat) + ret0, _ := ret[0].(error) + return ret0 +} + +// Fstat indicates an expected call of Fstat. +func (mr *MockSysCallMockRecorder) Fstat(fd, stat any) *MockSysCallFstatCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fstat", reflect.TypeOf((*MockSysCall)(nil).Fstat), fd, stat) + return &MockSysCallFstatCall{Call: call} +} + +// MockSysCallFstatCall wrap *gomock.Call +type MockSysCallFstatCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockSysCallFstatCall) Return(err error) *MockSysCallFstatCall { + c.Call = c.Call.Return(err) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockSysCallFstatCall) Do(f func(int, *unix.Stat_t) error) *MockSysCallFstatCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockSysCallFstatCall) DoAndReturn(f func(int, *unix.Stat_t) error) *MockSysCallFstatCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Syscall mocks base method. +func (m *MockSysCall) Syscall(trap, a1, a2, a3 uintptr) (uintptr, uintptr, syscall.Errno) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Syscall", trap, a1, a2, a3) + ret0, _ := ret[0].(uintptr) + ret1, _ := ret[1].(uintptr) + ret2, _ := ret[2].(syscall.Errno) + return ret0, ret1, ret2 +} + +// Syscall indicates an expected call of Syscall. +func (mr *MockSysCallMockRecorder) Syscall(trap, a1, a2, a3 any) *MockSysCallSyscallCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Syscall", reflect.TypeOf((*MockSysCall)(nil).Syscall), trap, a1, a2, a3) + return &MockSysCallSyscallCall{Call: call} +} + +// MockSysCallSyscallCall wrap *gomock.Call +type MockSysCallSyscallCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockSysCallSyscallCall) Return(r1, r2 uintptr, err syscall.Errno) *MockSysCallSyscallCall { + c.Call = c.Call.Return(r1, r2, err) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockSysCallSyscallCall) Do(f func(uintptr, uintptr, uintptr, uintptr) (uintptr, uintptr, syscall.Errno)) *MockSysCallSyscallCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockSysCallSyscallCall) DoAndReturn(f func(uintptr, uintptr, uintptr, uintptr) (uintptr, uintptr, syscall.Errno)) *MockSysCallSyscallCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MockBlockDevice is a mock of BlockDevice interface. +type MockBlockDevice struct { + ctrl *gomock.Controller + recorder *MockBlockDeviceMockRecorder + isgomock struct{} +} + +// MockBlockDeviceMockRecorder is the mock recorder for MockBlockDevice. +type MockBlockDeviceMockRecorder struct { + mock *MockBlockDevice +} + +// NewMockBlockDevice creates a new mock instance. +func NewMockBlockDevice(ctrl *gomock.Controller) *MockBlockDevice { + mock := &MockBlockDevice{ctrl: ctrl} + mock.recorder = &MockBlockDeviceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBlockDevice) EXPECT() *MockBlockDeviceMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockBlockDevice) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockBlockDeviceMockRecorder) Close() *MockBlockDeviceCloseCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockBlockDevice)(nil).Close)) + return &MockBlockDeviceCloseCall{Call: call} +} + +// MockBlockDeviceCloseCall wrap *gomock.Call +type MockBlockDeviceCloseCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockBlockDeviceCloseCall) Return(arg0 error) *MockBlockDeviceCloseCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockBlockDeviceCloseCall) Do(f func() error) *MockBlockDeviceCloseCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockBlockDeviceCloseCall) DoAndReturn(f func() error) *MockBlockDeviceCloseCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Discard mocks base method. +func (m *MockBlockDevice) Discard(start, count uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Discard", start, count) + ret0, _ := ret[0].(error) + return ret0 +} + +// Discard indicates an expected call of Discard. +func (mr *MockBlockDeviceMockRecorder) Discard(start, count any) *MockBlockDeviceDiscardCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discard", reflect.TypeOf((*MockBlockDevice)(nil).Discard), start, count) + return &MockBlockDeviceDiscardCall{Call: call} +} + +// MockBlockDeviceDiscardCall wrap *gomock.Call +type MockBlockDeviceDiscardCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockBlockDeviceDiscardCall) Return(arg0 error) *MockBlockDeviceDiscardCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockBlockDeviceDiscardCall) Do(f func(uint64, uint64) error) *MockBlockDeviceDiscardCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockBlockDeviceDiscardCall) DoAndReturn(f func(uint64, uint64) error) *MockBlockDeviceDiscardCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Fd mocks base method. +func (m *MockBlockDevice) Fd() uintptr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Fd") + ret0, _ := ret[0].(uintptr) + return ret0 +} + +// Fd indicates an expected call of Fd. +func (mr *MockBlockDeviceMockRecorder) Fd() *MockBlockDeviceFdCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fd", reflect.TypeOf((*MockBlockDevice)(nil).Fd)) + return &MockBlockDeviceFdCall{Call: call} +} + +// MockBlockDeviceFdCall wrap *gomock.Call +type MockBlockDeviceFdCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockBlockDeviceFdCall) Return(arg0 uintptr) *MockBlockDeviceFdCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockBlockDeviceFdCall) Do(f func() uintptr) *MockBlockDeviceFdCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockBlockDeviceFdCall) DoAndReturn(f func() uintptr) *MockBlockDeviceFdCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Name mocks base method. +func (m *MockBlockDevice) Name() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name") + ret0, _ := ret[0].(string) + return ret0 +} + +// Name indicates an expected call of Name. +func (mr *MockBlockDeviceMockRecorder) Name() *MockBlockDeviceNameCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockBlockDevice)(nil).Name)) + return &MockBlockDeviceNameCall{Call: call} +} + +// MockBlockDeviceNameCall wrap *gomock.Call +type MockBlockDeviceNameCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockBlockDeviceNameCall) Return(arg0 string) *MockBlockDeviceNameCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockBlockDeviceNameCall) Do(f func() string) *MockBlockDeviceNameCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockBlockDeviceNameCall) DoAndReturn(f func() string) *MockBlockDeviceNameCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Read mocks base method. +func (m *MockBlockDevice) Read(p []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", p) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockBlockDeviceMockRecorder) Read(p any) *MockBlockDeviceReadCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockBlockDevice)(nil).Read), p) + return &MockBlockDeviceReadCall{Call: call} +} + +// MockBlockDeviceReadCall wrap *gomock.Call +type MockBlockDeviceReadCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockBlockDeviceReadCall) Return(n int, err error) *MockBlockDeviceReadCall { + c.Call = c.Call.Return(n, err) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockBlockDeviceReadCall) Do(f func([]byte) (int, error)) *MockBlockDeviceReadCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockBlockDeviceReadCall) DoAndReturn(f func([]byte) (int, error)) *MockBlockDeviceReadCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ReadAt mocks base method. +func (m *MockBlockDevice) ReadAt(p []byte, off int64) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadAt", p, off) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadAt indicates an expected call of ReadAt. +func (mr *MockBlockDeviceMockRecorder) ReadAt(p, off any) *MockBlockDeviceReadAtCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadAt", reflect.TypeOf((*MockBlockDevice)(nil).ReadAt), p, off) + return &MockBlockDeviceReadAtCall{Call: call} +} + +// MockBlockDeviceReadAtCall wrap *gomock.Call +type MockBlockDeviceReadAtCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockBlockDeviceReadAtCall) Return(n int, err error) *MockBlockDeviceReadAtCall { + c.Call = c.Call.Return(n, err) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockBlockDeviceReadAtCall) Do(f func([]byte, int64) (int, error)) *MockBlockDeviceReadAtCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockBlockDeviceReadAtCall) DoAndReturn(f func([]byte, int64) (int, error)) *MockBlockDeviceReadAtCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Seek mocks base method. +func (m *MockBlockDevice) Seek(offset int64, whence int) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Seek", offset, whence) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Seek indicates an expected call of Seek. +func (mr *MockBlockDeviceMockRecorder) Seek(offset, whence any) *MockBlockDeviceSeekCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Seek", reflect.TypeOf((*MockBlockDevice)(nil).Seek), offset, whence) + return &MockBlockDeviceSeekCall{Call: call} +} + +// MockBlockDeviceSeekCall wrap *gomock.Call +type MockBlockDeviceSeekCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockBlockDeviceSeekCall) Return(arg0 int64, arg1 error) *MockBlockDeviceSeekCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockBlockDeviceSeekCall) Do(f func(int64, int) (int64, error)) *MockBlockDeviceSeekCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockBlockDeviceSeekCall) DoAndReturn(f func(int64, int) (int64, error)) *MockBlockDeviceSeekCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Size mocks base method. +func (m *MockBlockDevice) Size() (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Size") + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Size indicates an expected call of Size. +func (mr *MockBlockDeviceMockRecorder) Size() *MockBlockDeviceSizeCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Size", reflect.TypeOf((*MockBlockDevice)(nil).Size)) + return &MockBlockDeviceSizeCall{Call: call} +} + +// MockBlockDeviceSizeCall wrap *gomock.Call +type MockBlockDeviceSizeCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockBlockDeviceSizeCall) Return(arg0 int64, arg1 error) *MockBlockDeviceSizeCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockBlockDeviceSizeCall) Do(f func() (int64, error)) *MockBlockDeviceSizeCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockBlockDeviceSizeCall) DoAndReturn(f func() (int64, error)) *MockBlockDeviceSizeCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// WriteAt mocks base method. +func (m *MockBlockDevice) WriteAt(p []byte, off int64) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WriteAt", p, off) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// WriteAt indicates an expected call of WriteAt. +func (mr *MockBlockDeviceMockRecorder) WriteAt(p, off any) *MockBlockDeviceWriteAtCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteAt", reflect.TypeOf((*MockBlockDevice)(nil).WriteAt), p, off) + return &MockBlockDeviceWriteAtCall{Call: call} +} + +// MockBlockDeviceWriteAtCall wrap *gomock.Call +type MockBlockDeviceWriteAtCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockBlockDeviceWriteAtCall) Return(n int, err error) *MockBlockDeviceWriteAtCall { + c.Call = c.Call.Return(n, err) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockBlockDeviceWriteAtCall) Do(f func([]byte, int64) (int, error)) *MockBlockDeviceWriteAtCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockBlockDeviceWriteAtCall) DoAndReturn(f func([]byte, int64) (int, error)) *MockBlockDeviceWriteAtCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MockFile is a mock of File interface. +type MockFile struct { + ctrl *gomock.Controller + recorder *MockFileMockRecorder + isgomock struct{} +} + +// MockFileMockRecorder is the mock recorder for MockFile. +type MockFileMockRecorder struct { + mock *MockFile +} + +// NewMockFile creates a new mock instance. +func NewMockFile(ctrl *gomock.Controller) *MockFile { + mock := &MockFile{ctrl: ctrl} + mock.recorder = &MockFileMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFile) EXPECT() *MockFileMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockFile) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockFileMockRecorder) Close() *MockFileCloseCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockFile)(nil).Close)) + return &MockFileCloseCall{Call: call} +} + +// MockFileCloseCall wrap *gomock.Call +type MockFileCloseCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockFileCloseCall) Return(arg0 error) *MockFileCloseCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockFileCloseCall) Do(f func() error) *MockFileCloseCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockFileCloseCall) DoAndReturn(f func() error) *MockFileCloseCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Fd mocks base method. +func (m *MockFile) Fd() uintptr { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Fd") + ret0, _ := ret[0].(uintptr) + return ret0 +} + +// Fd indicates an expected call of Fd. +func (mr *MockFileMockRecorder) Fd() *MockFileFdCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fd", reflect.TypeOf((*MockFile)(nil).Fd)) + return &MockFileFdCall{Call: call} +} + +// MockFileFdCall wrap *gomock.Call +type MockFileFdCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockFileFdCall) Return(arg0 uintptr) *MockFileFdCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockFileFdCall) Do(f func() uintptr) *MockFileFdCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockFileFdCall) DoAndReturn(f func() uintptr) *MockFileFdCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Name mocks base method. +func (m *MockFile) Name() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name") + ret0, _ := ret[0].(string) + return ret0 +} + +// Name indicates an expected call of Name. +func (mr *MockFileMockRecorder) Name() *MockFileNameCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockFile)(nil).Name)) + return &MockFileNameCall{Call: call} +} + +// MockFileNameCall wrap *gomock.Call +type MockFileNameCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockFileNameCall) Return(arg0 string) *MockFileNameCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockFileNameCall) Do(f func() string) *MockFileNameCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockFileNameCall) DoAndReturn(f func() string) *MockFileNameCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Read mocks base method. +func (m *MockFile) Read(p []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", p) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read. +func (mr *MockFileMockRecorder) Read(p any) *MockFileReadCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockFile)(nil).Read), p) + return &MockFileReadCall{Call: call} +} + +// MockFileReadCall wrap *gomock.Call +type MockFileReadCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockFileReadCall) Return(n int, err error) *MockFileReadCall { + c.Call = c.Call.Return(n, err) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockFileReadCall) Do(f func([]byte) (int, error)) *MockFileReadCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockFileReadCall) DoAndReturn(f func([]byte) (int, error)) *MockFileReadCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// ReadAt mocks base method. +func (m *MockFile) ReadAt(p []byte, off int64) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReadAt", p, off) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReadAt indicates an expected call of ReadAt. +func (mr *MockFileMockRecorder) ReadAt(p, off any) *MockFileReadAtCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadAt", reflect.TypeOf((*MockFile)(nil).ReadAt), p, off) + return &MockFileReadAtCall{Call: call} +} + +// MockFileReadAtCall wrap *gomock.Call +type MockFileReadAtCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockFileReadAtCall) Return(n int, err error) *MockFileReadAtCall { + c.Call = c.Call.Return(n, err) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockFileReadAtCall) Do(f func([]byte, int64) (int, error)) *MockFileReadAtCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockFileReadAtCall) DoAndReturn(f func([]byte, int64) (int, error)) *MockFileReadAtCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Seek mocks base method. +func (m *MockFile) Seek(offset int64, whence int) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Seek", offset, whence) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Seek indicates an expected call of Seek. +func (mr *MockFileMockRecorder) Seek(offset, whence any) *MockFileSeekCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Seek", reflect.TypeOf((*MockFile)(nil).Seek), offset, whence) + return &MockFileSeekCall{Call: call} +} + +// MockFileSeekCall wrap *gomock.Call +type MockFileSeekCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockFileSeekCall) Return(arg0 int64, arg1 error) *MockFileSeekCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockFileSeekCall) Do(f func(int64, int) (int64, error)) *MockFileSeekCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockFileSeekCall) DoAndReturn(f func(int64, int) (int64, error)) *MockFileSeekCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// WriteAt mocks base method. +func (m *MockFile) WriteAt(p []byte, off int64) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WriteAt", p, off) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// WriteAt indicates an expected call of WriteAt. +func (mr *MockFileMockRecorder) WriteAt(p, off any) *MockFileWriteAtCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteAt", reflect.TypeOf((*MockFile)(nil).WriteAt), p, off) + return &MockFileWriteAtCall{Call: call} +} + +// MockFileWriteAtCall wrap *gomock.Call +type MockFileWriteAtCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockFileWriteAtCall) Return(n int, err error) *MockFileWriteAtCall { + c.Call = c.Call.Return(n, err) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockFileWriteAtCall) Do(f func([]byte, int64) (int, error)) *MockFileWriteAtCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockFileWriteAtCall) DoAndReturn(f func([]byte, int64) (int, error)) *MockFileWriteAtCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MockFileOpener is a mock of FileOpener interface. +type MockFileOpener struct { + ctrl *gomock.Controller + recorder *MockFileOpenerMockRecorder + isgomock struct{} +} + +// MockFileOpenerMockRecorder is the mock recorder for MockFileOpener. +type MockFileOpenerMockRecorder struct { + mock *MockFileOpener +} + +// NewMockFileOpener creates a new mock instance. +func NewMockFileOpener(ctrl *gomock.Controller) *MockFileOpener { + mock := &MockFileOpener{ctrl: ctrl} + mock.recorder = &MockFileOpenerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFileOpener) EXPECT() *MockFileOpenerMockRecorder { + return m.recorder +} + +// Open mocks base method. +func (m *MockFileOpener) Open(name string, flag int, mode fs.FileMode) (utils.File, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Open", name, flag, mode) + ret0, _ := ret[0].(utils.File) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Open indicates an expected call of Open. +func (mr *MockFileOpenerMockRecorder) Open(name, flag, mode any) *MockFileOpenerOpenCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*MockFileOpener)(nil).Open), name, flag, mode) + return &MockFileOpenerOpenCall{Call: call} +} + +// MockFileOpenerOpenCall wrap *gomock.Call +type MockFileOpenerOpenCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockFileOpenerOpenCall) Return(arg0 utils.File, arg1 error) *MockFileOpenerOpenCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockFileOpenerOpenCall) Do(f func(string, int, fs.FileMode) (utils.File, error)) *MockFileOpenerOpenCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockFileOpenerOpenCall) DoAndReturn(f func(string, int, fs.FileMode) (utils.File, error)) *MockFileOpenerOpenCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// MockBlockDeviceOpener is a mock of BlockDeviceOpener interface. +type MockBlockDeviceOpener struct { + ctrl *gomock.Controller + recorder *MockBlockDeviceOpenerMockRecorder + isgomock struct{} +} + +// MockBlockDeviceOpenerMockRecorder is the mock recorder for MockBlockDeviceOpener. +type MockBlockDeviceOpenerMockRecorder struct { + mock *MockBlockDeviceOpener +} + +// NewMockBlockDeviceOpener creates a new mock instance. +func NewMockBlockDeviceOpener(ctrl *gomock.Controller) *MockBlockDeviceOpener { + mock := &MockBlockDeviceOpener{ctrl: ctrl} + mock.recorder = &MockBlockDeviceOpenerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBlockDeviceOpener) EXPECT() *MockBlockDeviceOpenerMockRecorder { + return m.recorder +} + +// Open mocks base method. +func (m *MockBlockDeviceOpener) Open(name string, flag int) (utils.BlockDevice, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Open", name, flag) + ret0, _ := ret[0].(utils.BlockDevice) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Open indicates an expected call of Open. +func (mr *MockBlockDeviceOpenerMockRecorder) Open(name, flag any) *MockBlockDeviceOpenerOpenCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*MockBlockDeviceOpener)(nil).Open), name, flag) + return &MockBlockDeviceOpenerOpenCall{Call: call} +} + +// MockBlockDeviceOpenerOpenCall wrap *gomock.Call +type MockBlockDeviceOpenerOpenCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *MockBlockDeviceOpenerOpenCall) Return(arg0 utils.BlockDevice, arg1 error) *MockBlockDeviceOpenerOpenCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *MockBlockDeviceOpenerOpenCall) Do(f func(string, int) (utils.BlockDevice, error)) *MockBlockDeviceOpenerOpenCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *MockBlockDeviceOpenerOpenCall) DoAndReturn(f func(string, int) (utils.BlockDevice, error)) *MockBlockDeviceOpenerOpenCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/images/agent/src/internal/utils/block_device.go b/images/agent/src/internal/utils/block_device.go new file mode 100644 index 00000000..083a302a --- /dev/null +++ b/images/agent/src/internal/utils/block_device.go @@ -0,0 +1,116 @@ +package utils + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "unsafe" + + "golang.org/x/sys/unix" +) + +type blockDevice[TSysCall SysCall] struct { + File // TODO: can I make it generic TFile to keep call static? + syscall TSysCall +} + +type Discarder interface { + Discard(start, count uint64) error +} + +type File interface { + io.Closer + io.Reader + io.WriterAt + io.ReaderAt + io.Seeker + Name() string + Fd() uintptr +} + +type BlockDevice interface { + File + Discarder + + Size() (int64, error) +} + +type BlockDeviceOpener interface { + Open(name string, flag int) (BlockDevice, error) +} +type blockDeviceOpener[TFileOpener FileOpener, TSysCall SysCall] struct { + fileOpener TFileOpener + syscall TSysCall +} + +type FileOpener interface { + Open(name string, flag int, mode fs.FileMode) (File, error) +} + +type osFileOpener struct{} + +func (osFileOpener) Open(name string, flag int, mode fs.FileMode) (File, error) { + return os.OpenFile(name, flag, mode) +} + +func (opener *blockDeviceOpener[TFileOpener, TSysCall]) Open(name string, flag int) (BlockDevice, error) { + file, err := opener.fileOpener.Open(name, flag, os.ModeDevice) + if err != nil { + return nil, fmt.Errorf("opening os file: %w", err) + } + return &blockDevice[TSysCall]{ + file, + opener.syscall, + }, nil +} + +var defaultBlockDeviceOpener = blockDeviceOpener[osFileOpener, osSyscall]{ + fileOpener: osFileOpener{}, + syscall: OsSysCall(), +} + +//nolint:revive +func OsDeviceOpener() *blockDeviceOpener[osFileOpener, osSyscall] { + return &defaultBlockDeviceOpener +} + +//nolint:revive +func NewBlockDeviceOpener[TFileOpener FileOpener, TSysCall SysCall](fileOpener TFileOpener, syscall TSysCall) *blockDeviceOpener[TFileOpener, TSysCall] { + return &blockDeviceOpener[TFileOpener, TSysCall]{ + fileOpener: fileOpener, + syscall: syscall, + } +} + +func (device *blockDevice[TSysCall]) Size() (int64, error) { + var stat Stat_t + err := device.syscall.Fstat(int(device.Fd()), &stat) + if err != nil { + return 0, fmt.Errorf("calling fstat: %w", err) + } + if stat.Mode&S_IFMT != S_IFBLK { + return 0, fmt.Errorf("not a block device, mode: %x", stat.Mode) + } + + var blockDeviceSize uint64 + _, _, errno := device.syscall.Syscall( + unix.SYS_IOCTL, + device.Fd(), + BLKGETSIZE64, + uintptr(unsafe.Pointer(&blockDeviceSize))) + if errno != 0 { + err := errors.New(errno.Error()) + return 0, fmt.Errorf("error calling ioctl BLKGETSIZE64: %w", err) + } + if blockDeviceSize == 0 { + return 0, fmt.Errorf("block size is invalid") + } + + return int64(blockDeviceSize), nil +} + +func (device *blockDevice[TSysCall]) Discard(start, count uint64) error { + return device.syscall.Blkdiscard(device.Fd(), start, count) +} diff --git a/images/agent/src/internal/utils/block_device_test.go b/images/agent/src/internal/utils/block_device_test.go new file mode 100644 index 00000000..670a7dfd --- /dev/null +++ b/images/agent/src/internal/utils/block_device_test.go @@ -0,0 +1,98 @@ +package utils_test + +import ( + "errors" + "os" + "unsafe" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" + "golang.org/x/sys/unix" + + . "agent/internal/mock_utils" + . "agent/internal/utils" +) + +var _ = Describe("BlockDevice", func() { + var ctrl *gomock.Controller + var sysCall *MockSysCall + var fileOpener *MockFileOpener + var blockDeviceOpener BlockDeviceOpener + var file *MockFile + var err error + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + sysCall = NewMockSysCall(ctrl) + fileOpener = NewMockFileOpener(ctrl) + blockDeviceOpener = NewBlockDeviceOpener(fileOpener, sysCall) + file = NewMockFile(ctrl) + }) + + fileName := "fileName" + flag := int(0) + size := int64(1024) + fd := uintptr(1234) + + var device BlockDevice + + When("device properly opened", func() { + BeforeEach(func() { + file.EXPECT().Fd().AnyTimes().Return(fd) + fileOpener.EXPECT().Open(fileName, flag, os.ModeDevice).Return(file, nil) + }) + JustBeforeEach(func() { + device, err = blockDeviceOpener.Open(fileName, 0) + Expect(err).NotTo(HaveOccurred()) + Expect(device).NotTo(BeNil()) + }) + + It("finds out size", func() { + sysCall.EXPECT().Fstat(int(fd), gomock.Any()).DoAndReturn(func(_ int, stat *Stat_t) error { + stat.Mode = S_IFBLK + return nil + }) + sysCall.EXPECT().Syscall(uintptr(unix.SYS_IOCTL), fd, BLKGETSIZE64, gomock.Any()).DoAndReturn(func(_, _, _, a3 uintptr) (uintptr, uintptr, Errno) { + *(*uint64)(unsafe.Pointer(a3)) = uint64(size) + return 0, 0, 0 + }) + + got, err := device.Size() + Expect(err).NotTo(HaveOccurred()) + Expect(got).To(BeEquivalentTo(size)) + }) + + It("issues blkdiscard", func() { + start := uint64(256) + count := uint64(512) + sysCall.EXPECT().Blkdiscard(fd, start, count).Return(nil) + + err = device.Discard(start, count) + Expect(err).NotTo(HaveOccurred()) + }) + + It("closes underlying file", func() { + closingError := errors.New("Closing error") + file.EXPECT().Close().Return(closingError) + + err = device.Close() + + Expect(err).To(MatchError(closingError)) + }) + }) + + When("underlying open error occurred", func() { + openError := errors.New("Open file error") + BeforeEach(func() { + fileOpener.EXPECT().Open(fileName, flag, os.ModeDevice).Return(nil, openError) + }) + JustBeforeEach(func() { + device, err = blockDeviceOpener.Open(fileName, 0) + Expect(err).To(MatchError(openError)) + }) + + It("returns nil device", func() { + Expect(device).To(BeNil()) + }) + }) +}) diff --git a/images/agent/src/internal/utils/commands.go b/images/agent/src/internal/utils/commands.go index ce97af78..6fe3db9e 100644 --- a/images/agent/src/internal/utils/commands.go +++ b/images/agent/src/internal/utils/commands.go @@ -24,6 +24,7 @@ import ( "fmt" golog "log" "os/exec" + "path/filepath" "regexp" "strings" "time" @@ -126,7 +127,7 @@ func GetAllLVs(ctx context.Context) (data []internal.LVData, command string, std func GetLV(vgName, lvName string) (lvData internal.LVData, command string, stdErr bytes.Buffer, err error) { var outs bytes.Buffer lvData = internal.LVData{} - lvPath := fmt.Sprintf("/dev/%s/%s", vgName, lvName) + lvPath := filepath.Join("/dev", vgName, lvName) args := []string{"lvs", "-o", "+vg_uuid,tags", "--units", "B", "--nosuffix", "--reportformat", "json", lvPath} extendedArgs := lvmStaticExtendedArgs(args) cmd := exec.Command(internal.NSENTERCmd, extendedArgs...) @@ -361,7 +362,7 @@ func ExtendVG(vgName string, paths []string) (string, error) { } func ExtendLV(size int64, vgName, lvName string) (string, error) { - args := []string{"lvextend", "-L", fmt.Sprintf("%dk", size/1024), fmt.Sprintf("/dev/%s/%s", vgName, lvName)} + args := []string{"lvextend", "-L", fmt.Sprintf("%dk", size/1024), filepath.Join("/dev", vgName, lvName)} extendedArgs := lvmStaticExtendedArgs(args) cmd := exec.Command(internal.NSENTERCmd, extendedArgs...) @@ -378,7 +379,7 @@ func ExtendLV(size int64, vgName, lvName string) (string, error) { } func ExtendLVFullVGSpace(vgName, lvName string) (string, error) { - args := []string{"lvextend", "-l", "100%VG", fmt.Sprintf("/dev/%s/%s", vgName, lvName)} + args := []string{"lvextend", "-l", "100%VG", filepath.Join("/dev", vgName, lvName)} extendedArgs := lvmStaticExtendedArgs(args) cmd := exec.Command(internal.NSENTERCmd, extendedArgs...) @@ -440,7 +441,7 @@ func RemovePV(pvNames []string) (string, error) { } func RemoveLV(vgName, lvName string) (string, error) { - args := []string{"lvremove", fmt.Sprintf("/dev/%s/%s", vgName, lvName), "-y"} + args := []string{"lvremove", filepath.Join("/dev", vgName, lvName), "-y"} extendedArgs := lvmStaticExtendedArgs(args) cmd := exec.Command(internal.NSENTERCmd, extendedArgs...) @@ -482,7 +483,7 @@ func VGChangeDelTag(vGName, tag string) (string, error) { } func LVChangeDelTag(lv internal.LVData, tag string) (string, error) { - tmpStr := fmt.Sprintf("/dev/%s/%s", lv.VGName, lv.LVName) + tmpStr := filepath.Join("/dev/%s/%s", lv.VGName, lv.LVName) var outs, stdErr bytes.Buffer args := []string{"lvchange", tmpStr, "--deltag", tag} extendedArgs := lvmStaticExtendedArgs(args) diff --git a/images/agent/src/internal/utils/syscall.go b/images/agent/src/internal/utils/syscall.go new file mode 100644 index 00000000..cadc6bfd --- /dev/null +++ b/images/agent/src/internal/utils/syscall.go @@ -0,0 +1,88 @@ +package utils + +import ( + "errors" + "fmt" + "unsafe" + + "golang.org/x/sys/unix" +) + +//nolint:revive +type Stat_t = unix.Stat_t +type Errno = unix.Errno + +type SysCall interface { + Fstat(fd int, stat *Stat_t) (err error) + Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) + Blkdiscard(fd uintptr, start, count uint64) error +} + +type osSyscall struct { +} + +var theSysCall = osSyscall{} + +//nolint:revive +func OsSysCall() osSyscall { + return theSysCall +} + +func (osSyscall) Fstat(fd int, stat *Stat_t) (err error) { + return unix.Fstat(fd, stat) +} + +func (osSyscall) Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) { + return unix.Syscall(trap, a1, a2, a3) +} + +func (osSyscall) Blkdiscard(fd uintptr, start, count uint64) error { + rng := struct { + start, count uint64 + }{ + start: start, + count: count, + } + _, _, errno := unix.Syscall( + unix.SYS_IOCTL, + fd, + uintptr(BLKDISCARD), + uintptr(unsafe.Pointer(&rng))) + + if errno != 0 { + err := errors.New(errno.Error()) + return fmt.Errorf("calling ioctl BLKDISCARD: %w", err) + } + return nil +} + +/* To find these constant run: +gcc -o test -x c - < +#include +#include +#include + +#define PRINT_CONSTANT(name, fmt) printf(#name " = " fmt "\n", name) + +int main() { + PRINT_CONSTANT(S_IFMT, "0x%x"); + PRINT_CONSTANT(S_IFBLK, "0x%x"); + PRINT_CONSTANT(BLKGETSIZE64, "0x%lx"); + PRINT_CONSTANT(BLKDISCARD, "0x%x"); + return 0; +} +EOF +*/ + +// TODO: It will be nice to figure them out during compilation or maybe runtime? +// +//nolint:revive +const ( + BLKDISCARD = 0x1277 + + BLKGETSIZE64 = uintptr(0x80081272) + + S_IFMT = 0xf000 /* type of file mask */ + S_IFBLK = 0x6000 /* block special */ +) diff --git a/images/agent/src/internal/utils/utils_test.go b/images/agent/src/internal/utils/utils_test.go new file mode 100644 index 00000000..860b124c --- /dev/null +++ b/images/agent/src/internal/utils/utils_test.go @@ -0,0 +1,13 @@ +package utils_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestUtils(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "utils Suite") +} diff --git a/images/agent/src/internal/utils/volume_cleanup_ce.go b/images/agent/src/internal/utils/volume_cleanup_ce.go index 374ecad0..7992625b 100644 --- a/images/agent/src/internal/utils/volume_cleanup_ce.go +++ b/images/agent/src/internal/utils/volume_cleanup_ce.go @@ -22,6 +22,6 @@ import ( "agent/internal/logger" ) -func VolumeCleanup(_ context.Context, _ logger.Logger, _, _, _ string) error { +func VolumeCleanup(_ context.Context, _ logger.Logger, _ BlockDeviceOpener, _, _, _ string) error { return fmt.Errorf("volume cleanup is not supported in your edition") } diff --git a/images/agent/src/internal/utils/volume_cleanup_ce_test.go b/images/agent/src/internal/utils/volume_cleanup_ce_test.go new file mode 100644 index 00000000..c9b02353 --- /dev/null +++ b/images/agent/src/internal/utils/volume_cleanup_ce_test.go @@ -0,0 +1,39 @@ +//go:build ce + +/* +Copyright 2025 Flant JSC +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "agent/internal/logger" + . "agent/internal/utils" +) + +var _ = Describe("Cleaning up volume", func() { + var log logger.Logger + BeforeEach(func() { + logger, err := logger.NewLogger(logger.WarningLevel) + log = logger + Expect(err).NotTo(HaveOccurred()) + }) + It("Unsupported", func() { + err := VolumeCleanup(context.Background(), log, nil, "", "", "") + Expect(err).To(MatchError("volume cleanup is not supported in your edition")) + }) +}) diff --git a/images/agent/src/internal/utils/volume_cleanup_ee.go b/images/agent/src/internal/utils/volume_cleanup_ee.go index 8d2613c5..24197aeb 100644 --- a/images/agent/src/internal/utils/volume_cleanup_ee.go +++ b/images/agent/src/internal/utils/volume_cleanup_ee.go @@ -12,119 +12,71 @@ import ( "errors" "fmt" "io" - "os" - "syscall" + "path/filepath" "time" - "unsafe" "github.com/deckhouse/sds-node-configurator/lib/go/common/pkg/feature" + "golang.org/x/sys/unix" "agent/internal/logger" ) -func VolumeCleanup(ctx context.Context, log logger.Logger, vgName, lvName, volumeCleanup string) error { +func VolumeCleanup(ctx context.Context, log logger.Logger, deviceOpener BlockDeviceOpener, vgName, lvName, volumeCleanup string) error { log.Trace(fmt.Sprintf("[VolumeCleanup] cleaning up volume %s in volume group %s using %s", lvName, vgName, volumeCleanup)) if !feature.VolumeCleanupEnabled() { return fmt.Errorf("volume cleanup is not supported in your edition") } - devicePath := fmt.Sprintf("/dev/%s/%s", vgName, lvName) + devicePath := filepath.Join("/dev", vgName, lvName) randomSource := "/dev/urandom" var err error - closingErrors := []error{} switch volumeCleanup { case "RandomFillSinglePass": - err = volumeCleanupOverwrite(ctx, log, &closingErrors, devicePath, randomSource, 1) + err = volumeCleanupOverwrite(ctx, log, deviceOpener, devicePath, randomSource, 1) case "RandomFillThreePass": - err = volumeCleanupOverwrite(ctx, log, &closingErrors, devicePath, randomSource, 3) + err = volumeCleanupOverwrite(ctx, log, deviceOpener, devicePath, randomSource, 3) case "Discard": - err = volumeCleanupDiscard(ctx, log, &closingErrors, devicePath) + err = volumeCleanupDiscard(ctx, log, deviceOpener, devicePath) default: return fmt.Errorf("unknown cleanup method %s", volumeCleanup) } - if err != nil && len(closingErrors) > 0 { - closingErrors = append([]error{err}, closingErrors...) - } - - if len(closingErrors) > 0 { - err = errors.Join(closingErrors...) - } - - if err == nil { - return nil - } - - log.Error(err, fmt.Sprintf("[VolumeCleanup] fail to cleanup volume %s", devicePath)) - return fmt.Errorf("cleaning volume %s: %w", devicePath, err) -} - -func volumeSize(log logger.Logger, device *os.File) (int64, error) { - log.Trace(fmt.Sprintf("[volumeSize] finding size of device %v", device)) - var stat syscall.Stat_t - log.Debug("[volumeSize] Calling fstat") - if err := syscall.Fstat(int(device.Fd()), &stat); err != nil { - log.Error(err, "[volumeSize] Calling fstat") - return 0, fmt.Errorf("fstat call failed: %w", err) - } - - if stat.Size > 0 { - log.Debug(fmt.Sprintf("[volumeSize] Size %d is valid.", stat.Size)) - return stat.Size, nil - } - - if stat.Mode&S_IFMT != S_IFBLK { - log.Debug(fmt.Sprintf("[volumeSize] Device mode %x", stat.Mode)) - return 0, fmt.Errorf("not a block device, mode: %x", stat.Mode) - } - - var blockDeviceSize uint64 - _, _, errno := syscall.Syscall( - syscall.SYS_IOCTL, - device.Fd(), - uintptr(BLKGETSIZE64), - uintptr(unsafe.Pointer(&blockDeviceSize))) - if errno != 0 { - err := errors.New(errno.Error()) - log.Error(err, "[volumeSize] calling ioctl BLKGETSIZE64") - return 0, fmt.Errorf("error calling ioctl BLKGETSIZE64: %w", err) - } - log.Debug(fmt.Sprintf("Block device size is %d", blockDeviceSize)) - if blockDeviceSize <= 0 { - return 0, fmt.Errorf("block size is invalid") + if err != nil { + log.Error(err, fmt.Sprintf("[VolumeCleanup] fail to cleanup volume %s", devicePath)) + return fmt.Errorf("cleaning volume %s: %w", devicePath, err) } - return int64(blockDeviceSize), nil + return nil } -func volumeCleanupOverwrite(_ context.Context, log logger.Logger, closingErrors *[]error, devicePath, inputPath string, passes int) error { +func volumeCleanupOverwrite(_ context.Context, log logger.Logger, deviceOpener BlockDeviceOpener, devicePath, inputPath string, passes int) (err error) { log.Trace(fmt.Sprintf("[volumeCleanupOverwrite] overwriting %s by %s in %d passes", devicePath, inputPath, passes)) - closeFile := func(file *os.File) { + closeFile := func(file BlockDevice) { log.Trace(fmt.Sprintf("[volumeCleanupOverwrite] closing %s", file.Name())) - err := file.Close() - if err != nil { - log.Error(err, fmt.Sprintf("[volumeCleanupOverwrite] While closing file %s", file.Name())) - *closingErrors = append(*closingErrors, fmt.Errorf("closing file %s: %w", file.Name(), err)) + closingErr := file.Close() + if closingErr != nil { + log.Error(closingErr, fmt.Sprintf("[volumeCleanupOverwrite] While closing file %s", file.Name())) + err = errors.Join(err, fmt.Errorf("closing file %s: %w", file.Name(), closingErr)) } } - input, err := os.OpenFile(inputPath, syscall.O_RDONLY, os.ModeDevice) + input, err := deviceOpener.Open(inputPath, unix.O_RDONLY) if err != nil { log.Error(err, fmt.Sprintf("[volumeCleanupOverwrite] Opening file %s", inputPath)) return fmt.Errorf("opening source device %s to wipe: %w", inputPath, err) } defer closeFile(input) - output, err := os.OpenFile(devicePath, syscall.O_DIRECT|syscall.O_RDWR, os.ModeDevice) + output, err := deviceOpener.Open(devicePath, unix.O_DIRECT|unix.O_RDWR) if err != nil { log.Error(err, fmt.Sprintf("[volumeCleanupOverwrite] Opening file %s", devicePath)) return fmt.Errorf("opening device %s to wipe: %w", devicePath, err) } defer closeFile(output) - bytesToWrite, err := volumeSize(log, output) + bytesToWrite, err := output.Size() if err != nil { log.Error(err, "[volumeCleanupOverwrite] Finding volume size") return fmt.Errorf("can't find the size of device %s: %w", devicePath, err) @@ -151,87 +103,36 @@ func volumeCleanupOverwrite(_ context.Context, log logger.Logger, closingErrors } } - return err -} - -/* To find these constant run: -gcc -o test -x c - < -#include -#include -#include - -#define PRINT_CONSTANT(name, fmt) printf(#name " = " fmt "\n", name) - -int main() { - PRINT_CONSTANT(S_IFMT, "0x%x"); - PRINT_CONSTANT(S_IFBLK, "0x%x"); - PRINT_CONSTANT(BLKGETSIZE64, "0x%lx"); - PRINT_CONSTANT(BLKDISCARD, "0x%x"); - return 0; -} -EOF -*/ - -// TODO: It will be nice to figure them out during compilation or maybe runtime? -// -//nolint:revive -const ( - BLKDISCARD = 0x1277 - - BLKGETSIZE64 = 0x80081272 - - S_IFMT = 0xf000 /* type of file mask */ - S_IFBLK = 0x6000 /* block special */ -) - -type Range struct { - start, count uint64 + return nil } -func volumeCleanupDiscard(_ context.Context, log logger.Logger, closingErrors *[]error, devicePath string) error { +func volumeCleanupDiscard(_ context.Context, log logger.Logger, deviceOpener BlockDeviceOpener, devicePath string) (err error) { log.Trace(fmt.Sprintf("[volumeCleanupDiscard] discarding %s", devicePath)) - device, err := os.OpenFile(devicePath, syscall.O_RDWR, os.ModeDevice) + device, err := deviceOpener.Open(devicePath, unix.O_RDWR) if err != nil { log.Error(err, fmt.Sprintf("[volumeCleanupDiscard] Opening device %s", devicePath)) return fmt.Errorf("opening device %s to wipe: %w", devicePath, err) } defer func() { log.Trace(fmt.Sprintf("Closing file %s", devicePath)) - err := device.Close() - if err != nil { - log.Error(err, fmt.Sprintf("[volumeCleanupDiscard] While closing deice %s", devicePath)) - *closingErrors = append(*closingErrors, fmt.Errorf("closing file %s: %w", device.Name(), err)) + closingErr := device.Close() + if closingErr != nil { + log.Error(closingErr, fmt.Sprintf("[volumeCleanupDiscard] While closing deice %s", devicePath)) + err = errors.Join(err, fmt.Errorf("closing file %s: %w", device.Name(), closingErr)) } }() - deviceSize, err := volumeSize(log, device) + deviceSize, err := device.Size() if err != nil { log.Error(err, fmt.Sprintf("[volumeCleanupDiscard] can't find the size of device %s", devicePath)) return fmt.Errorf("can't find the size of device %s: %w", devicePath, err) } - rng := Range{ - start: 0, - count: uint64(deviceSize), - } - - log.Debug(fmt.Sprintf("[volumeCleanupDiscard] calling BLKDISCARD fd: %d, range %v", device.Fd(), rng)) start := time.Now() + log.Debug(fmt.Sprintf("[volumeCleanupDiscard] Discarding all %d bytes", deviceSize)) + defer func() { + log.Info(fmt.Sprintf("[volumeCleanupDiscard] Discarding is done in %s", time.Since(start).String())) + }() - _, _, errno := syscall.Syscall( - syscall.SYS_IOCTL, - device.Fd(), - uintptr(BLKDISCARD), - uintptr(unsafe.Pointer(&rng))) - - log.Info(fmt.Sprintf("[volumeCleanupDiscard] BLKDISCARD is done in %s", time.Since(start).String())) - - if errno != 0 { - err := errors.New(errno.Error()) - log.Error(err, "[volumeCleanupDiscard] error calling BLKDISCARD") - return fmt.Errorf("calling ioctl BLKDISCARD: %s", err) - } - - return nil + return device.Discard(0, uint64(deviceSize)) } diff --git a/images/agent/src/internal/utils/volume_cleanup_ee_test.go b/images/agent/src/internal/utils/volume_cleanup_ee_test.go new file mode 100644 index 00000000..f1e40a69 --- /dev/null +++ b/images/agent/src/internal/utils/volume_cleanup_ee_test.go @@ -0,0 +1,407 @@ +//go:build !ce + +/* +Copyright 2025 Flant JSC +Licensed under the Deckhouse Platform Enterprise Edition (EE) license. See https://github.com/deckhouse/deckhouse/blob/main/ee/LICENSE +*/ + +package utils_test + +import ( + "context" + "errors" + "fmt" + "io" + "path/filepath" + + "github.com/deckhouse/sds-node-configurator/lib/go/common/pkg/feature" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" + "golang.org/x/sys/unix" + + "agent/internal/logger" + . "agent/internal/mock_utils" + . "agent/internal/utils" +) + +var _ = Describe("Cleaning up volume", func() { + var log logger.Logger + var ctrl *gomock.Controller + var opener *MockBlockDeviceOpener + var device *MockBlockDevice + var err error + vgName := "vg" + lvName := "lv" + var method string + BeforeEach(func() { + ctrl = gomock.NewController(GinkgoT()) + opener = NewMockBlockDeviceOpener(ctrl) + device = NewMockBlockDevice(ctrl) + + log, err = logger.NewLogger(logger.WarningLevel) + Expect(err).NotTo(HaveOccurred()) + }) + + doCall := func() { + err = VolumeCleanup(context.Background(), log, opener, vgName, lvName, method) + if !feature.VolumeCleanupEnabled() { + Expect(err).To(MatchError("volume cleanup is not supported in your edition")) + } + } + + When("method is unknown", func() { + BeforeEach(func() { + method = "some" + }) + It("fails", func() { + doCall() + if feature.VolumeCleanupEnabled() { + Expect(err).To(MatchError(fmt.Sprintf("unknown cleanup method %s", method))) + } + }) + }) + When("method is Discard", func() { + BeforeEach(func() { + method = "Discard" + }) + + When("can't open device", func() { + deviceOpenError := errors.New("can't open device") + BeforeEach(func() { + if feature.VolumeCleanupEnabled() { + opener.EXPECT().Open(filepath.Join("/dev", vgName, lvName), unix.O_RDWR).Return(nil, deviceOpenError) + } + }) + It("fails with same error", func() { + doCall() + if feature.VolumeCleanupEnabled() { + Expect(err).To(MatchError(deviceOpenError)) + } + }) + }) + + When("device opened", func() { + BeforeEach(func() { + if feature.VolumeCleanupEnabled() { + device = NewMockBlockDevice(ctrl) + name := filepath.Join("/dev", vgName, lvName) + opener.EXPECT().Open(name, unix.O_RDWR).Return(device, nil) + device.EXPECT().Name().AnyTimes().Return(name) + } + }) + deviceSize := 1024 + When("discard succeed", func() { + BeforeEach(func() { + if feature.VolumeCleanupEnabled() { + device.EXPECT().Size().Return(int64(deviceSize), nil) + device.EXPECT().Discard(uint64(0), uint64(deviceSize)).Return(nil) + } + }) + When("no closing error", func() { + BeforeEach(func() { + if feature.VolumeCleanupEnabled() { + device.EXPECT().Close().Return(nil) + } + }) + It("calls device discard", func() { + doCall() + if feature.VolumeCleanupEnabled() { + Expect(err).ToNot(HaveOccurred()) + } + }) + }) + When("cannot close", func() { + closingError := errors.New("closing error") + BeforeEach(func() { + if feature.VolumeCleanupEnabled() { + device.EXPECT().Close().Return(closingError) + } + }) + It("fails with closing error", func() { + doCall() + if feature.VolumeCleanupEnabled() { + Expect(err).To(MatchError(closingError)) + } + }) + }) + }) + When("discard fails", func() { + discardError := errors.New("discard error") + BeforeEach(func() { + if feature.VolumeCleanupEnabled() { + device.EXPECT().Size().Return(int64(deviceSize), nil) + device.EXPECT().Discard(uint64(0), uint64(deviceSize)).Return(discardError) + } + }) + When("no closing error", func() { + BeforeEach(func() { + if feature.VolumeCleanupEnabled() { + device.EXPECT().Close().Return(nil) + } + }) + It("fails with matched error", func() { + doCall() + if feature.VolumeCleanupEnabled() { + Expect(err).To(MatchError(discardError)) + } + }) + }) + When("closing error", func() { + closingError := errors.New("closing error") + BeforeEach(func() { + if feature.VolumeCleanupEnabled() { + device.EXPECT().Close().Return(closingError) + } + }) + It("fails with matched errors", func() { + doCall() + if feature.VolumeCleanupEnabled() { + Expect(err).To(MatchError(discardError)) + Expect(err).To(MatchError(closingError)) + } + }) + }) + }) + }) + }) + inputName := "/dev/urandom" + deviceSize := 1024 * 1024 * 50 + When("method is RandomFill", func() { + var passCount int + var expectedWritePosition int + bufferSize := 1024 * 1024 * 4 + When("input open succeed", func() { + var input *MockBlockDevice + var inputClosingError error + BeforeEach(func() { + input = NewMockBlockDevice(ctrl) + if feature.VolumeCleanupEnabled() { + input.EXPECT().Name().AnyTimes().Return(inputName) + opener.EXPECT().Open(inputName, unix.O_RDONLY).DoAndReturn(func(_ string, _ int) (BlockDevice, error) { + input.EXPECT().Close().Return(inputClosingError) + return input, nil + }) + } + }) + WhenInputClosingErrorVariants := func(f func(), no_error func()) { + When("no input closing error", func() { + BeforeEach(func() { + inputClosingError = nil + }) + f() + no_error() + }) + When("input closing error", func() { + BeforeEach(func() { + inputClosingError = errors.New("can't close device") + }) + f() + JustAfterEach(func() { + if feature.VolumeCleanupEnabled() { + Expect(err).To(MatchError(inputClosingError)) + } + }) + }) + } + deviceName := filepath.Join("/dev", vgName, lvName) + When("device open succeed", func() { + var deviceClosingError error + BeforeEach(func() { + device = NewMockBlockDevice(ctrl) + if feature.VolumeCleanupEnabled() { + opener.EXPECT().Open(deviceName, unix.O_DIRECT|unix.O_RDWR).DoAndReturn(func(_ string, _ int) (BlockDevice, error) { + device.EXPECT().Close().Return(deviceClosingError) + return device, nil + }) + device.EXPECT().Size().Return(int64(deviceSize), nil) + device.EXPECT().Name().AnyTimes().Return(deviceName) + } + }) + WhenClosingErrorVariants := func(f func(), no_error func()) { + When("no device closing error", func() { + BeforeEach(func() { + deviceClosingError = nil + }) + WhenInputClosingErrorVariants(f, no_error) + }) + When("device closing error", func() { + BeforeEach(func() { + deviceClosingError = errors.New("can't close device") + }) + WhenInputClosingErrorVariants(f, func() {}) + JustAfterEach(func() { + if feature.VolumeCleanupEnabled() { + Expect(err).To(MatchError(deviceClosingError)) + } + }) + }) + } + When("read succeed", func() { + var readMissingBytes int + var totalBytesRead int + var lastBytesRead int + JustBeforeEach(func() { + totalBytesRead = 0 + buffersToReadPerPass := deviceSize / bufferSize + if 0 != deviceSize%bufferSize { + buffersToReadPerPass++ + } + expectedTotalBytesRead := deviceSize * passCount + readLimit := expectedTotalBytesRead - readMissingBytes + + expectedReadCallCount := buffersToReadPerPass * passCount + + if readLimit < expectedTotalBytesRead { + // Extra call for EOF + expectedReadCallCount++ + } + + if feature.VolumeCleanupEnabled() { + input.EXPECT().Read(gomock.Any()).DoAndReturn(func(p []byte) (int, error) { + GinkgoWriter.Printf("Reading %d from input\n", len(p)) + Expect(len(p) <= bufferSize).To(BeTrue()) + lastBytesRead = min(len(p), readLimit-totalBytesRead) + totalBytesRead += lastBytesRead + if lastBytesRead == 0 { + return 0, io.EOF + } + return lastBytesRead, nil + }).Times(expectedReadCallCount) + } + }) + When("write succeed", func() { + var totalBytesWritten int + var lastPassBytesWritten int + JustBeforeEach(func() { + expectedWritePosition = 0 + totalBytesWritten = 0 + lastPassBytesWritten = 0 + if feature.VolumeCleanupEnabled() { + device.EXPECT().WriteAt(gomock.Any(), gomock.Any()).DoAndReturn(func(p []byte, off int64) (int, error) { + GinkgoWriter.Printf("Writing to device [%d, %d]\n", off, off+int64(len(p))) + Expect(off).To(BeEquivalentTo(int64(expectedWritePosition))) + expectedWritePosition += len(p) + lastPassBytesWritten += len(p) + totalBytesWritten += len(p) + if expectedWritePosition >= deviceSize { + expectedWritePosition = 0 + + Expect(lastPassBytesWritten).To(Equal(deviceSize)) + lastPassBytesWritten = 0 + + Expect(len(p) <= bufferSize).To(BeTrue()) + } else { + Expect(len(p)).To(BeEquivalentTo(lastBytesRead)) + } + return len(p), nil + }).AnyTimes() + } + }) + When("input has enough bytes to read", func() { + BeforeEach(func() { + readMissingBytes = 0 + }) + + When("SinglePass", func() { + BeforeEach(func() { + method = "RandomFillSinglePass" + passCount = 1 + }) + WhenClosingErrorVariants(func() { + It("fails the device", doCall) + }, func() { + JustAfterEach(func() { + if feature.VolumeCleanupEnabled() { + Expect(err).ToNot(HaveOccurred()) + } + }) + }) + }) + When("ThreePass", func() { + BeforeEach(func() { + method = "RandomFillThreePass" + passCount = 3 + }) + WhenClosingErrorVariants(func() { + It("fills the device", doCall) + }, func() { + JustAfterEach(func() { + if feature.VolumeCleanupEnabled() { + Expect(err).ToNot(HaveOccurred()) + } + }) + }) + }) + + }) + When("input doesn't have enough bytes to read", func() { + BeforeEach(func() { + readMissingBytes = 512 + }) + + When("SinglePass", func() { + BeforeEach(func() { + method = "RandomFillSinglePass" + passCount = 1 + }) + + WhenClosingErrorVariants(func() { + It("fails", func() { + doCall() + if feature.VolumeCleanupEnabled() { + Expect(err).To(HaveOccurred()) + } + }) + }, func() {}) + }) + When("ThreePass", func() { + BeforeEach(func() { + method = "RandomFillThreePass" + passCount = 3 + }) + WhenClosingErrorVariants(func() { + It("fails", func() { + doCall() + if feature.VolumeCleanupEnabled() { + Expect(err).To(HaveOccurred()) + } + }) + }, func() {}) + }) + }) + }) + }) + }) + When("device open failed", func() { + deviceOpenError := errors.New("input open error") + BeforeEach(func() { + if feature.VolumeCleanupEnabled() { + opener.EXPECT().Open(deviceName, unix.O_DIRECT|unix.O_RDWR).Return(nil, deviceOpenError) + } + }) + It("fails with matched error", func() { + doCall() + if feature.VolumeCleanupEnabled() { + Expect(err).To(MatchError(deviceOpenError)) + } + }) + }) + }) + When("input open failed", func() { + inputOpenError := errors.New("input open error") + BeforeEach(func() { + if feature.VolumeCleanupEnabled() { + opener.EXPECT().Open(inputName, unix.O_RDONLY).Return(nil, inputOpenError) + } + }) + + It("fails with matched error", func() { + doCall() + if feature.VolumeCleanupEnabled() { + Expect(err).To(MatchError(inputOpenError)) + } + }) + }) + }) +})