This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -124,6 +125,17 @@ func (a *App) handleGetPool(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if detail is requested
|
||||
if r.URL.Query().Get("detail") == "true" {
|
||||
detail, err := a.zfs.GetPoolDetail(name)
|
||||
if err != nil {
|
||||
writeError(w, errors.ErrNotFound("pool not found").WithDetails(err.Error()))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, detail)
|
||||
return
|
||||
}
|
||||
|
||||
pool, err := a.zfs.GetPool(name)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
|
||||
@@ -133,6 +145,43 @@ func (a *App) handleGetPool(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, pool)
|
||||
}
|
||||
|
||||
func (a *App) handleAddSpareDisk(w http.ResponseWriter, r *http.Request) {
|
||||
name := pathParam(r, "/api/v1/pools/")
|
||||
name = strings.TrimSuffix(name, "/spare")
|
||||
if name == "" {
|
||||
writeError(w, errors.ErrBadRequest("pool name required"))
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Disk string `json:"disk"` // Disk path like /dev/sdb or sdb
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, errors.ErrBadRequest("invalid request body"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Disk == "" {
|
||||
writeError(w, errors.ErrValidation("disk path required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure disk path starts with /dev/ if not already
|
||||
diskPath := req.Disk
|
||||
if !strings.HasPrefix(diskPath, "/dev/") {
|
||||
diskPath = "/dev/" + diskPath
|
||||
}
|
||||
|
||||
if err := a.zfs.AddSpareDisk(name, diskPath); err != nil {
|
||||
log.Printf("add spare disk error: %v", err)
|
||||
writeError(w, errors.ErrInternal("failed to add spare disk").WithDetails(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{"message": "spare disk added", "pool": name, "disk": diskPath})
|
||||
}
|
||||
|
||||
func (a *App) handleDeletePool(w http.ResponseWriter, r *http.Request) {
|
||||
name := pathParam(r, "/api/v1/pools/")
|
||||
if name == "" {
|
||||
@@ -753,6 +802,13 @@ func (a *App) handleDeleteSnapshotPolicy(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// SMB Share Handlers
|
||||
func (a *App) handleListSMBShares(w http.ResponseWriter, r *http.Request) {
|
||||
// Sync shares from OS (smb.conf) to store
|
||||
// This ensures shares created before service restart are visible
|
||||
if err := a.syncSMBSharesFromOS(); err != nil {
|
||||
log.Printf("warning: failed to sync SMB shares from OS: %v", err)
|
||||
// Continue anyway - return what's in store
|
||||
}
|
||||
|
||||
shares := a.smbStore.List()
|
||||
writeJSON(w, http.StatusOK, shares)
|
||||
}
|
||||
@@ -1151,6 +1207,11 @@ func (a *App) handleDeleteNFSExport(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// iSCSI Handlers
|
||||
func (a *App) handleListISCSITargets(w http.ResponseWriter, r *http.Request) {
|
||||
// Sync targets from OS before listing
|
||||
if err := a.syncISCSITargetsFromOS(); err != nil {
|
||||
log.Printf("warning: failed to sync iSCSI targets from OS: %v", err)
|
||||
}
|
||||
|
||||
targets := a.iscsiStore.List()
|
||||
writeJSON(w, http.StatusOK, targets)
|
||||
}
|
||||
@@ -1158,37 +1219,63 @@ func (a *App) handleListISCSITargets(w http.ResponseWriter, r *http.Request) {
|
||||
func (a *App) handleCreateISCSITarget(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
IQN string `json:"iqn"`
|
||||
Type string `json:"type"` // "disk" or "tape" (default: "disk")
|
||||
Initiators []string `json:"initiators"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
log.Printf("create iSCSI target: invalid request body: %v", err)
|
||||
writeError(w, errors.ErrBadRequest("invalid request body").WithDetails(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// Validate and set target type
|
||||
targetType := models.ISCSITargetTypeDisk // Default to disk mode
|
||||
if req.Type != "" {
|
||||
if req.Type != "disk" && req.Type != "tape" {
|
||||
writeError(w, errors.ErrValidation("invalid target type: must be 'disk' or 'tape'"))
|
||||
return
|
||||
}
|
||||
targetType = models.ISCSITargetType(req.Type)
|
||||
}
|
||||
|
||||
log.Printf("create iSCSI target: IQN=%s, Type=%s, Initiators=%v", req.IQN, targetType, req.Initiators)
|
||||
|
||||
// Validate IQN format
|
||||
if err := validation.ValidateIQN(req.IQN); err != nil {
|
||||
log.Printf("IQN validation error: %v (IQN: %s)", err, req.IQN)
|
||||
writeError(w, errors.ErrValidation(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
target, err := a.iscsiStore.Create(req.IQN, req.Initiators)
|
||||
target, err := a.iscsiStore.CreateWithType(req.IQN, targetType, req.Initiators)
|
||||
if err != nil {
|
||||
if err == storage.ErrISCSITargetExists {
|
||||
writeJSON(w, http.StatusConflict, map[string]string{"error": "target with this IQN already exists"})
|
||||
log.Printf("create iSCSI target: target already exists (IQN: %s)", req.IQN)
|
||||
writeError(w, errors.ErrConflict("target with this IQN already exists"))
|
||||
return
|
||||
}
|
||||
log.Printf("create iSCSI target error: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
writeError(w, errors.ErrInternal("failed to create iSCSI target").WithDetails(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("create iSCSI target: target created in store (ID: %s, IQN: %s)", target.ID, target.IQN)
|
||||
|
||||
// Apply configuration to iSCSI service
|
||||
targets := a.iscsiStore.List()
|
||||
if err := a.iscsiService.ApplyConfiguration(targets); err != nil {
|
||||
log.Printf("apply iSCSI configuration error: %v", err)
|
||||
log.Printf("create iSCSI target: apply configuration error: %v", err)
|
||||
// Don't fail the request if configuration fails - target is already in store
|
||||
// User can retry configuration later
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"target": target,
|
||||
"warning": "target created but configuration may have failed. check logs.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("create iSCSI target: success (ID: %s, IQN: %s)", target.ID, target.IQN)
|
||||
writeJSON(w, http.StatusCreated, target)
|
||||
}
|
||||
|
||||
@@ -1325,7 +1412,10 @@ func (a *App) handleAddLUN(w http.ResponseWriter, r *http.Request) {
|
||||
id := parts[0]
|
||||
|
||||
var req struct {
|
||||
ZVOL string `json:"zvol"`
|
||||
ZVOL string `json:"zvol"` // ZVOL name (for block backstore)
|
||||
Device string `json:"device"` // Device path (e.g., /dev/st0 for tape)
|
||||
Backstore string `json:"backstore"` // Backstore type: "block", "pscsi", "fileio" (optional, auto-detected)
|
||||
BackstoreName string `json:"backstore_name"` // Custom backstore name (optional)
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -1333,35 +1423,55 @@ func (a *App) handleAddLUN(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.ZVOL == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "zvol is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate ZVOL exists
|
||||
zvols, err := a.zfs.ListZVOLs("")
|
||||
if err != nil {
|
||||
log.Printf("list zvols error: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to validate zvol"})
|
||||
// Validate: must have either ZVOL or Device
|
||||
if req.ZVOL == "" && req.Device == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "either zvol or device is required"})
|
||||
return
|
||||
}
|
||||
|
||||
var zvolSize uint64
|
||||
zvolExists := false
|
||||
for _, zvol := range zvols {
|
||||
if zvol.Name == req.ZVOL {
|
||||
zvolExists = true
|
||||
zvolSize = zvol.Size
|
||||
break
|
||||
var zvolName string
|
||||
|
||||
if req.ZVOL != "" {
|
||||
// Validate ZVOL exists
|
||||
zvols, err := a.zfs.ListZVOLs("")
|
||||
if err != nil {
|
||||
log.Printf("list zvols error: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to validate zvol"})
|
||||
return
|
||||
}
|
||||
|
||||
zvolExists := false
|
||||
for _, zvol := range zvols {
|
||||
if zvol.Name == req.ZVOL {
|
||||
zvolExists = true
|
||||
zvolSize = zvol.Size
|
||||
zvolName = zvol.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !zvolExists {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "zvol not found"})
|
||||
return
|
||||
}
|
||||
} else if req.Device != "" {
|
||||
// Validate device exists
|
||||
if _, err := os.Stat(req.Device); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": fmt.Sprintf("device not found: %s", req.Device)})
|
||||
return
|
||||
}
|
||||
log.Printf("stat device error: %v", err)
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to validate device"})
|
||||
return
|
||||
}
|
||||
// For tape devices, size is typically 0 or unknown
|
||||
zvolSize = 0
|
||||
}
|
||||
|
||||
if !zvolExists {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "zvol not found"})
|
||||
return
|
||||
}
|
||||
|
||||
lun, err := a.iscsiStore.AddLUN(id, req.ZVOL, zvolSize)
|
||||
// Use updated AddLUN signature that supports device and backstore
|
||||
lun, err := a.iscsiStore.AddLUNWithDevice(id, zvolName, req.Device, zvolSize, req.Backstore, req.BackstoreName)
|
||||
if err != nil {
|
||||
if err == storage.ErrISCSITargetNotFound {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "target not found"})
|
||||
@@ -1804,3 +1914,326 @@ func (a *App) syncNFSExportsFromOS() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncSMBSharesFromOS syncs SMB shares from /etc/samba/smb.conf to the store
|
||||
func (a *App) syncSMBSharesFromOS() error {
|
||||
configPath := "/etc/samba/smb.conf"
|
||||
cmd := exec.Command("sudo", "-n", "cat", configPath)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
// If can't read smb.conf, that's okay - might not exist yet
|
||||
return nil
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
currentShare := ""
|
||||
inShareSection := false
|
||||
sharePath := ""
|
||||
shareReadOnly := false
|
||||
shareGuestOK := false
|
||||
shareDescription := ""
|
||||
shareValidUsers := []string{}
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this is a share section
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
// Save previous share if exists
|
||||
if inShareSection && currentShare != "" && sharePath != "" {
|
||||
// Try to find corresponding dataset
|
||||
datasets, err := a.zfs.ListDatasets("")
|
||||
var dataset string
|
||||
if err == nil {
|
||||
for _, ds := range datasets {
|
||||
if ds.Mountpoint == sharePath {
|
||||
dataset = ds.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if share already exists
|
||||
existingShares := a.smbStore.List()
|
||||
exists := false
|
||||
for _, share := range existingShares {
|
||||
if share.Name == currentShare || share.Path == sharePath {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !exists {
|
||||
_, err = a.smbStore.Create(currentShare, sharePath, dataset, shareDescription, shareReadOnly, shareGuestOK, shareValidUsers)
|
||||
if err != nil && err != storage.ErrSMBShareExists {
|
||||
log.Printf("warning: failed to sync SMB share %s: %v", currentShare, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start new share section
|
||||
shareName := strings.Trim(line, "[]")
|
||||
if shareName != "global" && shareName != "printers" && shareName != "print$" {
|
||||
currentShare = shareName
|
||||
inShareSection = true
|
||||
sharePath = ""
|
||||
shareReadOnly = false
|
||||
shareGuestOK = false
|
||||
shareDescription = ""
|
||||
shareValidUsers = []string{}
|
||||
} else {
|
||||
inShareSection = false
|
||||
currentShare = ""
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse share properties
|
||||
if inShareSection && currentShare != "" {
|
||||
if strings.HasPrefix(line, "path = ") {
|
||||
sharePath = strings.TrimSpace(strings.TrimPrefix(line, "path = "))
|
||||
} else if strings.HasPrefix(line, "read only = ") {
|
||||
value := strings.TrimSpace(strings.TrimPrefix(line, "read only = "))
|
||||
shareReadOnly = (value == "yes" || value == "true")
|
||||
} else if strings.HasPrefix(line, "guest ok = ") {
|
||||
value := strings.TrimSpace(strings.TrimPrefix(line, "guest ok = "))
|
||||
shareGuestOK = (value == "yes" || value == "true")
|
||||
} else if strings.HasPrefix(line, "comment = ") {
|
||||
shareDescription = strings.TrimSpace(strings.TrimPrefix(line, "comment = "))
|
||||
} else if strings.HasPrefix(line, "valid users = ") {
|
||||
usersStr := strings.TrimSpace(strings.TrimPrefix(line, "valid users = "))
|
||||
shareValidUsers = strings.Split(usersStr, ",")
|
||||
for i := range shareValidUsers {
|
||||
shareValidUsers[i] = strings.TrimSpace(shareValidUsers[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save last share if exists
|
||||
if inShareSection && currentShare != "" && sharePath != "" {
|
||||
datasets, err := a.zfs.ListDatasets("")
|
||||
var dataset string
|
||||
if err == nil {
|
||||
for _, ds := range datasets {
|
||||
if ds.Mountpoint == sharePath {
|
||||
dataset = ds.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
existingShares := a.smbStore.List()
|
||||
exists := false
|
||||
for _, share := range existingShares {
|
||||
if share.Name == currentShare || share.Path == sharePath {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !exists {
|
||||
_, err = a.smbStore.Create(currentShare, sharePath, dataset, shareDescription, shareReadOnly, shareGuestOK, shareValidUsers)
|
||||
if err != nil && err != storage.ErrSMBShareExists {
|
||||
log.Printf("warning: failed to sync SMB share %s: %v", currentShare, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncISCSITargetsFromOS syncs iSCSI targets from targetcli to the store
|
||||
func (a *App) syncISCSITargetsFromOS() error {
|
||||
log.Printf("debug: starting syncISCSITargetsFromOS")
|
||||
// Get list of targets from targetcli
|
||||
// Set TARGETCLI_HOME and TARGETCLI_LOCK_DIR to writable directories
|
||||
// Create the directories first if they don't exist
|
||||
os.MkdirAll("/tmp/.targetcli", 0755)
|
||||
os.MkdirAll("/tmp/targetcli-run", 0755)
|
||||
// Use sudo to run as root, then set environment variables in the command
|
||||
cmd := exec.Command("sh", "-c", "sudo -n sh -c 'TARGETCLI_HOME=/tmp/.targetcli TARGETCLI_LOCK_DIR=/tmp/targetcli-run targetcli /iscsi ls'")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
// Log the error but don't fail - targetcli might not be configured
|
||||
log.Printf("warning: failed to list iSCSI targets from targetcli: %v (output: %s)", err, string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("debug: targetcli output: %s", string(output))
|
||||
lines := strings.Split(string(output), "\n")
|
||||
var currentIQN string
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this is a target line (starts with "o- iqn.")
|
||||
if strings.HasPrefix(line, "o- iqn.") {
|
||||
log.Printf("debug: found target line: %s", line)
|
||||
// Extract IQN from line like "o- iqn.2025-12.com.atlas:target-1"
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 {
|
||||
currentIQN = parts[1]
|
||||
|
||||
// Check if target already exists in store
|
||||
existingTargets := a.iscsiStore.List()
|
||||
exists := false
|
||||
for _, t := range existingTargets {
|
||||
if t.IQN == currentIQN {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !exists {
|
||||
// Try to determine target type from IQN
|
||||
targetType := models.ISCSITargetTypeDisk // Default to disk mode
|
||||
if strings.Contains(strings.ToLower(currentIQN), "tape") {
|
||||
targetType = models.ISCSITargetTypeTape
|
||||
}
|
||||
|
||||
// Create target in store
|
||||
target, err := a.iscsiStore.CreateWithType(currentIQN, targetType, []string{})
|
||||
if err != nil && err != storage.ErrISCSITargetExists {
|
||||
log.Printf("warning: failed to sync iSCSI target %s: %v", currentIQN, err)
|
||||
} else if err == nil {
|
||||
log.Printf("synced iSCSI target from OS: %s (type: %s)", currentIQN, targetType)
|
||||
|
||||
// Now try to sync LUNs for this target
|
||||
if err := a.syncLUNsFromOS(currentIQN, target.ID, targetType); err != nil {
|
||||
log.Printf("warning: failed to sync LUNs for target %s: %v", currentIQN, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncLUNsFromOS syncs LUNs for a specific target from targetcli
|
||||
func (a *App) syncLUNsFromOS(iqn, targetID string, targetType models.ISCSITargetType) error {
|
||||
// Get LUNs for this target
|
||||
// Use sudo to run as root, then set environment variables in the command
|
||||
cmd := exec.Command("sh", "-c", "sudo -n sh -c 'TARGETCLI_HOME=/tmp/.targetcli TARGETCLI_LOCK_DIR=/tmp/targetcli-run targetcli /iscsi/"+iqn+"/tpg1/luns ls'")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
// No LUNs or can't read - that's okay, log for debugging
|
||||
log.Printf("debug: failed to list LUNs for target %s: %v (output: %s)", iqn, err, string(output))
|
||||
return nil
|
||||
}
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "o- lun") {
|
||||
// Parse LUN line like "o- lun0 ....................................... [block/pool-test-02-vol01 (/dev/zvol/pool-test-02/vol01) (default_tg_pt_gp)]"
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 {
|
||||
// Extract LUN ID from "lun0"
|
||||
lunIDStr := strings.TrimPrefix(parts[1], "lun")
|
||||
lunID, err := strconv.Atoi(lunIDStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract backstore path and device from the line
|
||||
var backstorePath string
|
||||
var devicePath string
|
||||
var zvolName string
|
||||
|
||||
// Find the part with brackets - might span multiple parts
|
||||
fullLine := strings.Join(parts, " ")
|
||||
start := strings.Index(fullLine, "[")
|
||||
end := strings.LastIndex(fullLine, "]")
|
||||
if start >= 0 && end > start {
|
||||
content := fullLine[start+1 : end]
|
||||
// Parse content like "block/pool-test-02-vol01 (/dev/zvol/pool-test-02/vol01)"
|
||||
if strings.Contains(content, "(") {
|
||||
// Has device path
|
||||
parts2 := strings.Split(content, "(")
|
||||
if len(parts2) >= 2 {
|
||||
backstorePath = strings.TrimSpace(parts2[0])
|
||||
devicePath = strings.Trim(strings.TrimSpace(parts2[1]), "()")
|
||||
|
||||
// If device is a zvol, extract ZVOL name
|
||||
if strings.HasPrefix(devicePath, "/dev/zvol/") {
|
||||
zvolName = strings.TrimPrefix(devicePath, "/dev/zvol/")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
backstorePath = content
|
||||
}
|
||||
}
|
||||
|
||||
// Check if LUN already exists
|
||||
target, err := a.iscsiStore.Get(targetID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
lunExists := false
|
||||
for _, lun := range target.LUNs {
|
||||
if lun.ID == lunID {
|
||||
lunExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !lunExists {
|
||||
// Determine backstore type
|
||||
backstoreType := "block"
|
||||
if strings.HasPrefix(backstorePath, "pscsi/") {
|
||||
backstoreType = "pscsi"
|
||||
} else if strings.HasPrefix(backstorePath, "fileio/") {
|
||||
backstoreType = "fileio"
|
||||
}
|
||||
|
||||
// Get size if it's a ZVOL
|
||||
var size uint64
|
||||
if zvolName != "" {
|
||||
zvols, err := a.zfs.ListZVOLs("")
|
||||
if err == nil {
|
||||
for _, zvol := range zvols {
|
||||
if zvol.Name == zvolName {
|
||||
size = zvol.Size
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add LUN to store
|
||||
if targetType == models.ISCSITargetTypeTape && devicePath != "" {
|
||||
// Tape mode: use device
|
||||
_, err := a.iscsiStore.AddLUNWithDevice(targetID, "", devicePath, size, backstoreType, "")
|
||||
if err != nil && err != storage.ErrLUNExists {
|
||||
log.Printf("warning: failed to sync LUN %d for target %s: %v", lunID, iqn, err)
|
||||
}
|
||||
} else if zvolName != "" {
|
||||
// Disk mode: use ZVOL
|
||||
_, err := a.iscsiStore.AddLUNWithDevice(targetID, zvolName, "", size, backstoreType, "")
|
||||
if err != nil && err != storage.ErrLUNExists {
|
||||
log.Printf("warning: failed to sync LUN %d for target %s: %v", lunID, iqn, err)
|
||||
}
|
||||
} else if devicePath != "" {
|
||||
// Generic device
|
||||
_, err := a.iscsiStore.AddLUNWithDevice(targetID, "", devicePath, size, backstoreType, "")
|
||||
if err != nil && err != storage.ErrLUNExists {
|
||||
log.Printf("warning: failed to sync LUN %d for target %s: %v", lunID, iqn, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user