├── .gitignore ├── cmd └── iscsistart │ ├── .gitignore │ └── iscsistart.go ├── testdata ├── 1MB.ext4_vfat └── pristine-vfat-disk ├── go.mod ├── go.sum ├── README.md ├── .circleci └── config.yml ├── initiator_test.go ├── LICENSE ├── initiator_integration_test.go ├── netlink.go └── initiator.go /.gitignore: -------------------------------------------------------------------------------- 1 | .bb 2 | iscsistart 3 | -------------------------------------------------------------------------------- /cmd/iscsistart/.gitignore: -------------------------------------------------------------------------------- 1 | iscsistart 2 | -------------------------------------------------------------------------------- /testdata/1MB.ext4_vfat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-root/iscsinl/HEAD/testdata/1MB.ext4_vfat -------------------------------------------------------------------------------- /testdata/pristine-vfat-disk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/u-root/iscsinl/HEAD/testdata/pristine-vfat-disk -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/u-root/iscsinl 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/vishvananda/netlink v1.1.0 7 | golang.org/x/sys v0.0.0-20200121082415-34d275377bf9 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= 2 | github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= 3 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k= 4 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= 5 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 6 | golang.org/x/sys v0.0.0-20200121082415-34d275377bf9 h1:N19i1HjUnR7TF7rMt8O4p3dLvqvmYyzB6ifMFmrbY50= 7 | golang.org/x/sys v0.0.0-20200121082415-34d275377bf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iscsinl 2 | 3 | [![CircleCI](https://circleci.com/gh/u-root/iscsinl.svg?style=svg)](https://circleci.com/gh/u-root/iscsinl) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/u-root/iscsinl)](https://goreportcard.com/report/github.com/u-root/iscsinl) 5 | [![GoDoc](https://godoc.org/github.com/u-root/iscsinl?status.svg)](https://godoc.org/github.com/u-root/iscsinl) 6 | 7 | Go iSCSI netlink library 8 | 9 | ## TODO 10 | 11 | Currently, after establishing a successful iscsi session with target, iscsinl scans all LUNS i.e 12 | uses wild card `- - -` while writing to `/sys/class/scsi_host/host%d/scan.` 13 | The three `- - -` stand for channel, SCSI target ID, and LUN, where - means all. 14 | 15 | In future we would like the iscsnl initiator code to accept LUN as an input argument 16 | just like initiatorName, so that user can customize which LUN s(he) wants to be scanned. 17 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | templates: 4 | golang-template: &golang-template 5 | docker: 6 | - image: uroottest/test-image-amd64:v3.2.13 7 | working_directory: /go/src/github.com/u-root/iscsinl 8 | environment: 9 | - GOPATH: "/go" 10 | - CGO_ENABLED: 0 11 | # Double all timeouts for QEMU VM tests since they run without KVM. 12 | - UROOT_QEMU_TIMEOUT_X: 2 13 | - GO111MODULE: "off" 14 | 15 | workflows: 16 | version: 2 17 | build: 18 | jobs: 19 | - build 20 | 21 | jobs: 22 | build: 23 | <<: *golang-template 24 | steps: 25 | - checkout 26 | - run: go env 27 | - run: go get -v -t -d github.com/u-root/u-root 28 | - run: go get -v -d ./... 29 | - run: GO111MODULE=on go mod vendor 30 | - run: go test -timeout 15m -v ./... 31 | - run: | 32 | go get github.com/mitchellh/gox 33 | gox -os="linux" -arch="386 amd64 arm arm64 ppc64 ppc64le s390x mips mipsle mips64 mips64le" ./... 34 | 35 | -------------------------------------------------------------------------------- /cmd/iscsistart/iscsistart.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | 7 | "github.com/u-root/iscsinl" 8 | ) 9 | 10 | var ( 11 | targetAddr = flag.String("addr", "instance-1:3260", "target addr") 12 | initiatorName = flag.String("i", "hostname:iscsi_startup.go", "initiator name") 13 | volumeName = flag.String("volume", "FOO", "volume name") 14 | monitorNetlink = flag.Bool("monitorNetlink", false, "Set to true to monitor netlink socket until killed") 15 | teardownSession = flag.Int("teardownSid", -1, "Set to teardown a session") 16 | cmdsMax = flag.Int("cmdsMax", 128, "Max outstanding iSCSI commands") 17 | queueDepth = flag.Int("queueDepth", 16, "Max outstanding IOs") 18 | scheduler = flag.String("scheduler", "noop", "block scheduler for session") 19 | ) 20 | 21 | func main() { 22 | flag.Parse() 23 | 24 | if *teardownSession != -1 { 25 | if err := iscsinl.TearDownIscsi((uint32)(*teardownSession), 0); err != nil { 26 | log.Fatal(err) 27 | } 28 | return 29 | } 30 | 31 | devices, err := iscsinl.MountIscsi( 32 | iscsinl.WithInitiator(*initiatorName), 33 | iscsinl.WithTarget(*targetAddr, *volumeName), 34 | iscsinl.WithCmdsMax(uint16(*cmdsMax)), 35 | iscsinl.WithQueueDepth(uint16(*queueDepth)), 36 | iscsinl.WithScheduler(*scheduler), 37 | ) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | for i := range devices { 43 | log.Printf("Mounted at dev %v", devices[i]) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /initiator_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the u-root Authors. All rights reserved 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package iscsinl 6 | 7 | /*import ( 8 | "fmt" 9 | "log" 10 | "os" 11 | "testing" 12 | 13 | "github.com/u-root/u-root/pkg/mount" 14 | "github.com/u-root/u-root/pkg/sh" 15 | "github.com/u-root/u-root/pkg/testutil" 16 | ) 17 | 18 | func TestMain(m *testing.M) { 19 | if os.Getuid() == 0 { 20 | if err := sh.RunWithLogs("dhclient", "-ipv6=false"); err != nil { 21 | log.Fatalf("could not configure network for tests: %v", err) 22 | } 23 | } 24 | 25 | os.Exit(m.Run()) 26 | } 27 | 28 | func TestMountIscsi(t *testing.T) { 29 | testutil.SkipIfNotRoot(t) 30 | 31 | devices, err := MountIscsi( 32 | WithInitiator(os.Getenv("INITIATOR_NAME")), 33 | WithTarget(fmt.Sprintf("%s:%s", os.Getenv("TGT_SERVER"), os.Getenv("TGT_PORT")), os.Getenv("TGT_VOLUME")), 34 | WithDigests("None"), 35 | ) 36 | if err != nil { 37 | log.Println(err) 38 | t.Error(err) 39 | } 40 | 41 | for _, device := range devices { 42 | // Make the mountpoint, and mount it. 43 | if err := os.MkdirAll("/mp", 0755); err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | mp, err := mount.TryMount(fmt.Sprintf("/dev/%s1", device), "/mp", 0) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | if err := sh.RunWithLogs("ls", "-l", "/mp"); err != nil { 53 | t.Error(err) 54 | } 55 | 56 | if err := mp.Unmount(0); err != nil { 57 | t.Error(err) 58 | } 59 | } 60 | }*/ 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, u-root 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /initiator_integration_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the u-root Authors. All rights reserved 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build !race 6 | 7 | package iscsinl 8 | 9 | /*import ( 10 | "fmt" 11 | "net" 12 | "os" 13 | "testing" 14 | "time" 15 | 16 | // This iSCSI target is a disaster, but it works without any kernel 17 | // modules. 18 | iscsiconfig "github.com/gostor/gotgt/pkg/config" 19 | _ "github.com/gostor/gotgt/pkg/port/iscsit" 20 | "github.com/gostor/gotgt/pkg/scsi" 21 | _ "github.com/gostor/gotgt/pkg/scsi/backingstore" 22 | 23 | "github.com/hugelgupf/p9/fsimpl/test/vmdriver" 24 | "github.com/u-root/u-root/pkg/qemu" 25 | "github.com/u-root/u-root/pkg/vmtest" 26 | ) 27 | 28 | func ISCSIConfig(filename, volume, portal string) *iscsiconfig.Config { 29 | return &iscsiconfig.Config{ 30 | Storages: []iscsiconfig.BackendStorage{ 31 | { 32 | DeviceID: 1000, 33 | Path: fmt.Sprintf("file:%s", filename), 34 | Online: true, 35 | ThinProvisioning: true, 36 | BlockShift: 0, 37 | }, 38 | }, 39 | ISCSIPortals: []iscsiconfig.ISCSIPortalInfo{ 40 | { 41 | ID: 0, 42 | Portal: portal, 43 | }, 44 | }, 45 | ISCSITargets: map[string]iscsiconfig.ISCSITarget{ 46 | volume: { 47 | TPGTs: map[string][]uint64{ 48 | // ??? 49 | "1": {0}, 50 | }, 51 | LUNs: map[string]uint64{ 52 | // Map LUN ID to DeviceID (see above). 53 | "1": 1000, 54 | }, 55 | }, 56 | }, 57 | } 58 | } 59 | 60 | func StartISCSIService(config *iscsiconfig.Config) error { 61 | if err := scsi.InitSCSILUMap(config); err != nil { 62 | return err 63 | } 64 | 65 | scsiTarget := scsi.NewSCSITargetService() 66 | targetDriver, err := scsi.NewTargetDriver("iscsi", scsiTarget) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | for tgtname := range config.ISCSITargets { 72 | if err := targetDriver.NewTarget(tgtname, config); err != nil { 73 | return err 74 | } 75 | } 76 | 77 | // comment this to avoid concurrent issue 78 | // runtime.GOMAXPROCS(runtime.NumCPU()) 79 | // run a service 80 | go targetDriver.Run() //nolint:errcheck 81 | return nil 82 | } 83 | 84 | func TestIntegration(t *testing.T) { 85 | if os.Getuid() == 0 { 86 | t.Skipf("test") 87 | } 88 | volumeName := "iqn.2016-09.com.example:foobar" 89 | 90 | // 1MB.ext4_vfat has two partitions: 91 | // 92 | // part 1 ext4 93 | // part 2 vfat 94 | if err := StartISCSIService(ISCSIConfig("./testdata/1MB.ext4_vfat", volumeName, "127.0.0.1:3260")); err != nil { 95 | t.Fatalf("iscsi target failed to start: %v", err) 96 | } 97 | 98 | vmtest.GolangTest(t, 99 | []string{"github.com/u-root/iscsinl"}, 100 | &vmtest.Options{ 101 | QEMUOpts: qemu.Options{ 102 | Devices: []qemu.Device{ 103 | vmdriver.HostNetwork{ 104 | Net: &net.IPNet{ 105 | // 192.168.0.0/24 106 | IP: net.IP{192, 168, 0, 0}, 107 | Mask: net.CIDRMask(24, 32), 108 | }, 109 | }, 110 | }, 111 | KernelArgs: fmt.Sprintf("TGT_PORT=3260 TGT_SERVER=192.168.0.2 TGT_VOLUME=%s -v", volumeName), 112 | Timeout: 30 * time.Second, 113 | }, 114 | }, 115 | ) 116 | }*/ 117 | -------------------------------------------------------------------------------- /netlink.go: -------------------------------------------------------------------------------- 1 | package iscsinl 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "strconv" 11 | "sync/atomic" 12 | "syscall" 13 | "unsafe" 14 | 15 | "github.com/vishvananda/netlink/nl" 16 | "golang.org/x/sys/unix" 17 | ) 18 | 19 | // STOP_CONN_RECOVER - when stopping connection clean up I/O on that connection 20 | const STOP_CONN_RECOVER = 0x3 21 | 22 | // IscsiEvent iscsi_if.h:enum iscsi_uevent_e 23 | type IscsiEvent uint32 24 | 25 | // IscsiEvents that we use 26 | const ( 27 | UEVENT_BASE IscsiEvent = 10 28 | KEVENT_BASE = 100 29 | ISCSI_UEVENT_CREATE_SESSION = UEVENT_BASE + 1 30 | ISCSI_UEVENT_DESTROY_SESSION = UEVENT_BASE + 2 31 | ISCSI_UEVENT_CREATE_CONN = UEVENT_BASE + 3 32 | ISCSI_UEVENT_DESTROY_CONN = UEVENT_BASE + 4 33 | ISCSI_UEVENT_BIND_CONN = UEVENT_BASE + 5 34 | ISCSI_UEVENT_SET_PARAM = UEVENT_BASE + 6 35 | ISCSI_UEVENT_START_CONN = UEVENT_BASE + 7 36 | ISCSI_UEVENT_STOP_CONN = UEVENT_BASE + 8 37 | ISCSI_UEVENT_SEND_PDU = UEVENT_BASE + 9 38 | 39 | ISCSI_KEVENT_RECV_PDU = KEVENT_BASE + 1 40 | ISCSI_KEVENT_CONN_ERROR = KEVENT_BASE + 2 41 | ISCSI_KEVENT_IF_ERROR = KEVENT_BASE + 3 42 | ISCSI_KEVENT_CREATE_SESSION = KEVENT_BASE + 6 43 | ) 44 | 45 | // IscsiParam iscsi_if.h:enum iscsi_param 46 | type IscsiParam uint32 47 | 48 | // IscsiParams up until INITIATOR_NAME 49 | const ( 50 | ISCSI_PARAM_MAX_RECV_DLENGTH IscsiParam = iota 51 | ISCSI_PARAM_MAX_XMIT_DLENGTH 52 | ISCSI_PARAM_HDRDGST_EN 53 | ISCSI_PARAM_DATADGST_EN 54 | ISCSI_PARAM_INITIAL_R2T_EN 55 | ISCSI_PARAM_MAX_R2T 56 | ISCSI_PARAM_IMM_DATA_EN 57 | ISCSI_PARAM_FIRST_BURST 58 | ISCSI_PARAM_MAX_BURST 59 | ISCSI_PARAM_PDU_INORDER_EN 60 | ISCSI_PARAM_DATASEQ_INORDER_EN 61 | ISCSI_PARAM_ERL 62 | ISCSI_PARAM_IFMARKER_EN 63 | ISCSI_PARAM_OFMARKER_EN 64 | ISCSI_PARAM_EXP_STATSN 65 | ISCSI_PARAM_TARGET_NAME 66 | ISCSI_PARAM_TPGT 67 | ISCSI_PARAM_PERSISTENT_ADDRESS 68 | ISCSI_PARAM_PERSISTENT_PORT 69 | ISCSI_PARAM_SESS_RECOVERY_TMO 70 | ISCSI_PARAM_CONN_PORT 71 | ISCSI_PARAM_CONN_ADDRESS 72 | ISCSI_PARAM_USERNAME 73 | ISCSI_PARAM_USERNAME_IN 74 | ISCSI_PARAM_PASSWORD 75 | ISCSI_PARAM_PASSWORD_IN 76 | ISCSI_PARAM_FAST_ABORT 77 | ISCSI_PARAM_ABORT_TMO 78 | ISCSI_PARAM_LU_RESET_TMO 79 | ISCSI_PARAM_HOST_RESET_TMO 80 | ISCSI_PARAM_PING_TMO 81 | ISCSI_PARAM_RECV_TMO 82 | ISCSI_PARAM_IFACE_NAME 83 | ISCSI_PARAM_ISID 84 | ISCSI_PARAM_INITIATOR_NAME 85 | ) 86 | 87 | var paramToString = map[IscsiParam]string{ 88 | ISCSI_PARAM_TARGET_NAME: "Target Name", 89 | ISCSI_PARAM_INITIATOR_NAME: "Initiator Name", 90 | ISCSI_PARAM_MAX_RECV_DLENGTH: "Max Recv DLength", 91 | ISCSI_PARAM_MAX_XMIT_DLENGTH: "Max Xmit DLenght", 92 | ISCSI_PARAM_FIRST_BURST: "First Burst", 93 | ISCSI_PARAM_MAX_BURST: "Max Burst", 94 | ISCSI_PARAM_PDU_INORDER_EN: "PDU Inorder EN", 95 | ISCSI_PARAM_DATASEQ_INORDER_EN: "Data Seq In Order EN", 96 | ISCSI_PARAM_INITIAL_R2T_EN: "Inital R2T EN", 97 | ISCSI_PARAM_IMM_DATA_EN: "Immediate Data EN", 98 | ISCSI_PARAM_EXP_STATSN: "Exp Statsn", 99 | ISCSI_PARAM_HDRDGST_EN: "HDR Digest EN", 100 | ISCSI_PARAM_DATADGST_EN: "Data Digest EN", 101 | ISCSI_PARAM_PING_TMO: "Ping TMO", 102 | ISCSI_PARAM_RECV_TMO: "Recv TMO", 103 | } 104 | 105 | func (p IscsiParam) String() string { 106 | val, ok := paramToString[p] 107 | if !ok { 108 | return fmt.Sprintf("IscsiParam(%d)", int(p)) 109 | } 110 | 111 | return val 112 | } 113 | 114 | // IscsiErr iscsi_if.h:enum iscsi_err 115 | type IscsiErr uint32 116 | 117 | // IscsiErr iscsi_if.h:enum iscsi_err 118 | const ( 119 | ISCSI_OK IscsiErr = 0 120 | ISCSI_ERR_BASE = 1000 121 | ISCSI_ERR_DATASN = ISCSI_ERR_BASE + 1 122 | ISCSI_ERR_DATA_OFFSET = ISCSI_ERR_BASE + 2 123 | ISCSI_ERR_MAX_CMDSN = ISCSI_ERR_BASE + 3 124 | ISCSI_ERR_EXP_CMDSN = ISCSI_ERR_BASE + 4 125 | ISCSI_ERR_BAD_OPCODE = ISCSI_ERR_BASE + 5 126 | ISCSI_ERR_DATALEN = ISCSI_ERR_BASE + 6 127 | ISCSI_ERR_AHSLEN = ISCSI_ERR_BASE + 7 128 | ISCSI_ERR_PROTO = ISCSI_ERR_BASE + 8 129 | ISCSI_ERR_LUN = ISCSI_ERR_BASE + 9 130 | ISCSI_ERR_BAD_ITT = ISCSI_ERR_BASE + 10 131 | ISCSI_ERR_CONN_FAILED = ISCSI_ERR_BASE + 11 132 | ISCSI_ERR_R2TSN = ISCSI_ERR_BASE + 12 133 | ISCSI_ERR_SESSION_FAILED = ISCSI_ERR_BASE + 13 134 | ISCSI_ERR_HDR_DGST = ISCSI_ERR_BASE + 14 135 | ISCSI_ERR_DATA_DGST = ISCSI_ERR_BASE + 15 136 | ISCSI_ERR_PARAM_NOT_FOUND = ISCSI_ERR_BASE + 16 137 | ISCSI_ERR_NO_SCSI_CMD = ISCSI_ERR_BASE + 17 138 | ISCSI_ERR_INVALID_HOST = ISCSI_ERR_BASE + 18 139 | ISCSI_ERR_XMIT_FAILED = ISCSI_ERR_BASE + 19 140 | ISCSI_ERR_TCP_CONN_CLOSE = ISCSI_ERR_BASE + 20 141 | ISCSI_ERR_SCSI_EH_SESSION_RST = ISCSI_ERR_BASE + 21 142 | ISCSI_ERR_NOP_TIMEDOUT = ISCSI_ERR_BASE + 22 143 | ) 144 | 145 | func (e IscsiErr) String() string { 146 | switch e { 147 | case ISCSI_OK: 148 | return "ISCSI_OK" 149 | case ISCSI_ERR_BASE: 150 | return "ISCSI_ERR_BASE" 151 | case ISCSI_ERR_DATASN: 152 | return "ISCSI_ERR_DATASN" 153 | case ISCSI_ERR_DATA_OFFSET: 154 | return "ISCSI_ERR_DATA_OFFSET" 155 | case ISCSI_ERR_MAX_CMDSN: 156 | return "ISCSI_ERR_MAX_CMDSN" 157 | case ISCSI_ERR_EXP_CMDSN: 158 | return "ISCSI_ERR_EXP_CMDSN" 159 | case ISCSI_ERR_BAD_OPCODE: 160 | return "ISCSI_ERR_BAD_OPCODE" 161 | case ISCSI_ERR_DATALEN: 162 | return "ISCSI_ERR_DATALEN" 163 | case ISCSI_ERR_AHSLEN: 164 | return "ISCSI_ERR_AHSLEN" 165 | case ISCSI_ERR_PROTO: 166 | return "ISCSI_ERR_PROTO" 167 | case ISCSI_ERR_LUN: 168 | return "ISCSI_ERR_LUN" 169 | case ISCSI_ERR_BAD_ITT: 170 | return "ISCSI_ERR_BAD_ITT" 171 | case ISCSI_ERR_CONN_FAILED: 172 | return "ISCSI_ERR_CONN_FAILED" 173 | case ISCSI_ERR_R2TSN: 174 | return "ISCSI_ERR_R2TSN" 175 | case ISCSI_ERR_SESSION_FAILED: 176 | return "ISCSI_ERR_SESSION_FAILED" 177 | case ISCSI_ERR_HDR_DGST: 178 | return "ISCSI_ERR_HDR_DGST" 179 | case ISCSI_ERR_DATA_DGST: 180 | return "ISCSI_ERR_DATA_DGST" 181 | case ISCSI_ERR_PARAM_NOT_FOUND: 182 | return "ISCSI_ERR_PARAM_NOT_FOUND" 183 | case ISCSI_ERR_NO_SCSI_CMD: 184 | return "ISCSI_ERR_NO_SCSI_CMD" 185 | case ISCSI_ERR_INVALID_HOST: 186 | return "ISCSI_ERR_INVALID_HOST" 187 | case ISCSI_ERR_XMIT_FAILED: 188 | return "ISCSI_ERR_XMIT_FAILED" 189 | case ISCSI_ERR_TCP_CONN_CLOSE: 190 | return "ISCSI_ERR_TCP_CONN_CLOSE" 191 | case ISCSI_ERR_SCSI_EH_SESSION_RST: 192 | return "ISCSI_ERR_SCSI_EH_SESSION_RST" 193 | case ISCSI_ERR_NOP_TIMEDOUT: 194 | return "ISCSI_ERR_NOP_TIMEDOUT" 195 | default: 196 | return strconv.Itoa(int(e)) 197 | } 198 | } 199 | 200 | // All Iscsu[U/K]event structs must be the same size, 201 | // with UserArg always 24 bytes, ReturnArg always 16 bytes 202 | 203 | // iSCSIUEvent is a generic uevent, for reading header content 204 | type iSCSIUEvent struct { 205 | Type IscsiEvent 206 | IfError uint32 207 | TransportHandle uint64 208 | UserArg [24]byte 209 | ReturnArg [16]byte 210 | } 211 | 212 | // iSCSIUEventCreateSession corresponds with ISCSI_UEVENT_CREATE_SESSION 213 | type iSCSIUEventCreateSession struct { 214 | Type IscsiEvent 215 | IfError uint32 216 | TransportHandle uint64 217 | CSession struct { 218 | InitialCmdSN uint32 219 | CmdsMax uint16 220 | QueueDepth uint16 221 | _ [16]byte 222 | } 223 | CSessionRet struct { 224 | Sid uint32 225 | HostNo uint32 226 | _ [8]byte 227 | } 228 | } 229 | 230 | type iSCSIUEventDestroySession struct { 231 | Type IscsiEvent 232 | IfError uint32 233 | TransportHandle uint64 234 | DSession struct { 235 | Sid uint32 236 | _ [20]byte 237 | } 238 | DSessionRet struct { 239 | Ret uint32 240 | _ [12]byte 241 | } 242 | } 243 | 244 | type iSCSIUEventCreateConnection struct { 245 | Type IscsiEvent 246 | IfError uint32 247 | TransportHandle uint64 248 | CConn struct { 249 | Sid uint32 250 | Cid uint32 251 | _ [16]byte 252 | } 253 | CConnRet struct { 254 | Sid uint32 255 | Cid uint32 256 | _ [8]byte 257 | } 258 | } 259 | type iSCSIUEventDestroyConnection struct { 260 | Type IscsiEvent 261 | IfError uint32 262 | TransportHandle uint64 263 | DConn struct { 264 | Sid uint32 265 | Cid uint32 266 | _ [16]byte 267 | } 268 | DConnRet struct { 269 | Ret uint32 270 | _ [12]byte 271 | } 272 | } 273 | 274 | type iSCSIUEventBindConnection struct { 275 | Type IscsiEvent 276 | IfError uint32 277 | TransportHandle uint64 278 | BConn struct { 279 | Sid uint32 280 | Cid uint32 281 | TransportEph uint64 282 | IsLeading uint32 283 | _ [4]byte 284 | } 285 | BConnRet struct { 286 | Ret uint32 287 | _ [12]byte 288 | } 289 | } 290 | 291 | type iSCSIUEventSetParam struct { 292 | Type IscsiEvent 293 | IfError uint32 294 | TransportHandle uint64 295 | SetParam struct { 296 | Sid uint32 297 | Cid uint32 298 | Param IscsiParam 299 | Len uint32 300 | _ [8]byte 301 | } 302 | SetParamRet struct { 303 | Ret uint32 304 | _ [12]byte 305 | } 306 | } 307 | 308 | type iSCSIUEventStartConnection struct { 309 | Type IscsiEvent 310 | IfError uint32 311 | TransportHandle uint64 312 | StartConn struct { 313 | Sid uint32 314 | Cid uint32 315 | _ [16]byte 316 | } 317 | StartConnRet struct { 318 | Ret uint32 319 | _ [12]byte 320 | } 321 | } 322 | 323 | type iSCSIUEventStopConnection struct { 324 | Type IscsiEvent 325 | IfError uint32 326 | TransportHandle uint64 327 | StopConn struct { 328 | Sid uint32 329 | Cid uint32 330 | ConnHandle uint64 331 | Flag uint32 332 | _ [4]byte 333 | } 334 | StopConnRet struct { 335 | Ret uint32 336 | _ [12]byte 337 | } 338 | } 339 | 340 | type iSCSIUEventSendPDU struct { 341 | Type IscsiEvent 342 | IfError uint32 343 | TransportHandle uint64 344 | SendPDU struct { 345 | Sid uint32 346 | Cid uint32 347 | HdrSize uint32 348 | DataSize uint32 349 | _ [8]byte 350 | } 351 | SendPDURet struct { 352 | Ret int32 353 | _ [12]byte 354 | } 355 | } 356 | 357 | type iSCSIKEventConnError struct { 358 | Type IscsiEvent 359 | IfError uint32 360 | TransportHandle uint64 361 | _ [24]byte 362 | ConnErr struct { 363 | Sid uint32 364 | Cid uint32 365 | Error IscsiErr 366 | _ [4]byte 367 | } 368 | } 369 | 370 | type iSCSIKEventRecvEvent struct { 371 | Type IscsiEvent 372 | IfError uint32 373 | TransportHandle uint64 374 | _ [24]byte 375 | RecvReq struct { 376 | Sid uint32 377 | Cid uint32 378 | RecvHandle uint64 379 | } 380 | } 381 | 382 | // IscsiIpcConn is a single netlink connection 383 | type IscsiIpcConn struct { 384 | Conn *nl.NetlinkSocket 385 | TransportHandle uint64 386 | nextSeqNr uint32 387 | } 388 | 389 | // ConnectNetlink connects to the iscsi netlink socket, and if successful returns 390 | // an IscsiIpcConn ready to accept commands. 391 | func ConnectNetlink() (*IscsiIpcConn, error) { 392 | conn, err := nl.Subscribe(unix.NETLINK_ISCSI, 1) 393 | if err != nil { 394 | if errors.Is(err, syscall.EPROTONOSUPPORT) { 395 | return nil, fmt.Errorf("failed to open iSCSI netlink socket (CONFIG_SCSI_ISCSI_ATTRS missing?): %w", err) 396 | } 397 | return nil, fmt.Errorf("failed to open iSCSI netlink socket: %w", err) 398 | } 399 | 400 | handleFile := "/sys/class/iscsi_transport/tcp/handle" 401 | data, err := ioutil.ReadFile(handleFile) 402 | if err != nil { 403 | return nil, fmt.Errorf("failed to read sysfs iscsi tcp handle: %w", err) 404 | } 405 | handle, err := strconv.ParseUint(string(data[:len(data)-1]), 10, 64) 406 | if err != nil { 407 | return nil, fmt.Errorf("failed to transport handle (%s) to int: %w", handleFile, err) 408 | } 409 | 410 | conn.SetReceiveTimeout(&unix.Timeval{Sec: 30}) 411 | conn.SetSendTimeout(&unix.Timeval{Sec: 30}) 412 | 413 | return &IscsiIpcConn{Conn: conn, TransportHandle: handle}, nil 414 | } 415 | 416 | // WaitFor reads ipcs until event of type Type is received, discarding others. 417 | // (This presumes we're the only ones using netlink on this host, and we only 418 | // have one outstanding request we're waiting for...) 419 | func (c *IscsiIpcConn) WaitFor(Type IscsiEvent) (*syscall.NetlinkMessage, error) { 420 | for { 421 | msgs, _, err := c.Conn.Receive() 422 | 423 | if err != nil { 424 | return nil, err 425 | } 426 | 427 | for _, msg := range msgs { 428 | reader := bytes.NewReader(msg.Data) 429 | var uevent iSCSIUEvent 430 | err = binary.Read(reader, binary.LittleEndian, &uevent) 431 | if err != nil { 432 | return nil, err 433 | } 434 | if uevent.TransportHandle != c.TransportHandle { 435 | return nil, fmt.Errorf("wrong transport handle: %v", uevent.TransportHandle) 436 | } 437 | if uevent.Type == Type { 438 | return &msg, nil 439 | } else if uevent.Type == ISCSI_KEVENT_CONN_ERROR { 440 | reader.Seek(0, 0) 441 | var connErr iSCSIKEventConnError 442 | binary.Read(reader, binary.LittleEndian, &connErr) 443 | return nil, fmt.Errorf("connection error: %+v", connErr) 444 | } else if uevent.Type == ISCSI_KEVENT_IF_ERROR { 445 | return nil, fmt.Errorf("interface error: %v (invalid netlink message?)", uevent.IfError) 446 | } 447 | 448 | log.Printf("Dumping unexpected event of type %d", uevent.Type) 449 | } 450 | } 451 | } 452 | 453 | // FillNetlink aids attaching binary data to netlink request 454 | func FillNetlink(request *nl.NetlinkRequest, data ...interface{}) error { 455 | var buf bytes.Buffer 456 | 457 | for _, item := range data { 458 | switch v := item.(type) { 459 | case []byte: 460 | _, err := buf.Write(v) 461 | if err != nil { 462 | return err 463 | } 464 | default: 465 | err := binary.Write(&buf, binary.LittleEndian, item) 466 | if err != nil { 467 | return err 468 | } 469 | } 470 | } 471 | request.AddRawData(buf.Bytes()) 472 | 473 | return nil 474 | } 475 | 476 | // DoNetlink send netlink and listen to response 477 | // ueventP *must* be a pointer to iSCSIUEvent 478 | func (c *IscsiIpcConn) DoNetlink(ueventP unsafe.Pointer, data ...interface{}) error { 479 | uevent := (*iSCSIUEvent)(ueventP) 480 | request := nl.NetlinkRequest{ 481 | NlMsghdr: unix.NlMsghdr{ 482 | Len: uint32(unix.SizeofNlMsghdr), 483 | Type: uint16(uevent.Type), 484 | Flags: uint16(1), 485 | Seq: atomic.AddUint32(&c.nextSeqNr, 1), 486 | }, 487 | } 488 | 489 | if err := FillNetlink(&request, append([]interface{}{*uevent}, data...)...); err != nil { 490 | return err 491 | } 492 | if err := c.Conn.Send(&request); err != nil { 493 | return err 494 | } 495 | 496 | response, err := c.WaitFor(uevent.Type) 497 | if err != nil { 498 | return err 499 | } 500 | 501 | reader := bytes.NewReader(response.Data) 502 | return binary.Read(reader, binary.LittleEndian, uevent) 503 | } 504 | 505 | // CreateSession creates a new kernel iSCSI session, returning the new 506 | // iscsi_session id and scsi_host id 507 | func (c *IscsiIpcConn) CreateSession(cmdsMax uint16, queueDepth uint16) (sid uint32, hostID uint32, err error) { 508 | cSession := iSCSIUEventCreateSession{ 509 | Type: ISCSI_UEVENT_CREATE_SESSION, 510 | IfError: 0, 511 | TransportHandle: c.TransportHandle, 512 | } 513 | cSession.CSession.CmdsMax = cmdsMax 514 | cSession.CSession.QueueDepth = queueDepth 515 | 516 | if err := c.DoNetlink(unsafe.Pointer(&cSession)); err != nil { 517 | return 0, 0, err 518 | } 519 | log.Println("Created new session ", cSession) 520 | 521 | return cSession.CSessionRet.Sid, cSession.CSessionRet.HostNo, nil 522 | } 523 | 524 | // DestroySession attempts to destroy the identified session. May fail if 525 | // connections are still active 526 | func (c *IscsiIpcConn) DestroySession(sid uint32) error { 527 | dSession := iSCSIUEventDestroySession{ 528 | Type: ISCSI_UEVENT_DESTROY_SESSION, 529 | IfError: 0, 530 | TransportHandle: c.TransportHandle, 531 | } 532 | dSession.DSession.Sid = sid 533 | 534 | if err := c.DoNetlink(unsafe.Pointer(&dSession)); err != nil { 535 | return err 536 | } 537 | 538 | if dSession.DSessionRet.Ret != 0 { 539 | return fmt.Errorf("error destroying session: %d", dSession.DSessionRet.Ret) 540 | } 541 | 542 | return nil 543 | } 544 | 545 | // CreateConnection creates a new iSCSI connection for an existing session, 546 | // returning the connection id on success 547 | func (c *IscsiIpcConn) CreateConnection(sid uint32) (cid uint32, err error) { 548 | 549 | cConn := iSCSIUEventCreateConnection{ 550 | Type: ISCSI_UEVENT_CREATE_CONN, 551 | IfError: 0, 552 | TransportHandle: c.TransportHandle, 553 | } 554 | cConn.CConn.Sid = sid 555 | 556 | if err := c.DoNetlink(unsafe.Pointer(&cConn)); err != nil { 557 | return 0, err 558 | } 559 | log.Println("Created new connection", cConn) 560 | 561 | return cConn.CConnRet.Cid, nil 562 | } 563 | 564 | // DestroyConnection attempts to destroy the identified connection. May fail if 565 | // connection is still running 566 | func (c *IscsiIpcConn) DestroyConnection(sid uint32, cid uint32) error { 567 | dConn := iSCSIUEventDestroyConnection{ 568 | Type: ISCSI_UEVENT_DESTROY_CONN, 569 | IfError: 0, 570 | TransportHandle: c.TransportHandle, 571 | } 572 | dConn.DConn.Sid = sid 573 | dConn.DConn.Cid = cid 574 | 575 | if err := c.DoNetlink(unsafe.Pointer(&dConn)); err != nil { 576 | return err 577 | } 578 | 579 | if dConn.DConnRet.Ret != 0 { 580 | return fmt.Errorf("error destroying connection: %d", dConn.DConnRet.Ret) 581 | } 582 | 583 | return nil 584 | } 585 | 586 | // BindConnection binds a TCP socket to the given kernel connection. fd must be 587 | // the current program's fd for the TCP socket. 588 | func (c *IscsiIpcConn) BindConnection(sid uint32, cid uint32, fd int) error { 589 | bConn := iSCSIUEventBindConnection{ 590 | Type: ISCSI_UEVENT_BIND_CONN, 591 | IfError: 0, 592 | TransportHandle: c.TransportHandle, 593 | } 594 | bConn.BConn.Sid = sid 595 | bConn.BConn.Cid = cid 596 | bConn.BConn.TransportEph = uint64(fd) 597 | bConn.BConn.IsLeading = 1 598 | 599 | err := c.DoNetlink(unsafe.Pointer(&bConn)) 600 | if err != nil { 601 | return err 602 | } 603 | log.Println("Binded new connection ", bConn) 604 | 605 | if bConn.BConnRet.Ret != 0 { 606 | return fmt.Errorf("error binding connection: %d", bConn.BConnRet.Ret) 607 | } 608 | 609 | return nil 610 | } 611 | 612 | // SetParam sets a single parameter for the given connection. value will be 613 | // null terminated and must not contain null bytes 614 | func (c *IscsiIpcConn) SetParam(sid uint32, cid uint32, param IscsiParam, value string) error { 615 | setParam := iSCSIUEventSetParam{ 616 | Type: ISCSI_UEVENT_SET_PARAM, 617 | TransportHandle: c.TransportHandle, 618 | } 619 | setParam.SetParam.Sid = sid 620 | setParam.SetParam.Cid = cid 621 | setParam.SetParam.Param = param 622 | setParam.SetParam.Len = uint32(len(value) + 1) // Null terminatorrequest 623 | 624 | err := c.DoNetlink(unsafe.Pointer(&setParam), []byte(value+"\x00")) 625 | if err != nil { 626 | return err 627 | } 628 | 629 | if setParam.SetParamRet.Ret != 0 { 630 | return fmt.Errorf("error setting param %d: %d", param, setParam.SetParamRet.Ret) 631 | } 632 | return nil 633 | } 634 | 635 | // StartConnection starts the given connection. The connection should be bound 636 | // and logged in. 637 | func (c *IscsiIpcConn) StartConnection(sid uint32, cid uint32) error { 638 | sConn := iSCSIUEventStartConnection{ 639 | Type: ISCSI_UEVENT_START_CONN, 640 | IfError: 0, 641 | TransportHandle: c.TransportHandle, 642 | } 643 | sConn.StartConn.Sid = sid 644 | sConn.StartConn.Cid = cid 645 | 646 | err := c.DoNetlink(unsafe.Pointer(&sConn)) 647 | if err != nil { 648 | return err 649 | } 650 | 651 | if sConn.StartConnRet.Ret != 0 { 652 | return fmt.Errorf("error starting connection: %d", sConn.StartConnRet.Ret) 653 | } 654 | return nil 655 | } 656 | 657 | // StopConnection attempts to stop the identified connection. 658 | func (c *IscsiIpcConn) StopConnection(sid uint32, cid uint32) error { 659 | sConn := iSCSIUEventStopConnection{ 660 | Type: ISCSI_UEVENT_STOP_CONN, 661 | IfError: 0, 662 | TransportHandle: c.TransportHandle, 663 | } 664 | sConn.StopConn.Sid = sid 665 | sConn.StopConn.Cid = cid 666 | sConn.StopConn.Flag = STOP_CONN_RECOVER 667 | 668 | if err := c.DoNetlink(unsafe.Pointer(&sConn)); err != nil { 669 | return err 670 | } 671 | 672 | if sConn.StopConnRet.Ret != 0 { 673 | return fmt.Errorf("error stopping connection: %d", sConn.StopConnRet.Ret) 674 | } 675 | return nil 676 | } 677 | 678 | // PduLike interface for sending PDUs 679 | type PduLike interface { 680 | // Length of the PDU header (iscsi_hdr/etc.) 681 | HeaderLen() uint32 682 | // Length of the PDU data 683 | DataLen() uint32 684 | // Header + Data 685 | Serialize() []byte 686 | } 687 | 688 | // RecvPDU waits for a PDU for the given connection, and returns the raw 689 | // PDU with header on success. RecvPDU assumes a single iSCSI connection, 690 | // and will error if a different connection receives a PDU 691 | func (c *IscsiIpcConn) RecvPDU(sid uint32, cid uint32) ([]byte, error) { 692 | response, err := c.WaitFor(ISCSI_KEVENT_RECV_PDU) 693 | if err != nil { 694 | return nil, err 695 | } 696 | 697 | var recvReq iSCSIKEventRecvEvent 698 | reader := bytes.NewReader(response.Data) 699 | binary.Read(reader, binary.LittleEndian, &recvReq) 700 | 701 | if recvReq.RecvReq.Sid != sid || recvReq.RecvReq.Cid != cid { 702 | return nil, errors.New("unexpected PDU for different session... is another initiator running?") 703 | } 704 | 705 | log.Printf("Response length: %v, data length: %v", response.Header.Len, len(response.Data)) 706 | 707 | return ioutil.ReadAll(reader) 708 | } 709 | 710 | // SendPDU sends the given PDU on the given connection 711 | func (c *IscsiIpcConn) SendPDU(sid uint32, cid uint32, pdu PduLike) error { 712 | sendPdu := iSCSIUEventSendPDU{ 713 | Type: ISCSI_UEVENT_SEND_PDU, 714 | TransportHandle: c.TransportHandle, 715 | } 716 | sendPdu.SendPDU.Sid = sid 717 | sendPdu.SendPDU.Cid = cid 718 | sendPdu.SendPDU.HdrSize = pdu.HeaderLen() 719 | sendPdu.SendPDU.DataSize = pdu.DataLen() 720 | 721 | if err := c.DoNetlink(unsafe.Pointer(&sendPdu), pdu.Serialize()); err != nil { 722 | return err 723 | } 724 | 725 | if sendPdu.SendPDURet.Ret != 0 { 726 | return fmt.Errorf("SendPDU had unexpected error code %d", sendPdu.SendPDURet.Ret) 727 | } 728 | return nil 729 | } 730 | -------------------------------------------------------------------------------- /initiator.go: -------------------------------------------------------------------------------- 1 | // Package iscsinl acts as an initiator for bootstrapping an iscsi connection 2 | // Partial implementation of RFC3720 login and NETLINK_ISCSI, just enough to 3 | // get a connection going. 4 | package iscsinl 5 | 6 | import ( 7 | "bytes" 8 | "encoding/binary" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "log" 14 | "net" 15 | "os" 16 | "path/filepath" 17 | "strconv" 18 | "strings" 19 | "time" 20 | 21 | "golang.org/x/sys/unix" 22 | ) 23 | 24 | // Login constants 25 | const ( 26 | ISCSI_OP_LOGIN = 0x03 27 | ISCSI_OP_LOGIN_RSP = 0x23 28 | ISCSI_OP_IMMEDIATE = 0x40 29 | 30 | ISCSI_VERSION = 0x00 31 | 32 | ISCSI_FLAG_LOGIN_TRANSIT = 0x80 33 | ISCSI_FLAG_LOGIN_CONTINUE = 0x40 34 | ) 35 | 36 | // IscsiLoginStage corresponds to iSCSI login stage 37 | type IscsiLoginStage uint8 38 | 39 | // Login stages 40 | const ( 41 | ISCSI_SECURITY_NEGOTIATION_STAGE IscsiLoginStage = 0 42 | ISCSI_OP_PARMS_NEGOTIATION_STAGE = 1 43 | ISCSI_FULL_FEATURE_PHASE = 3 44 | ) 45 | 46 | func hton24(buf *[3]byte, num int) { 47 | buf[0] = uint8(((num) >> 16) & 0xFF) 48 | buf[1] = uint8(((num) >> 8) & 0xFF) 49 | buf[2] = uint8((num) & 0xFF) 50 | } 51 | 52 | func ntoh24(buf [3]byte) uint { 53 | return (uint(buf[0]) << 16) | (uint(buf[1]) << 8) | uint(buf[2]) 54 | } 55 | 56 | func hton48(buf *[6]byte, num int) { 57 | buf[0] = uint8(((num) >> 40) & 0xFF) 58 | buf[1] = uint8(((num) >> 32) & 0xFF) 59 | buf[2] = uint8(((num) >> 24) & 0xFF) 60 | buf[3] = uint8(((num) >> 16) & 0xFF) 61 | buf[4] = uint8(((num) >> 8) & 0xFF) 62 | buf[5] = uint8((num) & 0xFF) 63 | } 64 | 65 | // LoginHdr is the header for ISCSI_OP_LOGIN 66 | // See: RFC3720 10.12. 67 | type LoginHdr struct { 68 | Opcode uint8 69 | Flags uint8 70 | MaxVersion uint8 71 | MinVersion uint8 72 | HLength uint8 73 | DLength [3]uint8 74 | Isid [6]uint8 75 | Tsih uint16 76 | Itt uint32 77 | Cid uint16 78 | Rsvd3 uint16 79 | CmdSN uint32 80 | ExpStatSN uint32 81 | Rsvd5 [16]uint8 82 | } 83 | 84 | // LoginRspHdr is the header for ISCSI_OP_LOGIN_RSP 85 | // See: RFC3720 10.13. 86 | type LoginRspHdr struct { 87 | Opcode uint8 88 | Flags uint8 89 | MaxVersion uint8 90 | ActiveVersion uint8 91 | HLength uint8 92 | DLength [3]uint8 93 | Isid [6]uint8 94 | Tsih uint16 95 | Itt uint32 96 | Rsvd3 uint32 97 | StatSN uint32 98 | ExpCmdSN uint32 99 | MaxCmdSN uint32 100 | StatusClass uint8 101 | StatusDetail uint8 102 | Rsvd5 [10]uint8 103 | } 104 | 105 | // IscsiLoginPdu is an iSCSI Login Request PDU 106 | type IscsiLoginPdu struct { 107 | Header LoginHdr 108 | TextSegments bytes.Buffer 109 | } 110 | 111 | // HeaderLen gives the length of the PDU header 112 | func (l *IscsiLoginPdu) HeaderLen() uint32 { 113 | return uint32(binary.Size(l.Header)) 114 | } 115 | 116 | // DataLen gives the length of all data segements for this PDU 117 | func (l *IscsiLoginPdu) DataLen() uint32 { 118 | return uint32(l.TextSegments.Len()) 119 | } 120 | 121 | // Serialize to network order bytes 122 | func (l *IscsiLoginPdu) Serialize() []byte { 123 | var buf bytes.Buffer 124 | 125 | hton24(&l.Header.DLength, int(l.DataLen())) 126 | binary.Write(&buf, binary.LittleEndian, l.Header) 127 | buf.Write(l.TextSegments.Bytes()) 128 | return buf.Bytes() 129 | } 130 | 131 | // AddParam the key=value string to the login payload and adds null terminator 132 | func (l *IscsiLoginPdu) AddParam(keyvalue string) { 133 | l.TextSegments.WriteString(keyvalue) 134 | l.TextSegments.WriteByte(0) 135 | } 136 | 137 | // ReReadPartitionTable opens the given file and reads partition table from it 138 | func ReReadPartitionTable(devname string) error { 139 | f, err := os.OpenFile(devname, os.O_RDWR, 0) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | _, err = unix.IoctlGetInt(int(f.Fd()), unix.BLKRRPART) 145 | return err 146 | } 147 | 148 | // IscsiOptions configures iSCSI session. 149 | type IscsiOptions struct { 150 | InitiatorName string 151 | Address string 152 | Volume string 153 | 154 | // See RFC7143 Section 13 for these. 155 | // Max data per single incoming iSCSI packet 156 | MaxRecvDLength int 157 | // Max data per single outgoing iSCSI packet 158 | MaxXmitDLength int 159 | // Max unsolicited data per iSCSI command sequence 160 | FirstBurstLength int 161 | // Max data per iSCSI command sequence 162 | MaxBurstLength int 163 | // CRC32C or None 164 | HeaderDigest string 165 | // CRC32C or None 166 | DataDigest string 167 | // Seconds to wait for heartbeat response before declaring the connection dead 168 | PingTimeout int32 169 | // Seconds to wait on an idle connection before sending a heartbeat 170 | RecvTimeout int32 171 | // Max iSCSI commands outstanding 172 | CmdsMax uint16 173 | // Max IOs outstanding 174 | QueueDepth uint16 175 | // Require initial Ready To Transfer (R2T) (false enables unsolicited data) 176 | InitialR2T bool 177 | // Enable iSCSI Immediate Data 178 | ImmediateData bool 179 | // Require iSCSI data sequence to be sent order by offset 180 | DataPDUInOrder bool 181 | // Require packets in an iSCSI data sequence to be sent in order by sequence number 182 | DataSequenceInOrder bool 183 | 184 | // Scheduler to configure for the blockdev 185 | Scheduler string 186 | 187 | // ScanTimeout is the total time to wait for block devices to appear in 188 | // the file system after initiating a scan. 189 | ScanTimeout time.Duration 190 | } 191 | 192 | // IscsiTargetSession represents an iSCSI session and a single connection to a target 193 | type IscsiTargetSession struct { 194 | opts IscsiOptions 195 | cid uint32 196 | hostID uint32 197 | sid uint32 198 | 199 | // Update this on login response 200 | tsih uint16 201 | expCmdSN uint32 202 | maxCmdSN uint32 203 | expStatSN uint32 204 | currStage IscsiLoginStage 205 | 206 | // Seconds to wait for heartbeat response before declaring the connection dead 207 | pingTimeout int32 208 | // Seconds to wait on an idle connection before sending a heartbeat 209 | recvTimeout int32 210 | 211 | blockDevName []string 212 | 213 | conn *net.TCPConn 214 | netlink *IscsiIpcConn 215 | } 216 | 217 | const ( 218 | oneMegabyte = 1048576 219 | oneMinute = 60 220 | ) 221 | 222 | var defaultOpts = IscsiOptions{ 223 | MaxRecvDLength: oneMegabyte, 224 | MaxXmitDLength: oneMegabyte, 225 | FirstBurstLength: oneMegabyte, 226 | MaxBurstLength: oneMegabyte, 227 | HeaderDigest: "CRC32C", 228 | DataDigest: "CRC32C", 229 | PingTimeout: oneMinute, 230 | RecvTimeout: oneMinute, 231 | CmdsMax: 128, 232 | QueueDepth: 16, 233 | InitialR2T: false, 234 | ImmediateData: true, 235 | DataPDUInOrder: true, 236 | DataSequenceInOrder: true, 237 | Scheduler: "noop", 238 | ScanTimeout: 3 * time.Second, 239 | } 240 | 241 | // Option is a functional API for setting optional configuration. 242 | type Option func(i *IscsiOptions) 243 | 244 | // WithTarget adds the target address and volume to the config. 245 | func WithTarget(addr, volume string) Option { 246 | return func(i *IscsiOptions) { 247 | i.Address = addr 248 | i.Volume = volume 249 | } 250 | } 251 | 252 | // WithScanTimeout sets the timeout to wait for devices to appear after sending the scan event. 253 | func WithScanTimeout(dur time.Duration) Option { 254 | return func(i *IscsiOptions) { 255 | i.ScanTimeout = dur 256 | } 257 | } 258 | 259 | // WithInitiator adds the initiator name to the config. 260 | func WithInitiator(initiatorName string) Option { 261 | return func(i *IscsiOptions) { 262 | i.InitiatorName = initiatorName 263 | } 264 | } 265 | 266 | // WithCmdsMax sets the maximum number of outstanding iSCSI commands. 267 | func WithCmdsMax(n uint16) Option { 268 | return func(i *IscsiOptions) { 269 | i.CmdsMax = n 270 | } 271 | } 272 | 273 | // WithQueueDepth sets the maximum number of outstanding IOs. 274 | func WithQueueDepth(n uint16) Option { 275 | return func(i *IscsiOptions) { 276 | i.QueueDepth = n 277 | } 278 | } 279 | 280 | // WithScheduler sets the block device scheduler. 281 | func WithScheduler(sched string) Option { 282 | return func(i *IscsiOptions) { 283 | i.Scheduler = sched 284 | } 285 | } 286 | 287 | // WithDigests sets both the header and data digest. Acceptable values: None or CRC32C. 288 | func WithDigests(digest string) Option { 289 | return func(i *IscsiOptions) { 290 | i.HeaderDigest = digest 291 | i.DataDigest = digest 292 | } 293 | } 294 | 295 | // NewSession constructs an IscsiTargetSession 296 | func NewSession(netlink *IscsiIpcConn, opts ...Option) *IscsiTargetSession { 297 | i := &IscsiTargetSession{ 298 | opts: defaultOpts, 299 | netlink: netlink, 300 | } 301 | // Apply optional arguments from user. 302 | for _, opt := range opts { 303 | opt(&i.opts) 304 | } 305 | return i 306 | } 307 | 308 | // Connect creates a kernel iSCSI session and connection, connects to the 309 | // target, and binds the connection to the kernel session. 310 | func (s *IscsiTargetSession) Connect() error { 311 | var err error 312 | s.sid, s.hostID, err = s.netlink.CreateSession(s.opts.CmdsMax, s.opts.QueueDepth) 313 | if err != nil { 314 | return err 315 | } 316 | 317 | s.cid, err = s.netlink.CreateConnection(s.sid) 318 | if err != nil { 319 | return err 320 | } 321 | 322 | resolvedAddr, err := net.ResolveTCPAddr("tcp", s.opts.Address) 323 | if err != nil { 324 | return err 325 | } 326 | 327 | s.conn, err = net.DialTCP("tcp", nil, resolvedAddr) 328 | if err != nil { 329 | return err 330 | } 331 | 332 | file, err := s.conn.File() 333 | if err != nil { 334 | return err 335 | } 336 | defer file.Close() 337 | fd := file.Fd() 338 | 339 | return s.netlink.BindConnection(s.sid, s.cid, int(fd)) 340 | } 341 | 342 | // Start starts the kernel iSCSI session. Call this after successfully 343 | // logging in and setting all desired parameters. 344 | func (s *IscsiTargetSession) Start() error { 345 | return s.netlink.StartConnection(s.sid, s.cid) 346 | } 347 | 348 | // TearDown stops and destroys the connection & session 349 | // in case of partially created session, stopping connections/destroying 350 | // connections won't work, so try it all 351 | func (s *IscsiTargetSession) TearDown() error { 352 | sConnErr := s.netlink.StopConnection(s.sid, s.cid) 353 | 354 | dConnErr := s.netlink.DestroyConnection(s.sid, s.cid) 355 | 356 | if err := s.netlink.DestroySession(s.sid); err != nil { 357 | return fmt.Errorf("failure to destroy session DestroySession:%v DestroyConnection:%v StopConnection:%v", err, dConnErr, sConnErr) 358 | } 359 | return nil 360 | } 361 | 362 | func netlinkBoolStr(pred bool) string { 363 | if pred { 364 | return "1" 365 | } 366 | return "0" 367 | } 368 | 369 | func iscsiParseBool(inval string) (bool, error) { 370 | if inval == "Yes" { 371 | return true, nil 372 | } else if inval == "No" { 373 | return false, nil 374 | } 375 | return false, fmt.Errorf("invalid bool: %s", inval) 376 | } 377 | 378 | func iscsiBoolStr(pred bool) string { 379 | if pred { 380 | return "Yes" 381 | } 382 | return "No" 383 | } 384 | 385 | // SetParams sets some desired parameters for the kernel session 386 | func (s *IscsiTargetSession) SetParams() error { 387 | params := []struct { 388 | p IscsiParam 389 | v string 390 | }{ 391 | {ISCSI_PARAM_TARGET_NAME, s.opts.Volume}, 392 | {ISCSI_PARAM_INITIATOR_NAME, s.opts.InitiatorName}, 393 | {ISCSI_PARAM_MAX_RECV_DLENGTH, fmt.Sprintf("%d", s.opts.MaxRecvDLength)}, 394 | {ISCSI_PARAM_MAX_XMIT_DLENGTH, fmt.Sprintf("%d", s.opts.MaxXmitDLength)}, 395 | {ISCSI_PARAM_FIRST_BURST, fmt.Sprintf("%d", s.opts.FirstBurstLength)}, 396 | {ISCSI_PARAM_MAX_BURST, fmt.Sprintf("%d", s.opts.MaxBurstLength)}, 397 | {ISCSI_PARAM_PDU_INORDER_EN, netlinkBoolStr(s.opts.DataPDUInOrder)}, 398 | {ISCSI_PARAM_DATASEQ_INORDER_EN, netlinkBoolStr(s.opts.DataSequenceInOrder)}, 399 | {ISCSI_PARAM_INITIAL_R2T_EN, netlinkBoolStr(s.opts.InitialR2T)}, 400 | {ISCSI_PARAM_IMM_DATA_EN, netlinkBoolStr(s.opts.ImmediateData)}, 401 | {ISCSI_PARAM_EXP_STATSN, fmt.Sprintf("%d", s.expStatSN)}, 402 | {ISCSI_PARAM_HDRDGST_EN, netlinkBoolStr(s.opts.HeaderDigest == "CRC32C")}, 403 | {ISCSI_PARAM_DATADGST_EN, netlinkBoolStr(s.opts.DataDigest == "CRC32C")}, 404 | {ISCSI_PARAM_PING_TMO, fmt.Sprintf("%d", s.opts.PingTimeout)}, 405 | {ISCSI_PARAM_RECV_TMO, fmt.Sprintf("%d", s.opts.RecvTimeout)}, 406 | } 407 | 408 | for _, pp := range params { 409 | log.Printf("Setting param %s to %v", pp.p.String(), pp.v) 410 | if err := s.netlink.SetParam(s.sid, s.cid, pp.p, pp.v); err != nil { 411 | return err 412 | } 413 | } 414 | return nil 415 | } 416 | 417 | // writeFile is ioutil.WriteFile but disallows creating new file 418 | func writeFile(filename string, contents string) error { 419 | file, err := os.OpenFile(filename, os.O_WRONLY, 0) 420 | if err != nil { 421 | return err 422 | } 423 | wlen, err := file.WriteString(contents) 424 | if err != nil && wlen < len(contents) { 425 | err = io.ErrShortWrite 426 | } 427 | // If Close() fails this likely indicates a write failure. 428 | if errClose := file.Close(); err == nil { 429 | err = errClose 430 | } 431 | return err 432 | } 433 | 434 | // ReScan triggers a scsi host scan so the kernel creates a block device for the 435 | // newly attached session, then waits for the block device to be created 436 | func (s *IscsiTargetSession) ReScan() error { 437 | // The three wildcards stand for channel, SCSI target ID, and LUN. 438 | if err := writeFile(fmt.Sprintf("/sys/class/scsi_host/host%d/scan", s.hostID), "- - -"); err != nil { 439 | return err 440 | } 441 | 442 | var matches []string 443 | start := time.Now() 444 | // The kernel may add devices it finds through scanning at any time. If 445 | // a scan yields multiple, kernel will not add them atomically. We wait 446 | // until at least one device has appeared, and no new devices have 447 | // appeared for 100ms. We also time out based on the user defined 448 | // ScanTimeout. 449 | for elapsed := time.Now().Sub(start); elapsed <= s.opts.ScanTimeout; { 450 | log.Printf("Waiting for device...") 451 | time.Sleep(100 * time.Millisecond) 452 | newMatches, err := filepath.Glob(fmt.Sprintf( 453 | "/sys/class/iscsi_session/session%d/device/target*/*/block/*/uevent", s.sid)) 454 | if err != nil { 455 | return err 456 | } 457 | if len(newMatches) > 0 { 458 | if len(matches) == len(newMatches) { 459 | break 460 | } 461 | matches = newMatches 462 | } 463 | } 464 | 465 | found := false 466 | for _, match := range matches { 467 | contents, err := ioutil.ReadFile(match) 468 | if err != nil { 469 | log.Printf("error reading file for %v err=%v skipping error\n", match, err) 470 | continue 471 | } 472 | 473 | for _, kv := range strings.Split(string(contents), "\n") { 474 | splitkv := strings.Split(kv, "=") 475 | if splitkv[0] == "DEVNAME" { 476 | s.blockDevName = append(s.blockDevName, splitkv[1]) 477 | found = true 478 | } 479 | } 480 | } 481 | 482 | if !found { 483 | return errors.New("could not find any device DEVNAMEs") 484 | } 485 | return nil 486 | 487 | } 488 | 489 | // ConfigureBlockDevs will set blockdev params for this iSCSI session, and returns blockdev name 490 | func (s *IscsiTargetSession) ConfigureBlockDevs() ([]string, error) { 491 | if err := s.ReScan(); err != nil { 492 | return nil, err 493 | } 494 | 495 | for i := range s.blockDevName { 496 | for { 497 | log.Printf("Waiting for sysfs...") 498 | time.Sleep(30 * time.Millisecond) 499 | _, err := os.Stat(fmt.Sprintf("/sys/block/%v/queue/nr_requests", s.blockDevName[i])) 500 | if !os.IsNotExist(err) { 501 | break 502 | } 503 | } 504 | params := []struct { 505 | filen string 506 | val string 507 | }{ 508 | {fmt.Sprintf("/sys/block/%v/queue/nr_requests", s.blockDevName[i]), fmt.Sprintf("%d", s.opts.QueueDepth)}, 509 | {fmt.Sprintf("/sys/block/%v/queue/scheduler", s.blockDevName[i]), s.opts.Scheduler}, 510 | {fmt.Sprintf("/sys/block/%v/queue/rotational", s.blockDevName[i]), "0"}, 511 | } 512 | 513 | for _, pp := range params { 514 | if err := writeFile(pp.filen, pp.val); err != nil { 515 | return nil, err 516 | } 517 | } 518 | } 519 | return s.blockDevName, nil 520 | } 521 | 522 | // processOperationalParam assigns params returned from the target. Errors if 523 | // we cannot continue with negotiation. 524 | func (s *IscsiTargetSession) processOperationalParam(keyvalue string) error { 525 | split := strings.Split(keyvalue, "=") 526 | if len(split) != 2 { 527 | return fmt.Errorf("invalid format for operational param \"%v\"", keyvalue) 528 | } 529 | key, value := split[0], split[1] 530 | 531 | if value == "Reject" { 532 | return fmt.Errorf("target rejected parameter %q", key) 533 | } 534 | 535 | switch key { 536 | case "HeaderDigest": 537 | s.opts.HeaderDigest = value 538 | case "DataDigest": 539 | s.opts.DataDigest = value 540 | case "InitialR2T": 541 | val, err := iscsiParseBool(value) 542 | if err != nil { 543 | return err 544 | } 545 | s.opts.InitialR2T = val || s.opts.InitialR2T 546 | case "ImmediateData": 547 | val, err := iscsiParseBool(value) 548 | if err != nil { 549 | return err 550 | } 551 | s.opts.ImmediateData = val && s.opts.ImmediateData 552 | case "MaxRecvDataSegmentLength": 553 | length, err := strconv.ParseInt(value, 10, 32) 554 | if err != nil { 555 | return err 556 | } 557 | s.opts.MaxXmitDLength = int(length) 558 | case "MaxBurstLength": 559 | length, err := strconv.ParseInt(value, 10, 32) 560 | if err != nil { 561 | return err 562 | } 563 | s.opts.MaxBurstLength = int(length) 564 | case "FirstBurstLength": 565 | length, err := strconv.ParseInt(value, 10, 32) 566 | if err != nil { 567 | return err 568 | } 569 | s.opts.FirstBurstLength = int(length) 570 | case "DataPDUInOrder": 571 | val, err := iscsiParseBool(value) 572 | if err != nil { 573 | return err 574 | } 575 | s.opts.DataPDUInOrder = val || s.opts.DataPDUInOrder 576 | case "DataSequenceInOrder": 577 | val, err := iscsiParseBool(value) 578 | if err != nil { 579 | return err 580 | } 581 | s.opts.DataSequenceInOrder = val || s.opts.DataSequenceInOrder 582 | default: 583 | log.Printf("Ignoring unknown param \"%v\"", keyvalue) 584 | } 585 | return nil 586 | } 587 | 588 | // processOperationalParams processes all parameters in a login response 589 | func (s *IscsiTargetSession) processOperationalParams(data []byte) error { 590 | params := strings.Split(string(data), "\x00") 591 | // Annoyingly, strings.Split will always have an empty string at the end 592 | // An empty string in the middle of params suggests we have an otherwise 593 | // malformed request, since we shouldn't expect double nul bytes 594 | params = params[0 : len(params)-1] 595 | for _, param := range params { 596 | if err := s.processOperationalParam(param); err != nil { 597 | return err 598 | } 599 | } 600 | return nil 601 | } 602 | 603 | func (s *IscsiTargetSession) processLoginResponse(response []byte) error { 604 | var loginRespPdu LoginRspHdr 605 | reader := bytes.NewReader(response) 606 | if err := binary.Read(reader, binary.LittleEndian, &loginRespPdu); err != nil { 607 | return err 608 | } 609 | if loginRespPdu.Opcode != ISCSI_OP_LOGIN_RSP { 610 | return fmt.Errorf("unexpected response pdu opcode %d", loginRespPdu.Opcode) 611 | } 612 | 613 | if loginRespPdu.StatusClass != 0 { 614 | return fmt.Errorf("error in login response %d %d", loginRespPdu.StatusClass, loginRespPdu.StatusDetail) 615 | } 616 | 617 | s.maxCmdSN = loginRespPdu.MaxCmdSN 618 | s.expCmdSN = loginRespPdu.ExpCmdSN 619 | s.tsih = loginRespPdu.Tsih 620 | s.expStatSN = loginRespPdu.StatSN + 1 621 | if (loginRespPdu.Flags & ISCSI_FLAG_LOGIN_TRANSIT) != 0 { 622 | s.currStage = IscsiLoginStage(loginRespPdu.Flags & 0x03) 623 | } 624 | 625 | // dLength generally != the length of the rest of the netlink buffer 626 | dLength := int(ntoh24(loginRespPdu.DLength)) 627 | if dLength == 0 { 628 | return nil 629 | } 630 | theRest := make([]byte, dLength) 631 | read, err := reader.Read(theRest) 632 | if err != nil { 633 | return err 634 | } 635 | if read != dLength { 636 | return errors.New("unexpected EOF reading PDU data") 637 | } 638 | return s.processOperationalParams(theRest) 639 | } 640 | 641 | // Login - RFC iSCSI login 642 | // https://www.ietf.org/rfc/rfc3720.txt 643 | // For now "negotiates" no auth security. 644 | func (s *IscsiTargetSession) Login() error { 645 | log.Println("Starting login...") 646 | 647 | for s.currStage != ISCSI_OP_PARMS_NEGOTIATION_STAGE { 648 | loginReq := IscsiLoginPdu{ 649 | Header: LoginHdr{ 650 | Opcode: ISCSI_OP_LOGIN | ISCSI_OP_IMMEDIATE, 651 | MaxVersion: ISCSI_VERSION, 652 | MinVersion: ISCSI_VERSION, 653 | ExpStatSN: s.expStatSN, 654 | Tsih: s.tsih, 655 | Flags: uint8((s.currStage << 2) | ISCSI_OP_PARMS_NEGOTIATION_STAGE | ISCSI_FLAG_LOGIN_TRANSIT), 656 | }, 657 | } 658 | hton48(&loginReq.Header.Isid, int(s.sid)) 659 | loginReq.AddParam("AuthMethod=None") 660 | // RFC 3720 page 36 last line, https://tools.ietf.org/html/rfc3720#page-36 661 | // The session type is defined during login with the key=value parameter 662 | // in the login command. 663 | loginReq.AddParam("SessionType=Normal") 664 | loginReq.AddParam(fmt.Sprintf("InitiatorName=%s", s.opts.InitiatorName)) 665 | loginReq.AddParam(fmt.Sprintf("TargetName=%s", s.opts.Volume)) 666 | 667 | if err := s.netlink.SendPDU(s.sid, s.cid, &loginReq); err != nil { 668 | return fmt.Errorf("sendPDU: %v", err) 669 | } 670 | 671 | response, err := s.netlink.RecvPDU(s.sid, s.cid) 672 | if err != nil { 673 | return fmt.Errorf("recvpdu: %v", err) 674 | } 675 | if err = s.processLoginResponse(response); err != nil { 676 | return err 677 | } 678 | } 679 | 680 | for s.currStage != ISCSI_FULL_FEATURE_PHASE { 681 | loginReq := IscsiLoginPdu{ 682 | Header: LoginHdr{ 683 | Opcode: ISCSI_OP_LOGIN | ISCSI_OP_IMMEDIATE, 684 | MaxVersion: ISCSI_VERSION, 685 | MinVersion: ISCSI_VERSION, 686 | ExpStatSN: s.expStatSN, 687 | Tsih: s.tsih, 688 | Flags: uint8((s.currStage << 2) | ISCSI_FULL_FEATURE_PHASE | ISCSI_FLAG_LOGIN_TRANSIT), 689 | }, 690 | } 691 | hton48(&loginReq.Header.Isid, int(s.sid)) 692 | loginReq.AddParam(fmt.Sprintf("InitiatorName=%s", s.opts.InitiatorName)) 693 | loginReq.AddParam(fmt.Sprintf("TargetName=%s", s.opts.Volume)) 694 | loginReq.AddParam("SessionType=Normal") 695 | loginReq.AddParam(fmt.Sprintf("MaxRecvDataSegmentLength=%d", s.opts.MaxRecvDLength)) 696 | loginReq.AddParam(fmt.Sprintf("FirstBurstLength=%d", s.opts.FirstBurstLength)) 697 | loginReq.AddParam(fmt.Sprintf("MaxBurstLength=%d", s.opts.MaxBurstLength)) 698 | loginReq.AddParam(fmt.Sprintf("HeaderDigest=%v", s.opts.HeaderDigest)) 699 | loginReq.AddParam(fmt.Sprintf("DataDigest=%v", s.opts.DataDigest)) 700 | loginReq.AddParam(fmt.Sprintf("InitialR2T=%v", iscsiBoolStr(s.opts.InitialR2T))) 701 | loginReq.AddParam(fmt.Sprintf("ImmediateData=%v", iscsiBoolStr(s.opts.ImmediateData))) 702 | loginReq.AddParam(fmt.Sprintf("DataPDUInOrder=%v", iscsiBoolStr(s.opts.DataPDUInOrder))) 703 | loginReq.AddParam(fmt.Sprintf("DataSequenceInOrder=%v", iscsiBoolStr(s.opts.DataSequenceInOrder))) 704 | 705 | if err := s.netlink.SendPDU(s.sid, s.cid, &loginReq); err != nil { 706 | return fmt.Errorf("sendpdu2: %v", err) 707 | } 708 | 709 | response, err := s.netlink.RecvPDU(s.sid, s.cid) 710 | if err != nil { 711 | return fmt.Errorf("recvpdu2: %v", err) 712 | } 713 | if err = s.processLoginResponse(response); err != nil { 714 | return err 715 | } 716 | } 717 | return nil 718 | 719 | } 720 | 721 | // MountIscsi connects to the given iscsi target and mounts it, returning the 722 | // device name on success 723 | func MountIscsi(opts ...Option) ([]string, error) { 724 | netlink, err := ConnectNetlink() 725 | if err != nil { 726 | return nil, err 727 | } 728 | 729 | session := NewSession(netlink, opts...) 730 | if err = session.Connect(); err != nil { 731 | return nil, fmt.Errorf("connect: %v", err) 732 | } 733 | 734 | if err := session.Login(); err != nil { 735 | return nil, fmt.Errorf("login: %v", err) 736 | } 737 | 738 | if err := session.SetParams(); err != nil { 739 | return nil, fmt.Errorf("params: %v", err) 740 | } 741 | 742 | if err := session.Start(); err != nil { 743 | return nil, fmt.Errorf("start: %v", err) 744 | } 745 | 746 | devnames, err := session.ConfigureBlockDevs() 747 | if err != nil { 748 | return nil, err 749 | } 750 | 751 | for i := range devnames { 752 | if err := ReReadPartitionTable("/dev/" + devnames[i]); err != nil { 753 | return nil, err 754 | } 755 | } 756 | 757 | return devnames, nil 758 | } 759 | 760 | // TearDownIscsi tears down the specified session 761 | func TearDownIscsi(sid uint32, cid uint32) error { 762 | netlink, err := ConnectNetlink() 763 | if err != nil { 764 | return err 765 | } 766 | session := IscsiTargetSession{sid: sid, cid: cid, netlink: netlink} 767 | 768 | return session.TearDown() 769 | } 770 | --------------------------------------------------------------------------------