3 Star 0 Fork 0

mirrors_hugelgupf/iscsinl

Create your Gitee Account
Explore and code with more than 14 million developers,Free private repositories !:)
Sign up
文件
Clone or Download
netlink.go 17.97 KB
Copy Edit Raw Blame History
Khazhismel Kumykov authored 2020-01-22 06:53 +08:00 . iscsinl: more configuration
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
package iscsinl
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io/ioutil"
"log"
"strconv"
"sync/atomic"
"syscall"
"unsafe"
"github.com/vishvananda/netlink/nl"
"golang.org/x/sys/unix"
)
// STOP_CONN_RECOVER - when stopping connection clean up I/O on that connection
const STOP_CONN_RECOVER = 0x3
// IscsiEvent iscsi_if.h:enum iscsi_uevent_e
type IscsiEvent uint32
// IscsiEvents that we use
const (
UEVENT_BASE IscsiEvent = 10
KEVENT_BASE = 100
ISCSI_UEVENT_CREATE_SESSION = UEVENT_BASE + 1
ISCSI_UEVENT_DESTROY_SESSION = UEVENT_BASE + 2
ISCSI_UEVENT_CREATE_CONN = UEVENT_BASE + 3
ISCSI_UEVENT_DESTROY_CONN = UEVENT_BASE + 4
ISCSI_UEVENT_BIND_CONN = UEVENT_BASE + 5
ISCSI_UEVENT_SET_PARAM = UEVENT_BASE + 6
ISCSI_UEVENT_START_CONN = UEVENT_BASE + 7
ISCSI_UEVENT_STOP_CONN = UEVENT_BASE + 8
ISCSI_UEVENT_SEND_PDU = UEVENT_BASE + 9
ISCSI_KEVENT_RECV_PDU = KEVENT_BASE + 1
ISCSI_KEVENT_CONN_ERROR = KEVENT_BASE + 2
ISCSI_KEVENT_IF_ERROR = KEVENT_BASE + 3
ISCSI_KEVENT_CREATE_SESSION = KEVENT_BASE + 6
)
// IscsiParam iscsi_if.h:enum iscsi_param
type IscsiParam uint32
// IscsiParams up until INITIATOR_NAME
const (
ISCSI_PARAM_MAX_RECV_DLENGTH IscsiParam = iota
ISCSI_PARAM_MAX_XMIT_DLENGTH
ISCSI_PARAM_HDRDGST_EN
ISCSI_PARAM_DATADGST_EN
ISCSI_PARAM_INITIAL_R2T_EN
ISCSI_PARAM_MAX_R2T
ISCSI_PARAM_IMM_DATA_EN
ISCSI_PARAM_FIRST_BURST
ISCSI_PARAM_MAX_BURST
ISCSI_PARAM_PDU_INORDER_EN
ISCSI_PARAM_DATASEQ_INORDER_EN
ISCSI_PARAM_ERL
ISCSI_PARAM_IFMARKER_EN
ISCSI_PARAM_OFMARKER_EN
ISCSI_PARAM_EXP_STATSN
ISCSI_PARAM_TARGET_NAME
ISCSI_PARAM_TPGT
ISCSI_PARAM_PERSISTENT_ADDRESS
ISCSI_PARAM_PERSISTENT_PORT
ISCSI_PARAM_SESS_RECOVERY_TMO
ISCSI_PARAM_CONN_PORT
ISCSI_PARAM_CONN_ADDRESS
ISCSI_PARAM_USERNAME
ISCSI_PARAM_USERNAME_IN
ISCSI_PARAM_PASSWORD
ISCSI_PARAM_PASSWORD_IN
ISCSI_PARAM_FAST_ABORT
ISCSI_PARAM_ABORT_TMO
ISCSI_PARAM_LU_RESET_TMO
ISCSI_PARAM_HOST_RESET_TMO
ISCSI_PARAM_PING_TMO
ISCSI_PARAM_RECV_TMO
ISCSI_PARAM_IFACE_NAME
ISCSI_PARAM_ISID
ISCSI_PARAM_INITIATOR_NAME
)
// IscsiErr iscsi_if.h:enum iscsi_err
type IscsiErr uint32
// IscsiErr iscsi_if.h:enum iscsi_err
const (
ISCSI_OK IscsiErr = 0
ISCSI_ERR_BASE = 1000
ISCSI_ERR_DATASN = ISCSI_ERR_BASE + 1
ISCSI_ERR_DATA_OFFSET = ISCSI_ERR_BASE + 2
ISCSI_ERR_MAX_CMDSN = ISCSI_ERR_BASE + 3
ISCSI_ERR_EXP_CMDSN = ISCSI_ERR_BASE + 4
ISCSI_ERR_BAD_OPCODE = ISCSI_ERR_BASE + 5
ISCSI_ERR_DATALEN = ISCSI_ERR_BASE + 6
ISCSI_ERR_AHSLEN = ISCSI_ERR_BASE + 7
ISCSI_ERR_PROTO = ISCSI_ERR_BASE + 8
ISCSI_ERR_LUN = ISCSI_ERR_BASE + 9
ISCSI_ERR_BAD_ITT = ISCSI_ERR_BASE + 10
ISCSI_ERR_CONN_FAILED = ISCSI_ERR_BASE + 11
ISCSI_ERR_R2TSN = ISCSI_ERR_BASE + 12
ISCSI_ERR_SESSION_FAILED = ISCSI_ERR_BASE + 13
ISCSI_ERR_HDR_DGST = ISCSI_ERR_BASE + 14
ISCSI_ERR_DATA_DGST = ISCSI_ERR_BASE + 15
ISCSI_ERR_PARAM_NOT_FOUND = ISCSI_ERR_BASE + 16
ISCSI_ERR_NO_SCSI_CMD = ISCSI_ERR_BASE + 17
ISCSI_ERR_INVALID_HOST = ISCSI_ERR_BASE + 18
ISCSI_ERR_XMIT_FAILED = ISCSI_ERR_BASE + 19
ISCSI_ERR_TCP_CONN_CLOSE = ISCSI_ERR_BASE + 20
ISCSI_ERR_SCSI_EH_SESSION_RST = ISCSI_ERR_BASE + 21
ISCSI_ERR_NOP_TIMEDOUT = ISCSI_ERR_BASE + 22
)
func (e IscsiErr) String() string {
switch e {
case ISCSI_OK:
return "ISCSI_OK"
case ISCSI_ERR_BASE:
return "ISCSI_ERR_BASE"
case ISCSI_ERR_DATASN:
return "ISCSI_ERR_DATASN"
case ISCSI_ERR_DATA_OFFSET:
return "ISCSI_ERR_DATA_OFFSET"
case ISCSI_ERR_MAX_CMDSN:
return "ISCSI_ERR_MAX_CMDSN"
case ISCSI_ERR_EXP_CMDSN:
return "ISCSI_ERR_EXP_CMDSN"
case ISCSI_ERR_BAD_OPCODE:
return "ISCSI_ERR_BAD_OPCODE"
case ISCSI_ERR_DATALEN:
return "ISCSI_ERR_DATALEN"
case ISCSI_ERR_AHSLEN:
return "ISCSI_ERR_AHSLEN"
case ISCSI_ERR_PROTO:
return "ISCSI_ERR_PROTO"
case ISCSI_ERR_LUN:
return "ISCSI_ERR_LUN"
case ISCSI_ERR_BAD_ITT:
return "ISCSI_ERR_BAD_ITT"
case ISCSI_ERR_CONN_FAILED:
return "ISCSI_ERR_CONN_FAILED"
case ISCSI_ERR_R2TSN:
return "ISCSI_ERR_R2TSN"
case ISCSI_ERR_SESSION_FAILED:
return "ISCSI_ERR_SESSION_FAILED"
case ISCSI_ERR_HDR_DGST:
return "ISCSI_ERR_HDR_DGST"
case ISCSI_ERR_DATA_DGST:
return "ISCSI_ERR_DATA_DGST"
case ISCSI_ERR_PARAM_NOT_FOUND:
return "ISCSI_ERR_PARAM_NOT_FOUND"
case ISCSI_ERR_NO_SCSI_CMD:
return "ISCSI_ERR_NO_SCSI_CMD"
case ISCSI_ERR_INVALID_HOST:
return "ISCSI_ERR_INVALID_HOST"
case ISCSI_ERR_XMIT_FAILED:
return "ISCSI_ERR_XMIT_FAILED"
case ISCSI_ERR_TCP_CONN_CLOSE:
return "ISCSI_ERR_TCP_CONN_CLOSE"
case ISCSI_ERR_SCSI_EH_SESSION_RST:
return "ISCSI_ERR_SCSI_EH_SESSION_RST"
case ISCSI_ERR_NOP_TIMEDOUT:
return "ISCSI_ERR_NOP_TIMEDOUT"
default:
return strconv.Itoa(int(e))
}
}
// All Iscsu[U/K]event structs must be the same size,
// with UserArg always 24 bytes, ReturnArg always 16 bytes
// iSCSIUEvent is a generic uevent, for reading header content
type iSCSIUEvent struct {
Type IscsiEvent
IfError uint32
TransportHandle uint64
UserArg [24]byte
ReturnArg [16]byte
}
// iSCSIUEventCreateSession corresponds with ISCSI_UEVENT_CREATE_SESSION
type iSCSIUEventCreateSession struct {
Type IscsiEvent
IfError uint32
TransportHandle uint64
CSession struct {
InitialCmdSN uint32
CmdsMax uint16
QueueDepth uint16
_ [16]byte
}
CSessionRet struct {
Sid uint32
HostNo uint32
_ [8]byte
}
}
type iSCSIUEventDestroySession struct {
Type IscsiEvent
IfError uint32
TransportHandle uint64
DSession struct {
Sid uint32
_ [20]byte
}
DSessionRet struct {
Ret uint32
_ [12]byte
}
}
type iSCSIUEventCreateConnection struct {
Type IscsiEvent
IfError uint32
TransportHandle uint64
CConn struct {
Sid uint32
Cid uint32
_ [16]byte
}
CConnRet struct {
Sid uint32
Cid uint32
_ [8]byte
}
}
type iSCSIUEventDestroyConnection struct {
Type IscsiEvent
IfError uint32
TransportHandle uint64
DConn struct {
Sid uint32
Cid uint32
_ [16]byte
}
DConnRet struct {
Ret uint32
_ [12]byte
}
}
type iSCSIUEventBindConnection struct {
Type IscsiEvent
IfError uint32
TransportHandle uint64
BConn struct {
Sid uint32
Cid uint32
TransportEph uint64
IsLeading uint32
_ [4]byte
}
BConnRet struct {
Ret uint32
_ [12]byte
}
}
type iSCSIUEventSetParam struct {
Type IscsiEvent
IfError uint32
TransportHandle uint64
SetParam struct {
Sid uint32
Cid uint32
Param IscsiParam
Len uint32
_ [8]byte
}
SetParamRet struct {
Ret uint32
_ [12]byte
}
}
type iSCSIUEventStartConnection struct {
Type IscsiEvent
IfError uint32
TransportHandle uint64
StartConn struct {
Sid uint32
Cid uint32
_ [16]byte
}
StartConnRet struct {
Ret uint32
_ [12]byte
}
}
type iSCSIUEventStopConnection struct {
Type IscsiEvent
IfError uint32
TransportHandle uint64
StopConn struct {
Sid uint32
Cid uint32
ConnHandle uint64
Flag uint32
_ [4]byte
}
StopConnRet struct {
Ret uint32
_ [12]byte
}
}
type iSCSIUEventSendPDU struct {
Type IscsiEvent
IfError uint32
TransportHandle uint64
SendPDU struct {
Sid uint32
Cid uint32
HdrSize uint32
DataSize uint32
_ [8]byte
}
SendPDURet struct {
Ret int32
_ [12]byte
}
}
type iSCSIKEventConnError struct {
Type IscsiEvent
IfError uint32
TransportHandle uint64
_ [24]byte
ConnErr struct {
Sid uint32
Cid uint32
Error IscsiErr
_ [4]byte
}
}
type iSCSIKEventRecvEvent struct {
Type IscsiEvent
IfError uint32
TransportHandle uint64
_ [24]byte
RecvReq struct {
Sid uint32
Cid uint32
RecvHandle uint64
}
}
// IscsiIpcConn is a single netlink connection
type IscsiIpcConn struct {
Conn *nl.NetlinkSocket
TransportHandle uint64
nextSeqNr uint32
}
// ConnectNetlink connects to the iscsi netlink socket, and if successful returns
// an IscsiIpcConn ready to accept commands.
func ConnectNetlink() (*IscsiIpcConn, error) {
conn, err := nl.Subscribe(unix.NETLINK_ISCSI, 1)
if err != nil {
return nil, err
}
data, err := ioutil.ReadFile("/sys/class/iscsi_transport/tcp/handle")
if err != nil {
return nil, err
}
handle, err := strconv.ParseUint(string(data[:len(data)-1]), 10, 64)
if err != nil {
return nil, err
}
conn.SetReceiveTimeout(&unix.Timeval{Sec: 30})
conn.SetSendTimeout(&unix.Timeval{Sec: 30})
return &IscsiIpcConn{Conn: conn, TransportHandle: handle}, nil
}
// WaitFor reads ipcs until event of type Type is received, discarding others.
// (This presumes we're the only ones using netlink on this host, and we only
// have one outstanding request we're waiting for...)
func (c *IscsiIpcConn) WaitFor(Type IscsiEvent) (*syscall.NetlinkMessage, error) {
for {
msgs, _, err := c.Conn.Receive()
if err != nil {
return nil, err
}
for _, msg := range msgs {
reader := bytes.NewReader(msg.Data)
var uevent iSCSIUEvent
err = binary.Read(reader, binary.LittleEndian, &uevent)
if err != nil {
return nil, err
}
if uevent.TransportHandle != c.TransportHandle {
return nil, fmt.Errorf("wrong transport handle: %v", uevent.TransportHandle)
}
if uevent.Type == Type {
return &msg, nil
} else if uevent.Type == ISCSI_KEVENT_CONN_ERROR {
reader.Seek(0, 0)
var connErr iSCSIKEventConnError
binary.Read(reader, binary.LittleEndian, &connErr)
return nil, fmt.Errorf("connection error: %+v", connErr)
} else if uevent.Type == ISCSI_KEVENT_IF_ERROR {
return nil, fmt.Errorf("interface error: %v (invalid netlink message?)", uevent.IfError)
}
log.Printf("Dumping unexpected event of type %d", uevent.Type)
}
}
}
// FillNetlink aids attaching binary data to netlink request
func FillNetlink(request *nl.NetlinkRequest, data ...interface{}) error {
var buf bytes.Buffer
for _, item := range data {
switch v := item.(type) {
case []byte:
_, err := buf.Write(v)
if err != nil {
return err
}
default:
err := binary.Write(&buf, binary.LittleEndian, item)
if err != nil {
return err
}
}
}
request.AddRawData(buf.Bytes())
return nil
}
// DoNetlink send netlink and listen to response
// ueventP *must* be a pointer to iSCSIUEvent
func (c *IscsiIpcConn) DoNetlink(ueventP unsafe.Pointer, data ...interface{}) error {
uevent := (*iSCSIUEvent)(ueventP)
request := nl.NetlinkRequest{
NlMsghdr: unix.NlMsghdr{
Len: uint32(unix.SizeofNlMsghdr),
Type: uint16(uevent.Type),
Flags: uint16(1),
Seq: atomic.AddUint32(&c.nextSeqNr, 1),
},
}
if err := FillNetlink(&request, append([]interface{}{*uevent}, data...)...); err != nil {
return err
}
if err := c.Conn.Send(&request); err != nil {
return err
}
response, err := c.WaitFor(uevent.Type)
if err != nil {
return err
}
reader := bytes.NewReader(response.Data)
return binary.Read(reader, binary.LittleEndian, uevent)
}
// CreateSession creates a new kernel iSCSI session, returning the new
// iscsi_session id and scsi_host id
func (c *IscsiIpcConn) CreateSession(cmdsMax uint16, queueDepth uint16) (sid uint32, hostID uint32, err error) {
cSession := iSCSIUEventCreateSession{
Type: ISCSI_UEVENT_CREATE_SESSION,
IfError: 0,
TransportHandle: c.TransportHandle,
}
cSession.CSession.CmdsMax = cmdsMax
cSession.CSession.QueueDepth = queueDepth
if err := c.DoNetlink(unsafe.Pointer(&cSession)); err != nil {
return 0, 0, err
}
log.Println("Created new session ", cSession)
return cSession.CSessionRet.Sid, cSession.CSessionRet.HostNo, nil
}
// DestroySession attempts to destroy the identified session. May fail if
// connections are still active
func (c *IscsiIpcConn) DestroySession(sid uint32) error {
dSession := iSCSIUEventDestroySession{
Type: ISCSI_UEVENT_DESTROY_SESSION,
IfError: 0,
TransportHandle: c.TransportHandle,
}
dSession.DSession.Sid = sid
if err := c.DoNetlink(unsafe.Pointer(&dSession)); err != nil {
return err
}
if dSession.DSessionRet.Ret != 0 {
return fmt.Errorf("error destroying session: %d", dSession.DSessionRet.Ret)
}
return nil
}
// CreateConnection creates a new iSCSI connection for an existing session,
// returning the connection id on success
func (c *IscsiIpcConn) CreateConnection(sid uint32) (cid uint32, err error) {
cConn := iSCSIUEventCreateConnection{
Type: ISCSI_UEVENT_CREATE_CONN,
IfError: 0,
TransportHandle: c.TransportHandle,
}
cConn.CConn.Sid = sid
if err := c.DoNetlink(unsafe.Pointer(&cConn)); err != nil {
return 0, err
}
log.Println("Created new connection", cConn)
return cConn.CConnRet.Cid, nil
}
// DestroyConnection attempts to destroy the identified connection. May fail if
// connection is still running
func (c *IscsiIpcConn) DestroyConnection(sid uint32, cid uint32) error {
dConn := iSCSIUEventDestroyConnection{
Type: ISCSI_UEVENT_DESTROY_CONN,
IfError: 0,
TransportHandle: c.TransportHandle,
}
dConn.DConn.Sid = sid
dConn.DConn.Cid = cid
if err := c.DoNetlink(unsafe.Pointer(&dConn)); err != nil {
return err
}
if dConn.DConnRet.Ret != 0 {
return fmt.Errorf("error destroying connection: %d", dConn.DConnRet.Ret)
}
return nil
}
// BindConnection binds a TCP socket to the given kernel connection. fd must be
// the current program's fd for the TCP socket.
func (c *IscsiIpcConn) BindConnection(sid uint32, cid uint32, fd int) error {
bConn := iSCSIUEventBindConnection{
Type: ISCSI_UEVENT_BIND_CONN,
IfError: 0,
TransportHandle: c.TransportHandle,
}
bConn.BConn.Sid = sid
bConn.BConn.Cid = cid
bConn.BConn.TransportEph = uint64(fd)
bConn.BConn.IsLeading = 1
err := c.DoNetlink(unsafe.Pointer(&bConn))
if err != nil {
return err
}
log.Println("Binded new connection ", bConn)
if bConn.BConnRet.Ret != 0 {
return fmt.Errorf("error binding connection: %d", bConn.BConnRet.Ret)
}
return nil
}
// SetParam sets a single parameter for the given connection. value will be
// null terminated and must not contain null bytes
func (c *IscsiIpcConn) SetParam(sid uint32, cid uint32, param IscsiParam, value string) error {
setParam := iSCSIUEventSetParam{
Type: ISCSI_UEVENT_SET_PARAM,
TransportHandle: c.TransportHandle,
}
setParam.SetParam.Sid = sid
setParam.SetParam.Cid = cid
setParam.SetParam.Param = param
setParam.SetParam.Len = uint32(len(value) + 1) // Null terminatorrequest
err := c.DoNetlink(unsafe.Pointer(&setParam), []byte(value+"\x00"))
if err != nil {
return err
}
if setParam.SetParamRet.Ret != 0 {
return fmt.Errorf("error setting param %d: %d", param, setParam.SetParamRet.Ret)
}
return nil
}
// StartConnection starts the given connection. The connection should be bound
// and logged in.
func (c *IscsiIpcConn) StartConnection(sid uint32, cid uint32) error {
sConn := iSCSIUEventStartConnection{
Type: ISCSI_UEVENT_START_CONN,
IfError: 0,
TransportHandle: c.TransportHandle,
}
sConn.StartConn.Sid = sid
sConn.StartConn.Cid = cid
err := c.DoNetlink(unsafe.Pointer(&sConn))
if err != nil {
return err
}
if sConn.StartConnRet.Ret != 0 {
return fmt.Errorf("error starting connection: %d", sConn.StartConnRet.Ret)
}
return nil
}
// StopConnection attempts to stop the identified connection.
func (c *IscsiIpcConn) StopConnection(sid uint32, cid uint32) error {
sConn := iSCSIUEventStopConnection{
Type: ISCSI_UEVENT_STOP_CONN,
IfError: 0,
TransportHandle: c.TransportHandle,
}
sConn.StopConn.Sid = sid
sConn.StopConn.Cid = cid
sConn.StopConn.Flag = STOP_CONN_RECOVER
if err := c.DoNetlink(unsafe.Pointer(&sConn)); err != nil {
return err
}
if sConn.StopConnRet.Ret != 0 {
return fmt.Errorf("error stopping connection: %d", sConn.StopConnRet.Ret)
}
return nil
}
// PduLike interface for sending PDUs
type PduLike interface {
// Length of the PDU header (iscsi_hdr/etc.)
HeaderLen() uint32
// Length of the PDU data
DataLen() uint32
// Header + Data
Serialize() []byte
}
// RecvPDU waits for a PDU for the given connection, and returns the raw
// PDU with header on success. RecvPDU assumes a single iSCSI connection,
// and will error if a different connection receives a PDU
func (c *IscsiIpcConn) RecvPDU(sid uint32, cid uint32) ([]byte, error) {
response, err := c.WaitFor(ISCSI_KEVENT_RECV_PDU)
if err != nil {
return nil, err
}
var recvReq iSCSIKEventRecvEvent
reader := bytes.NewReader(response.Data)
binary.Read(reader, binary.LittleEndian, &recvReq)
if recvReq.RecvReq.Sid != sid || recvReq.RecvReq.Cid != cid {
return nil, errors.New("unexpected PDU for different session... is another initiator running?")
}
log.Printf("Response length: %v, data length: %v", response.Header.Len, len(response.Data))
return ioutil.ReadAll(reader)
}
// SendPDU sends the given PDU on the given connection
func (c *IscsiIpcConn) SendPDU(sid uint32, cid uint32, pdu PduLike) error {
sendPdu := iSCSIUEventSendPDU{
Type: ISCSI_UEVENT_SEND_PDU,
TransportHandle: c.TransportHandle,
}
sendPdu.SendPDU.Sid = sid
sendPdu.SendPDU.Cid = cid
sendPdu.SendPDU.HdrSize = pdu.HeaderLen()
sendPdu.SendPDU.DataSize = pdu.DataLen()
if err := c.DoNetlink(unsafe.Pointer(&sendPdu), pdu.Serialize()); err != nil {
return err
}
if sendPdu.SendPDURet.Ret != 0 {
return fmt.Errorf("SendPDU had unexpected error code %d", sendPdu.SendPDURet.Ret)
}
return nil
}
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/mirrors_hugelgupf/iscsinl.git
git@gitee.com:mirrors_hugelgupf/iscsinl.git
mirrors_hugelgupf
iscsinl
iscsinl
31902154701f

Search