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