github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/storage/plans/iscsi/iscsi.go (about)

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package iscsi
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/juju/clock"
    17  	"github.com/juju/errors"
    18  	"github.com/juju/loggo"
    19  	"github.com/juju/retry"
    20  	"github.com/juju/utils/v3/exec"
    21  
    22  	"github.com/juju/juju/storage"
    23  	"github.com/juju/juju/storage/plans/common"
    24  )
    25  
    26  var logger = loggo.GetLogger("juju.storage.plans.iscsi")
    27  
    28  const (
    29  	// ISCSI_ERR_SESS_EXISTS is the error code open-iscsi returns if the
    30  	// target is already logged in
    31  	ISCSI_ERR_SESS_EXISTS = 15
    32  
    33  	// ISCSI_ERR_NO_OBJS_FOUND is the error code open-iscsi returns if
    34  	// no records/targets/sessions/portals are found to execute operation on
    35  	ISCSI_ERR_NO_OBJS_FOUND = 21
    36  )
    37  
    38  var (
    39  	sysfsBlock = "/sys/block"
    40  
    41  	sysfsiSCSIHost    = "/sys/class/iscsi_host"
    42  	sysfsiSCSISession = "/sys/class/iscsi_session"
    43  
    44  	iscsiConfigFolder = "/etc/iscsi"
    45  )
    46  
    47  type iscsiPlan struct{}
    48  
    49  func NewiSCSIPlan() common.Plan {
    50  	return &iscsiPlan{}
    51  }
    52  
    53  func (i *iscsiPlan) AttachVolume(volumeInfo map[string]string) (storage.BlockDevice, error) {
    54  	plan, err := newiSCSIInfo(volumeInfo)
    55  	if err != nil {
    56  		return storage.BlockDevice{}, errors.Trace(err)
    57  	}
    58  	return plan.attach()
    59  }
    60  
    61  func (i *iscsiPlan) DetachVolume(volumeInfo map[string]string) error {
    62  	plan, err := newiSCSIInfo(volumeInfo)
    63  	if err != nil {
    64  		return errors.Trace(err)
    65  	}
    66  	return plan.detach()
    67  }
    68  
    69  type iscsiConnectionInfo struct {
    70  	iqn        string
    71  	address    string
    72  	port       int
    73  	chapSecret string
    74  	chapUser   string
    75  }
    76  
    77  var runCommand = func(params []string) (*exec.ExecResponse, error) {
    78  	cmd := strings.Join(params, " ")
    79  	execParams := exec.RunParams{
    80  		Commands: cmd,
    81  	}
    82  	resp, err := exec.RunCommands(execParams)
    83  
    84  	return resp, err
    85  }
    86  
    87  func getHardwareInfo(name string) (storage.BlockDevice, error) {
    88  	cmd := []string{
    89  		"udevadm", "info",
    90  		"-q", "property",
    91  		"--path", fmt.Sprintf("/block/%s", name),
    92  	}
    93  
    94  	result, err := runCommand(cmd)
    95  	if err != nil {
    96  		return storage.BlockDevice{}, errors.Annotatef(err, "error running udevadm")
    97  	}
    98  	blockDevice := storage.BlockDevice{
    99  		DeviceName: name,
   100  	}
   101  	var busId, serialId string
   102  	s := bufio.NewScanner(bytes.NewReader(result.Stdout))
   103  	for s.Scan() {
   104  		line := s.Text()
   105  		if line == "" {
   106  			continue
   107  		}
   108  		if strings.Contains(line, "") {
   109  			continue
   110  		}
   111  		fields := strings.SplitN(line, "=", 2)
   112  		if len(fields) != 2 {
   113  			logger.Tracef("failed to parse line %s", line)
   114  			continue
   115  		}
   116  
   117  		key := fields[0]
   118  		value := fields[1]
   119  		switch key {
   120  		case "ID_WWN":
   121  			blockDevice.WWN = value
   122  		case "DEVLINKS":
   123  			blockDevice.DeviceLinks = strings.Split(value, " ")
   124  		case "ID_BUS":
   125  			busId = value
   126  		case "ID_SERIAL":
   127  			serialId = value
   128  		}
   129  	}
   130  	if busId != "" && serialId != "" {
   131  		blockDevice.HardwareId = fmt.Sprintf("%s-%s", busId, serialId)
   132  	}
   133  	return blockDevice, nil
   134  }
   135  
   136  func newiSCSIInfo(info map[string]string) (*iscsiConnectionInfo, error) {
   137  	var iqn, address, user, secret, port string
   138  	var ok bool
   139  	if iqn, ok = info["iqn"]; !ok {
   140  		return nil, errors.Errorf("missing required field: iqn")
   141  	}
   142  	if address, ok = info["address"]; !ok {
   143  		return nil, errors.Errorf("missing required field: address")
   144  	}
   145  	if port, ok = info["port"]; !ok {
   146  		return nil, errors.Errorf("missing required field: port")
   147  	}
   148  
   149  	iPort, err := strconv.Atoi(port)
   150  	if err != nil {
   151  		return nil, errors.Errorf("invalid port: %v", port)
   152  	}
   153  	user, _ = info["chap-user"]
   154  	secret, _ = info["chap-secret"]
   155  	plan := &iscsiConnectionInfo{
   156  		iqn:        iqn,
   157  		address:    address,
   158  		port:       iPort,
   159  		chapSecret: secret,
   160  		chapUser:   user,
   161  	}
   162  	return plan, nil
   163  }
   164  
   165  // sessionBase returns the iSCSI sysfs session folder
   166  func (i *iscsiConnectionInfo) sessionBase(deviceName string) (string, error) {
   167  	lnkPath := filepath.Join(sysfsBlock, deviceName)
   168  	lnkRealPath, err := os.Readlink(lnkPath)
   169  	if err != nil {
   170  		return "", err
   171  	}
   172  	fullPath, err := filepath.Abs(filepath.Join(sysfsBlock, lnkRealPath))
   173  	if err != nil {
   174  		return "", err
   175  	}
   176  	segments := strings.SplitN(fullPath[1:], "/", -1)
   177  	if len(segments) != 9 {
   178  		// iscsi block devices look like:
   179  		// /sys/devices/platform/host2/session1/target2:0:0/2:0:0:1/block/sda
   180  		return "", errors.Errorf("not an iscsi device")
   181  	}
   182  	if _, err := os.Stat(filepath.Join(sysfsiSCSIHost, segments[3])); err != nil {
   183  		return "", errors.Errorf("not an iscsi device")
   184  	}
   185  	sessionPath := filepath.Join(sysfsiSCSISession, segments[4])
   186  	if _, err := os.Stat(sessionPath); err != nil {
   187  		return "", errors.Errorf("session does not exits")
   188  	}
   189  	return sessionPath, nil
   190  }
   191  
   192  func (i *iscsiConnectionInfo) deviceName() (string, error) {
   193  	items, err := os.ReadDir(sysfsBlock)
   194  	if err != nil {
   195  		return "", err
   196  	}
   197  
   198  	for _, val := range items {
   199  		sessionBase, err := i.sessionBase(val.Name())
   200  		if err != nil {
   201  			logger.Tracef("failed to get session folder for device %s: %s", val.Name(), err)
   202  			continue
   203  		}
   204  		tgtnameFile := filepath.Join(sessionBase, "targetname")
   205  		if _, err := os.Stat(tgtnameFile); err != nil {
   206  			logger.Tracef("%s was not found. Skipping", tgtnameFile)
   207  			continue
   208  		}
   209  		tgtname, err := os.ReadFile(tgtnameFile)
   210  		if err != nil {
   211  			return "", err
   212  		}
   213  		trimmed := strings.TrimSuffix(string(tgtname), "\n")
   214  		if trimmed == i.iqn {
   215  			return val.Name(), nil
   216  		}
   217  	}
   218  	return "", errors.NotFoundf("device for iqn %s not found", i.iqn)
   219  }
   220  
   221  func (i *iscsiConnectionInfo) portal() string {
   222  	return fmt.Sprintf("%s:%d", i.address, i.port)
   223  }
   224  
   225  func (i *iscsiConnectionInfo) configFile() string {
   226  	hostPortPair := fmt.Sprintf("%s,%d", i.address, i.port)
   227  	return filepath.Join(iscsiConfigFolder, i.iqn, hostPortPair)
   228  }
   229  
   230  func (i *iscsiConnectionInfo) isNodeConfigured() bool {
   231  	if _, err := os.Stat(i.configFile()); err != nil {
   232  		return false
   233  	}
   234  	return true
   235  }
   236  
   237  // addTarget adds the iscsi target config
   238  func (i *iscsiConnectionInfo) addTarget() error {
   239  	newNodeParams := []string{
   240  		"iscsiadm", "-m", "node",
   241  		"-o", "new",
   242  		"-T", i.iqn,
   243  		"-p", i.portal()}
   244  	result, err := runCommand(newNodeParams)
   245  	if err != nil {
   246  		return errors.Annotatef(err, "iscsiadm failed to add new node: %s", result.Stderr)
   247  	}
   248  
   249  	startupParams := []string{
   250  		"iscsiadm", "-m", "node",
   251  		"-o", "update",
   252  		"-T", i.iqn,
   253  		"-n", "node.startup",
   254  		"-v", "automatic"}
   255  	result, err = runCommand(startupParams)
   256  	if err != nil {
   257  		return errors.Annotatef(err, "iscsiadm failed to set startup mode: %s", result.Stderr)
   258  	}
   259  
   260  	if i.chapSecret != "" && i.chapUser != "" {
   261  		authModeParams := []string{
   262  			"iscsiadm", "-m", "node",
   263  			"-o", "update",
   264  			"-T", i.iqn,
   265  			"-p", i.portal(),
   266  			"-n", "node.session.auth.authmethod",
   267  			"-v", "CHAP",
   268  		}
   269  		result, err = runCommand(authModeParams)
   270  		if err != nil {
   271  			return errors.Annotatef(err, "iscsiadm failed to set auth method: %s", result.Stderr)
   272  		}
   273  		usernameParams := []string{
   274  			"iscsiadm", "-m", "node",
   275  			"-o", "update",
   276  			"-T", i.iqn,
   277  			"-p", i.portal(),
   278  			"-n", "node.session.auth.username",
   279  			"-v", i.chapUser,
   280  		}
   281  		result, err = runCommand(usernameParams)
   282  		if err != nil {
   283  			return errors.Annotatef(err, "iscsiadm failed to set auth username: %s", result.Stderr)
   284  		}
   285  		passwordParams := []string{
   286  			"iscsiadm", "-m", "node",
   287  			"-o", "update",
   288  			"-T", i.iqn,
   289  			"-p", i.portal(),
   290  			"-n", "node.session.auth.password",
   291  			"-v", i.chapSecret,
   292  		}
   293  		result, err = runCommand(passwordParams)
   294  		if err != nil {
   295  			return errors.Annotatef(err, "iscsiadm failed to set auth password: %s", result.Stderr)
   296  		}
   297  	}
   298  	return nil
   299  }
   300  
   301  func (i *iscsiConnectionInfo) login() error {
   302  	if i.isNodeConfigured() == false {
   303  		if err := i.addTarget(); err != nil {
   304  			return errors.Trace(err)
   305  		}
   306  	}
   307  	loginCmd := []string{
   308  		"iscsiadm", "-m", "node",
   309  		"-T", i.iqn,
   310  		"-p", i.portal(),
   311  		"-l",
   312  	}
   313  	result, err := runCommand(loginCmd)
   314  	if err != nil {
   315  		// test if error code is because we are already logged into this target
   316  		if result.Code != ISCSI_ERR_SESS_EXISTS {
   317  			return errors.Annotatef(err, "iscsiadm failed to log into target: %d", result.Code)
   318  		}
   319  	}
   320  	return nil
   321  }
   322  
   323  func (i *iscsiConnectionInfo) logout() error {
   324  	logoutCmd := []string{
   325  		"iscsiadm", "-m", "node",
   326  		"-T", i.iqn,
   327  		"-p", i.portal(),
   328  		"-u",
   329  	}
   330  	result, err := runCommand(logoutCmd)
   331  	if err != nil {
   332  		if result.Code != ISCSI_ERR_NO_OBJS_FOUND {
   333  			return errors.Annotatef(err, "iscsiadm failed to logout of target: %d", result.Code)
   334  		}
   335  	}
   336  	return nil
   337  }
   338  
   339  func (i *iscsiConnectionInfo) delete() error {
   340  	deleteNodeCmd := []string{
   341  		"iscsiadm", "-m", "node",
   342  		"-o", "delete",
   343  		"-T", i.iqn,
   344  		"-p", i.portal(),
   345  	}
   346  	result, err := runCommand(deleteNodeCmd)
   347  	if err != nil {
   348  		if result.Code != ISCSI_ERR_NO_OBJS_FOUND {
   349  			return errors.Annotatef(err, "iscsiadm failed to delete node: %d", result.Code)
   350  		}
   351  	}
   352  	return nil
   353  }
   354  
   355  func (i *iscsiConnectionInfo) attach() (storage.BlockDevice, error) {
   356  	if err := i.addTarget(); err != nil {
   357  		return storage.BlockDevice{}, errors.Trace(err)
   358  	}
   359  
   360  	if err := i.login(); err != nil {
   361  		return storage.BlockDevice{}, errors.Trace(err)
   362  	}
   363  	// Wait for device to show up
   364  	err := retry.Call(retry.CallArgs{
   365  		Func: func() error {
   366  			_, err := i.deviceName()
   367  			return err
   368  		},
   369  		Attempts: 20,
   370  		Delay:    time.Second,
   371  		Clock:    clock.WallClock,
   372  	})
   373  	if err != nil {
   374  		return storage.BlockDevice{}, errors.Trace(err)
   375  	}
   376  
   377  	devName, err := i.deviceName()
   378  	if err != nil {
   379  		return storage.BlockDevice{}, errors.Trace(err)
   380  	}
   381  	return getHardwareInfo(devName)
   382  }
   383  
   384  func (i *iscsiConnectionInfo) detach() error {
   385  	if err := i.logout(); err != nil {
   386  		return errors.Trace(err)
   387  	}
   388  	return errors.Trace(i.delete())
   389  }