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 }