Skip to content

Commit 4eea777

Browse files
authored
Merge pull request #19 from 27149chen/delete_volume
delete individual volume
2 parents 959f12c + c725e6b commit 4eea777

File tree

3 files changed

+344
-6
lines changed

3 files changed

+344
-6
lines changed

iscsi/iscsi.go

Lines changed: 86 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() {
@@ -352,6 +357,81 @@ func Disconnect(tgtIqn string, portals []string) error {
352357
return err
353358
}
354359

360+
// DisconnectVolume removes a volume from a Linux host.
361+
func DisconnectVolume(c Connector) error {
362+
// Steps to safely remove an iSCSI storage volume from a Linux host are as following:
363+
// 1. Unmount the disk from a filesystem on the system.
364+
// 2. Flush the multipath map for the disk we’re removing (if multipath is enabled).
365+
// 3. Remove the physical disk entities that Linux maintains.
366+
// 4. Take the storage volume (disk) offline on the storage subsystem.
367+
// 5. Rescan the iSCSI sessions.
368+
//
369+
// DisconnectVolume focuses on step 2 and 3.
370+
// Note: make sure the volume is already unmounted before calling this method.
371+
372+
debug.Printf("Disconnecting volume in path %s.\n", c.DevicePath)
373+
if c.Multipath {
374+
debug.Printf("Removing multipath device.\n")
375+
devices, err := GetSysDevicesFromMultipathDevice(c.DevicePath)
376+
if err != nil {
377+
return err
378+
}
379+
err := FlushMultipathDevice(c.DevicePath)
380+
if err != nil {
381+
return err
382+
}
383+
debug.Printf("Found multipath slaves %v, removing all of them.\n", devices)
384+
if err := RemovePhysicalDevice(devices...); err != nil {
385+
return err
386+
}
387+
} else {
388+
debug.Printf("Removing normal device.\n")
389+
if err := RemovePhysicalDevice(c.DevicePath); err != nil {
390+
return err
391+
}
392+
}
393+
394+
debug.Printf("Finished disconnecting volume.\n")
395+
return nil
396+
}
397+
398+
// RemovePhysicalDevice removes device(s) sdx from a Linux host.
399+
func RemovePhysicalDevice(devices ...string) error {
400+
debug.Printf("Removing scsi device %v.\n", devices)
401+
var errs []error
402+
for _, deviceName := range devices {
403+
if deviceName == "" {
404+
continue
405+
}
406+
407+
debug.Printf("Delete scsi device %v.\n", deviceName)
408+
// Remove a scsi device by executing 'echo "1" > /sys/block/sdx/device/delete
409+
filename := filepath.Join(sysBlockPath, deviceName, "device", "delete")
410+
if f, err := os.OpenFile(filename, os.O_TRUNC|os.O_WRONLY, 0200); err != nil {
411+
if os.IsNotExist(err) {
412+
continue
413+
} else {
414+
debug.Printf("Error while opening file %v: %v\n", filename, err)
415+
errs = append(errs, err)
416+
continue
417+
}
418+
} else {
419+
defer f.Close()
420+
if _, err := f.WriteString("1"); err != nil {
421+
debug.Printf("Error while writing to file %v: %v", filename, err)
422+
errs = append(errs, err)
423+
continue
424+
}
425+
}
426+
}
427+
428+
if len(errs) > 0 {
429+
return errs[0]
430+
}
431+
debug.Println("Finshed removing SCSI devices.")
432+
return nil
433+
}
434+
355435
// PersistConnector persists the provided Connector to the specified file (ie /var/lib/pfile/myConnector.json)
356436
func PersistConnector(c *Connector, filePath string) error {
357437
//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+
execWithTimeout = fakeExecWithTimeout
332+
devicePath := multipathDevice
333+
334+
tests := []struct {
335+
name string
336+
timeout bool
337+
removeDevice bool
338+
wantErr bool
339+
cmdError error
340+
}{
341+
{"DisconnectMultipathVolume", false, false, false, nil},
342+
{"DisconnectMultipathVolumeFlushFailed", false, false, true, fmt.Errorf("error")},
343+
{"DisconnectMultipathVolumeFlushTimeout", true, false, true, nil},
344+
{"DisconnectNonexistentMultipathVolume", false, true, false, fmt.Errorf("error")},
345+
}
346+
for _, tt := range tests {
347+
t.Run(tt.name, func(t *testing.T) {
348+
349+
tmpDir, err := ioutil.TempDir("", "")
350+
if err != nil {
351+
t.Errorf("can not create temp directory: %v", err)
352+
return
353+
}
354+
sysBlockPath = tmpDir
355+
devPath = filepath.Join(tmpDir, "dev")
356+
defer os.RemoveAll(tmpDir)
357+
358+
err = preparePaths(tmpDir)
359+
if err != nil {
360+
t.Errorf("can not create temp directories and files: %v", err)
361+
return
362+
}
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 && !tt.wantErr {
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)