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 }