Skip to content

Commit a11bd0f

Browse files
author
Lou
committed
delete individual volume
Signed-off-by: Lou <[email protected]>
1 parent 4a4d032 commit a11bd0f

File tree

3 files changed

+331
-6
lines changed

3 files changed

+331
-6
lines changed

iscsi/iscsi.go

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ import (
1919
const defaultPort = "3260"
2020

2121
var (
22-
debug *log.Logger
23-
execCommand = exec.Command
22+
debug *log.Logger
23+
execCommand = exec.Command
24+
execWithTimeout = ExecWithTimeout
2425
)
2526

2627
type statFunc func(string) (os.FileInfo, error)
@@ -50,10 +51,14 @@ type Connector struct {
5051
SessionSecrets Secrets `json:"session_secrets"`
5152
Interface string `json:"interface"`
5253
Multipath bool `json:"multipath"`
53-
RetryCount int32 `json:"retry_count"`
54-
CheckInterval int32 `json:"check_interval"`
55-
DoDiscovery bool `json:"do_discovery"`
56-
DoCHAPDiscovery bool `json:"do_chap_discovery"`
54+
55+
// DevicePath is dm-x for a multipath device, and sdx for a normal device.
56+
DevicePath string `json:"device_path"`
57+
58+
RetryCount int32 `json:"retry_count"`
59+
CheckInterval int32 `json:"check_interval"`
60+
DoDiscovery bool `json:"do_discovery"`
61+
DoCHAPDiscovery bool `json:"do_chap_discovery"`
5762
}
5863

5964
func init() {
@@ -350,6 +355,68 @@ func Disconnect(tgtIqn string, portals []string) error {
350355
return err
351356
}
352357

358+
// DisconnectVolume removes a volume from a Linux host.
359+
func DisconnectVolume(c Connector) error {
360+
// Steps to safely remove an iSCSI storage volume from a Linux host are as following:
361+
// 1. Unmount the disk from a filesystem on the system.
362+
// 2. Flush the multipath map for the disk we’re removing (if multipath is enabled).
363+
// 3. Remove the physical disk entities that Linux maintains.
364+
// 4. Take the storage volume (disk) offline on the storage subsystem.
365+
// 5. Rescan the iSCSI sessions.
366+
//
367+
// DisconnectVolume focuses on step 2 and 3.
368+
// Note: make sure the volume is already unmounted before calling this method.
369+
if c.Multipath {
370+
err := Flush(c.DevicePath)
371+
if err != nil {
372+
return err
373+
}
374+
devices, err := GetSysDevicesFromMultipathDevice(c.DevicePath)
375+
if err != nil {
376+
return err
377+
}
378+
return RemovePhysicalDevice(devices...)
379+
}
380+
return RemovePhysicalDevice(c.DevicePath)
381+
}
382+
383+
// RemovePhysicalDevice removes device(s) sdx from a Linux host.
384+
func RemovePhysicalDevice(devices ...string) error {
385+
debug.Printf("Removing scsi device %v.\n", devices)
386+
var errs []error
387+
for _, deviceName := range devices {
388+
if deviceName == "" {
389+
continue
390+
}
391+
392+
debug.Printf("Delete scsi device %v.\n", deviceName)
393+
// Remove a scsi device by executing 'echo "1" > /sys/block/sdx/device/delete
394+
filename := filepath.Join(sysBlockPath, deviceName, "device", "delete")
395+
if f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0200); err != nil {
396+
if os.IsNotExist(err) {
397+
continue
398+
} else {
399+
debug.Printf("Error while opening file %v: %v\n", filename, err)
400+
errs = append(errs, err)
401+
continue
402+
}
403+
} else {
404+
defer f.Close()
405+
if _, err := f.WriteString("1"); err != nil {
406+
debug.Printf("Error while writing to file %v: %v", filename, err)
407+
errs = append(errs, err)
408+
continue
409+
}
410+
}
411+
}
412+
413+
if len(errs) > 0 {
414+
return errs[0]
415+
}
416+
debug.Println("Finshed to remove SCSI devices.")
417+
return nil
418+
}
419+
353420
// PersistConnector persists the provided Connector to the specified file (ie /var/lib/pfile/myConnector.json)
354421
func PersistConnector(c *Connector, filePath string) error {
355422
//file := path.Join("mnt", c.VolumeName+".json")

iscsi/iscsi_test.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package iscsi
22

33
import (
4+
"context"
45
"fmt"
6+
"io/ioutil"
57
"os"
68
"os/exec"
9+
"path/filepath"
710
"reflect"
811
"strconv"
912
"testing"
13+
"time"
1014
)
1115

1216
var nodeDB = `
@@ -79,10 +83,16 @@ node.conn[0].iscsi.OFMarker = No
7983
var emptyTransportName = "iface.transport_name = \n"
8084
var emptyDbRecord = "\n\n\n"
8185
var testCmdOutput = ""
86+
var testCmdTimeout = false
8287
var testCmdError error
88+
var testExecWithTimeoutError error
8389
var mockedExitStatus = 0
8490
var mockedStdout string
8591

92+
var normalDevice = "sda"
93+
var multipathDevice = "dm-1"
94+
var slaves = []string{"sdb", "sdc"}
95+
8696
type testCmdRunner struct{}
8797

8898
func fakeExecCommand(command string, args ...string) *exec.Cmd {
@@ -96,6 +106,13 @@ func fakeExecCommand(command string, args ...string) *exec.Cmd {
96106
return cmd
97107
}
98108

109+
func fakeExecWithTimeout(command string, args []string, timeout time.Duration) ([]byte, error) {
110+
if testCmdTimeout {
111+
return nil, context.DeadlineExceeded
112+
}
113+
return []byte(testCmdOutput), testExecWithTimeoutError
114+
}
115+
99116
func TestExecCommandHelper(t *testing.T) {
100117
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
101118
return
@@ -111,6 +128,33 @@ func (tr testCmdRunner) execCmd(cmd string, args ...string) (string, error) {
111128

112129
}
113130

131+
func preparePaths(sysBlockPath string) error {
132+
for _, d := range append(slaves, normalDevice) {
133+
devicePath := filepath.Join(sysBlockPath, d, "device")
134+
err := os.MkdirAll(devicePath, os.ModePerm)
135+
if err != nil {
136+
return err
137+
}
138+
err = ioutil.WriteFile(filepath.Join(devicePath, "delete"), []byte(""), 0600)
139+
if err != nil {
140+
return err
141+
}
142+
}
143+
for _, s := range slaves {
144+
err := os.MkdirAll(filepath.Join(sysBlockPath, multipathDevice, "slaves", s), os.ModePerm)
145+
if err != nil {
146+
return err
147+
}
148+
}
149+
150+
err := os.MkdirAll(filepath.Join(sysBlockPath, "dev", multipathDevice), os.ModePerm)
151+
if err != nil {
152+
return err
153+
}
154+
return nil
155+
156+
}
157+
114158
func Test_parseSessions(t *testing.T) {
115159
var sessions []iscsiSession
116160
output := "tcp: [2] 192.168.1.107:3260,1 iqn.2010-10.org.openstack:volume-eb393993-73d0-4e39-9ef4-b5841e244ced (non-flash)\n" +
@@ -226,3 +270,130 @@ func Test_sessionExists(t *testing.T) {
226270
})
227271
}
228272
}
273+
274+
func Test_DisconnectNormalVolume(t *testing.T) {
275+
276+
tmpDir, err := ioutil.TempDir("", "")
277+
if err != nil {
278+
t.Errorf("can not create temp directory: %v", err)
279+
return
280+
}
281+
sysBlockPath = tmpDir
282+
defer os.RemoveAll(tmpDir)
283+
284+
err = preparePaths(tmpDir)
285+
if err != nil {
286+
t.Errorf("can not create temp directories and files: %v", err)
287+
return
288+
}
289+
290+
execWithTimeout = fakeExecWithTimeout
291+
devicePath := normalDevice
292+
293+
tests := []struct {
294+
name string
295+
removeDevice bool
296+
wantErr bool
297+
}{
298+
{"DisconnectNormalVolume", false, false},
299+
{"DisconnectNonexistentNormalVolume", true, false},
300+
}
301+
for _, tt := range tests {
302+
t.Run(tt.name, func(t *testing.T) {
303+
if tt.removeDevice {
304+
os.RemoveAll(filepath.Join(sysBlockPath, devicePath))
305+
}
306+
c := Connector{Multipath: false, DevicePath: devicePath}
307+
err := DisconnectVolume(c)
308+
if (err != nil) != tt.wantErr {
309+
t.Errorf("DisconnectVolume() error = %v, wantErr %v", err, tt.wantErr)
310+
return
311+
}
312+
313+
if !tt.removeDevice {
314+
deleteFile := filepath.Join(sysBlockPath, devicePath, "device", "delete")
315+
out, err := ioutil.ReadFile(deleteFile)
316+
if err != nil {
317+
t.Errorf("can not read file %v: %v", deleteFile, err)
318+
return
319+
}
320+
if string(out) != "1" {
321+
t.Errorf("file content mismatch, got = %s, want = 1", string(out))
322+
return
323+
}
324+
}
325+
})
326+
}
327+
}
328+
329+
func Test_DisconnectMultipathVolume(t *testing.T) {
330+
331+
tmpDir, err := ioutil.TempDir("", "")
332+
if err != nil {
333+
t.Errorf("can not create temp directory: %v", err)
334+
return
335+
}
336+
sysBlockPath = tmpDir
337+
devPath = filepath.Join(tmpDir, "dev")
338+
defer os.RemoveAll(tmpDir)
339+
340+
err = preparePaths(tmpDir)
341+
if err != nil {
342+
t.Errorf("can not create temp directories and files: %v", err)
343+
return
344+
}
345+
346+
execWithTimeout = fakeExecWithTimeout
347+
devicePath := multipathDevice
348+
349+
tests := []struct {
350+
name string
351+
timeout bool
352+
removeDevice bool
353+
wantErr bool
354+
cmdError error
355+
}{
356+
{"DisconnectMultipathVolume", false, false, false, nil},
357+
{"DisconnectMultipathVolumeFlushFailed", false, false, true, fmt.Errorf("error")},
358+
{"DisconnectMultipathVolumeFlushTimeout", true, false, true, nil},
359+
{"DisconnectNonexistentMultipathVolume", false, false, true, fmt.Errorf("error")},
360+
}
361+
for _, tt := range tests {
362+
t.Run(tt.name, func(t *testing.T) {
363+
testExecWithTimeoutError = tt.cmdError
364+
testCmdTimeout = tt.timeout
365+
if tt.removeDevice {
366+
os.RemoveAll(filepath.Join(sysBlockPath, devicePath))
367+
os.RemoveAll(devPath)
368+
}
369+
c := Connector{Multipath: true, DevicePath: devicePath}
370+
err := DisconnectVolume(c)
371+
if (err != nil) != tt.wantErr {
372+
t.Errorf("DisconnectVolume() error = %v, wantErr %v", err, tt.wantErr)
373+
return
374+
}
375+
if tt.timeout {
376+
if err != context.DeadlineExceeded {
377+
t.Errorf("DisconnectVolume() error = %v, wantErr %v", err, context.DeadlineExceeded)
378+
return
379+
}
380+
}
381+
382+
if !tt.removeDevice {
383+
for _, s := range slaves {
384+
deleteFile := filepath.Join(sysBlockPath, s, "device", "delete")
385+
out, err := ioutil.ReadFile(deleteFile)
386+
if err != nil {
387+
t.Errorf("can not read file %v: %v", deleteFile, err)
388+
return
389+
}
390+
if string(out) != "1" {
391+
t.Errorf("file content mismatch, got = %s, want = 1", string(out))
392+
return
393+
}
394+
}
395+
396+
}
397+
})
398+
}
399+
}

0 commit comments

Comments
 (0)