k8s.io/kubernetes@v1.29.3/pkg/volume/iscsi/iscsi_util.go (about) 1 /* 2 Copyright 2015 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package iscsi 18 19 import ( 20 "encoding/json" 21 "errors" 22 "fmt" 23 "io/ioutil" 24 "os" 25 "path/filepath" 26 "regexp" 27 "sort" 28 "strconv" 29 "strings" 30 "time" 31 32 "k8s.io/klog/v2" 33 "k8s.io/mount-utils" 34 utilexec "k8s.io/utils/exec" 35 36 v1 "k8s.io/api/core/v1" 37 "k8s.io/kubernetes/pkg/kubelet/config" 38 "k8s.io/kubernetes/pkg/volume" 39 volumeutil "k8s.io/kubernetes/pkg/volume/util" 40 "k8s.io/kubernetes/pkg/volume/util/types" 41 ) 42 43 const ( 44 // Minimum number of paths that the volume plugin considers enough when a multipath volume is requested. 45 minMultipathCount = 2 46 47 // Minimal number of attempts to attach all paths of a multipath volumes. If at least minMultipathCount paths 48 // are available after this nr. of attempts, the volume plugin continues with mounting the volume. 49 minAttachAttempts = 2 50 51 // Total number of attempts to attach at least minMultipathCount paths. If there are less than minMultipathCount, 52 // the volume plugin tries to attach the remaining paths at least this number of times in total. After 53 // maxAttachAttempts attempts, it mounts even a single path. 54 maxAttachAttempts = 5 55 56 // How many seconds to wait for a multipath device if at least two paths are available. 57 multipathDeviceTimeout = 10 58 59 // How many seconds to wait for a device/path to appear before giving up. 60 deviceDiscoveryTimeout = 30 61 62 // 'iscsiadm' error code stating that a session is logged in 63 // See https://github.com/open-iscsi/open-iscsi/blob/7d121d12ad6ba7783308c25ffd338a9fa0cc402b/include/iscsi_err.h#L37-L38 64 iscsiadmErrorSessExists = 15 65 66 // iscsiadm exit code for "session could not be found" 67 exit_ISCSI_ERR_SESS_NOT_FOUND = 2 68 // iscsiadm exit code for "no records/targets/sessions/portals found to execute operation on." 69 exit_ISCSI_ERR_NO_OBJS_FOUND = 21 70 ) 71 72 var ( 73 chapSt = []string{ 74 "discovery.sendtargets.auth.username", 75 "discovery.sendtargets.auth.password", 76 "discovery.sendtargets.auth.username_in", 77 "discovery.sendtargets.auth.password_in"} 78 chapSess = []string{ 79 "node.session.auth.username", 80 "node.session.auth.password", 81 "node.session.auth.username_in", 82 "node.session.auth.password_in"} 83 ifaceTransportNameRe = regexp.MustCompile(`iface.transport_name = (.*)\n`) 84 ifaceRe = regexp.MustCompile(`.+/iface-([^/]+)/.+`) 85 ) 86 87 func updateISCSIDiscoverydb(b iscsiDiskMounter, tp string) error { 88 if !b.chapDiscovery { 89 return nil 90 } 91 out, err := execWithLog(b, "iscsiadm", "-m", "discoverydb", "-t", "sendtargets", "-p", tp, "-I", b.Iface, "-o", "update", "-n", "discovery.sendtargets.auth.authmethod", "-v", "CHAP") 92 if err != nil { 93 return fmt.Errorf("iscsi: failed to update discoverydb with CHAP, output: %v", out) 94 } 95 96 for _, k := range chapSt { 97 v := b.secret[k] 98 if len(v) > 0 { 99 // explicitly not using execWithLog so secrets are not logged 100 out, err := b.exec.Command("iscsiadm", "-m", "discoverydb", "-t", "sendtargets", "-p", tp, "-I", b.Iface, "-o", "update", "-n", k, "-v", v).CombinedOutput() 101 if err != nil { 102 return fmt.Errorf("iscsi: failed to update discoverydb key %q error: %v", k, string(out)) 103 } 104 } 105 } 106 return nil 107 } 108 109 func updateISCSINode(b iscsiDiskMounter, tp string) error { 110 // setting node.session.scan to manual to handle https://github.com/kubernetes/kubernetes/issues/90982 111 out, err := execWithLog(b, "iscsiadm", "-m", "node", "-p", tp, "-T", b.Iqn, "-I", b.Iface, "-o", "update", "-n", "node.session.scan", "-v", "manual") 112 if err != nil { 113 // don't fail if iscsiadm fails or the version does not support node.session.scan - log a warning to highlight the potential exposure 114 klog.Warningf("iscsi: failed to update node with node.session.scan=manual, possible exposure to issue 90982: %v", out) 115 } 116 117 if !b.chapSession { 118 return nil 119 } 120 121 out, err = execWithLog(b, "iscsiadm", "-m", "node", "-p", tp, "-T", b.Iqn, "-I", b.Iface, "-o", "update", "-n", "node.session.auth.authmethod", "-v", "CHAP") 122 if err != nil { 123 return fmt.Errorf("iscsi: failed to update node with CHAP, output: %v", out) 124 } 125 126 for _, k := range chapSess { 127 v := b.secret[k] 128 if len(v) > 0 { 129 // explicitly not using execWithLog so secrets are not logged 130 out, err := b.exec.Command("iscsiadm", "-m", "node", "-p", tp, "-T", b.Iqn, "-I", b.Iface, "-o", "update", "-n", k, "-v", v).CombinedOutput() 131 if err != nil { 132 return fmt.Errorf("iscsi: failed to update node session key %q error: %v", k, string(out)) 133 } 134 } 135 } 136 return nil 137 } 138 139 // stat a path, if not exists, retry maxRetries times 140 // when iscsi transports other than default are used, use glob instead as pci id of device is unknown 141 type StatFunc func(string) (os.FileInfo, error) 142 type GlobFunc func(string) ([]string, error) 143 144 func waitForPathToExist(devicePath *string, maxRetries int, deviceTransport string) bool { 145 // This makes unit testing a lot easier 146 return waitForPathToExistInternal(devicePath, maxRetries, deviceTransport, os.Stat, filepath.Glob) 147 } 148 149 func waitForPathToExistInternal(devicePath *string, maxRetries int, deviceTransport string, osStat StatFunc, filepathGlob GlobFunc) bool { 150 if devicePath == nil { 151 return false 152 } 153 154 for i := 0; i < maxRetries; i++ { 155 var err error 156 if deviceTransport == "tcp" { 157 _, err = osStat(*devicePath) 158 } else { 159 fpath, _ := filepathGlob(*devicePath) 160 if fpath == nil { 161 err = os.ErrNotExist 162 } else { 163 // There might be a case that fpath contains multiple device paths if 164 // multiple PCI devices connect to same iscsi target. We handle this 165 // case at subsequent logic. Pick up only first path here. 166 *devicePath = fpath[0] 167 } 168 } 169 if err == nil { 170 return true 171 } 172 if !os.IsNotExist(err) { 173 return false 174 } 175 if i == maxRetries-1 { 176 break 177 } 178 time.Sleep(time.Second) 179 } 180 return false 181 } 182 183 // make a directory like /var/lib/kubelet/plugins/kubernetes.io/iscsi/iface_name/portal-some_iqn-lun-lun_id 184 func makePDNameInternal(host volume.VolumeHost, portal string, iqn string, lun string, iface string) string { 185 return filepath.Join(host.GetPluginDir(iscsiPluginName), "iface-"+iface, portal+"-"+iqn+"-lun-"+lun) 186 } 187 188 // make a directory like /var/lib/kubelet/plugins/kubernetes.io/iscsi/volumeDevices/iface_name/portal-some_iqn-lun-lun_id 189 func makeVDPDNameInternal(host volume.VolumeHost, portal string, iqn string, lun string, iface string) string { 190 return filepath.Join(host.GetVolumeDevicePluginDir(iscsiPluginName), "iface-"+iface, portal+"-"+iqn+"-lun-"+lun) 191 } 192 193 type ISCSIUtil struct{} 194 195 // MakeGlobalPDName returns path of global plugin dir 196 func (util *ISCSIUtil) MakeGlobalPDName(iscsi iscsiDisk) string { 197 return makePDNameInternal(iscsi.plugin.host, iscsi.Portals[0], iscsi.Iqn, iscsi.Lun, iscsi.Iface) 198 } 199 200 // MakeGlobalVDPDName returns path of global volume device plugin dir 201 func (util *ISCSIUtil) MakeGlobalVDPDName(iscsi iscsiDisk) string { 202 return makeVDPDNameInternal(iscsi.plugin.host, iscsi.Portals[0], iscsi.Iqn, iscsi.Lun, iscsi.Iface) 203 } 204 205 // persistISCSIFile saves iSCSI volume configuration for DetachDisk 206 // into given directory. 207 func (util *ISCSIUtil) persistISCSIFile(conf iscsiDisk, mnt string) error { 208 file := filepath.Join(mnt, "iscsi.json") 209 fp, err := os.Create(file) 210 if err != nil { 211 return fmt.Errorf("iscsi: create %s err %s", file, err) 212 } 213 defer fp.Close() 214 encoder := json.NewEncoder(fp) 215 if err = encoder.Encode(conf); err != nil { 216 return fmt.Errorf("iscsi: encode err: %v", err) 217 } 218 return nil 219 } 220 221 func (util *ISCSIUtil) loadISCSI(conf *iscsiDisk, mnt string) error { 222 file := filepath.Join(mnt, "iscsi.json") 223 fp, err := os.Open(file) 224 if err != nil { 225 return fmt.Errorf("iscsi: open %s err %s", file, err) 226 } 227 defer fp.Close() 228 decoder := json.NewDecoder(fp) 229 if err = decoder.Decode(conf); err != nil { 230 return fmt.Errorf("iscsi: decode err: %v", err) 231 } 232 return nil 233 } 234 235 // scanOneLun scans a single LUN on one SCSI bus 236 // Use this to avoid scanning the whole SCSI bus for all of the LUNs, which 237 // would result in the kernel on this node discovering LUNs that it shouldn't 238 // know about. Extraneous LUNs cause problems because they may get deleted 239 // without us getting notified, since we were never supposed to know about 240 // them. When LUNs are deleted without proper cleanup in the kernel, I/O errors 241 // and timeouts result, which can noticeably degrade performance of future 242 // operations. 243 func scanOneLun(hostNumber int, lunNumber int) error { 244 filename := fmt.Sprintf("/sys/class/scsi_host/host%d/scan", hostNumber) 245 fd, err := os.OpenFile(filename, os.O_WRONLY, 0) 246 if err != nil { 247 return err 248 } 249 defer fd.Close() 250 251 // Channel/Target are always 0 for iSCSI 252 scanCmd := fmt.Sprintf("0 0 %d", lunNumber) 253 if written, err := fd.WriteString(scanCmd); err != nil { 254 return err 255 } else if 0 == written { 256 return fmt.Errorf("no data written to file: %s", filename) 257 } 258 259 klog.V(3).Infof("Scanned SCSI host %d LUN %d", hostNumber, lunNumber) 260 return nil 261 } 262 263 func waitForMultiPathToExist(devicePaths []string, maxRetries int, deviceUtil volumeutil.DeviceUtil) string { 264 if 0 == len(devicePaths) { 265 return "" 266 } 267 268 for i := 0; i < maxRetries; i++ { 269 for _, path := range devicePaths { 270 // There shouldn't be any empty device paths. However adding this check 271 // for safer side to avoid the possibility of an empty entry. 272 if path == "" { 273 continue 274 } 275 // check if the dev is using mpio and if so mount it via the dm-XX device 276 if mappedDevicePath := deviceUtil.FindMultipathDeviceForDevice(path); mappedDevicePath != "" { 277 return mappedDevicePath 278 } 279 } 280 if i == maxRetries-1 { 281 break 282 } 283 time.Sleep(time.Second) 284 } 285 return "" 286 } 287 288 // AttachDisk returns devicePath of volume if attach succeeded otherwise returns error 289 func (util *ISCSIUtil) AttachDisk(b iscsiDiskMounter) (string, error) { 290 var devicePath string 291 devicePaths := map[string]string{} 292 var iscsiTransport string 293 var lastErr error 294 295 out, err := execWithLog(b, "iscsiadm", "-m", "iface", "-I", b.InitIface, "-o", "show") 296 if err != nil { 297 klog.Errorf("iscsi: could not read iface %s error: %s", b.InitIface, out) 298 return "", err 299 } 300 301 iscsiTransport = extractTransportname(out) 302 303 bkpPortal := b.Portals 304 305 // If the initiator name was set, the iface isn't created yet, 306 // so create it and copy parameters from the pre-configured one 307 if b.InitiatorName != "" { 308 if err = cloneIface(b); err != nil { 309 klog.Errorf("iscsi: failed to clone iface: %s error: %v", b.InitIface, err) 310 return "", err 311 } 312 } 313 314 // Lock the target while we login to avoid races between 2 volumes that share the same 315 // target both logging in or one logging out while another logs in. 316 b.plugin.targetLocks.LockKey(b.Iqn) 317 defer b.plugin.targetLocks.UnlockKey(b.Iqn) 318 319 // Build a map of SCSI hosts for each target portal. We will need this to 320 // issue the bus rescans. 321 portalHostMap, err := b.deviceUtil.GetISCSIPortalHostMapForTarget(b.Iqn) 322 if err != nil { 323 return "", err 324 } 325 klog.V(4).Infof("AttachDisk portal->host map for %s is %v", b.Iqn, portalHostMap) 326 327 for i := 1; i <= maxAttachAttempts; i++ { 328 for _, tp := range bkpPortal { 329 if _, found := devicePaths[tp]; found { 330 klog.V(4).Infof("Device for portal %q already known", tp) 331 continue 332 } 333 334 hostNumber, loggedIn := portalHostMap[tp] 335 if !loggedIn { 336 klog.V(4).Infof("Could not get SCSI host number for portal %s, will attempt login", tp) 337 338 // build discoverydb and discover iscsi target 339 execWithLog(b, "iscsiadm", "-m", "discoverydb", "-t", "sendtargets", "-p", tp, "-I", b.Iface, "-o", "new") 340 341 // update discoverydb with CHAP secret 342 err = updateISCSIDiscoverydb(b, tp) 343 if err != nil { 344 lastErr = fmt.Errorf("iscsi: failed to update discoverydb to portal %s error: %v", tp, err) 345 continue 346 } 347 348 out, err = execWithLog(b, "iscsiadm", "-m", "discoverydb", "-t", "sendtargets", "-p", tp, "-I", b.Iface, "--discover") 349 if err != nil { 350 // delete discoverydb record 351 execWithLog(b, "iscsiadm", "-m", "discoverydb", "-t", "sendtargets", "-p", tp, "-I", b.Iface, "-o", "delete") 352 lastErr = fmt.Errorf("iscsi: failed to sendtargets to portal %s output: %s, err %v", tp, out, err) 353 continue 354 } 355 356 err = updateISCSINode(b, tp) 357 if err != nil { 358 // failure to update node db is rare. But deleting record will likely impact those who already start using it. 359 lastErr = fmt.Errorf("iscsi: failed to update iscsi node to portal %s error: %v", tp, err) 360 continue 361 } 362 363 // login to iscsi target 364 out, err = execWithLog(b, "iscsiadm", "-m", "node", "-p", tp, "-T", b.Iqn, "-I", b.Iface, "--login") 365 if err != nil { 366 // delete the node record from database 367 execWithLog(b, "iscsiadm", "-m", "node", "-p", tp, "-I", b.Iface, "-T", b.Iqn, "-o", "delete") 368 lastErr = fmt.Errorf("iscsi: failed to attach disk: Error: %s (%v)", out, err) 369 continue 370 } 371 372 // in case of node failure/restart, explicitly set to manual login so it doesn't hang on boot 373 _, err = execWithLog(b, "iscsiadm", "-m", "node", "-p", tp, "-T", b.Iqn, "-o", "update", "-n", "node.startup", "-v", "manual") 374 if err != nil { 375 // don't fail if we can't set startup mode, but log warning so there is a clue 376 klog.Warningf("Warning: Failed to set iSCSI login mode to manual. Error: %v", err) 377 } 378 379 // Rebuild the host map after logging in 380 portalHostMap, err := b.deviceUtil.GetISCSIPortalHostMapForTarget(b.Iqn) 381 if err != nil { 382 return "", err 383 } 384 klog.V(6).Infof("AttachDisk portal->host map for %s is %v", b.Iqn, portalHostMap) 385 386 hostNumber, loggedIn = portalHostMap[tp] 387 if !loggedIn { 388 klog.Warningf("Could not get SCSI host number for portal %s after logging in", tp) 389 continue 390 } 391 } 392 393 klog.V(5).Infof("AttachDisk: scanning SCSI host %d LUN %s", hostNumber, b.Lun) 394 lunNumber, err := strconv.Atoi(b.Lun) 395 if err != nil { 396 return "", fmt.Errorf("AttachDisk: lun is not a number: %s\nError: %v", b.Lun, err) 397 } 398 399 // Scan the iSCSI bus for the LUN 400 err = scanOneLun(hostNumber, lunNumber) 401 if err != nil { 402 return "", err 403 } 404 405 if iscsiTransport == "" { 406 klog.Errorf("iscsi: could not find transport name in iface %s", b.Iface) 407 return "", fmt.Errorf("could not parse iface file for %s", b.Iface) 408 } 409 410 addr := tp 411 if strings.HasPrefix(tp, "[") { 412 // Delete [] from IP address, links in /dev/disk/by-path do not have it. 413 addr = strings.NewReplacer("[", "", "]", "").Replace(tp) 414 } 415 if iscsiTransport == "tcp" { 416 devicePath = strings.Join([]string{"/dev/disk/by-path/ip", addr, "iscsi", b.Iqn, "lun", b.Lun}, "-") 417 } else { 418 devicePath = strings.Join([]string{"/dev/disk/by-path/pci", "*", "ip", addr, "iscsi", b.Iqn, "lun", b.Lun}, "-") 419 } 420 421 if exist := waitForPathToExist(&devicePath, deviceDiscoveryTimeout, iscsiTransport); !exist { 422 msg := fmt.Sprintf("Timed out waiting for device at path %s after %ds", devicePath, deviceDiscoveryTimeout) 423 klog.Error(msg) 424 // update last error 425 lastErr = errors.New(msg) 426 continue 427 } else { 428 devicePaths[tp] = devicePath 429 } 430 } 431 klog.V(4).Infof("iscsi: tried all devices for %q %d times, %d paths found", b.Iqn, i, len(devicePaths)) 432 if len(devicePaths) == 0 { 433 // No path attached, report error and stop trying. kubelet will try again in a short while 434 // delete cloned iface 435 execWithLog(b, "iscsiadm", "-m", "iface", "-I", b.Iface, "-o", "delete") 436 klog.Errorf("iscsi: failed to get any path for iscsi disk, last err seen:\n%v", lastErr) 437 return "", fmt.Errorf("failed to get any path for iscsi disk, last err seen:\n%v", lastErr) 438 } 439 if len(devicePaths) == len(bkpPortal) { 440 // We have all paths 441 klog.V(4).Infof("iscsi: all devices for %q found", b.Iqn) 442 break 443 } 444 if len(devicePaths) >= minMultipathCount && i >= minAttachAttempts { 445 // We have at least two paths for multipath and we tried the other paths long enough 446 klog.V(4).Infof("%d devices found for %q", len(devicePaths), b.Iqn) 447 break 448 } 449 } 450 451 if lastErr != nil { 452 klog.Errorf("iscsi: last error occurred during iscsi init:\n%v", lastErr) 453 } 454 455 devicePathList := []string{} 456 for _, path := range devicePaths { 457 devicePathList = append(devicePathList, path) 458 } 459 // Try to find a multipath device for the volume 460 if len(bkpPortal) > 1 { 461 // Multipath volume was requested. Wait up to multipathDeviceTimeout seconds for the multipath device to appear. 462 devicePath = waitForMultiPathToExist(devicePathList, multipathDeviceTimeout, b.deviceUtil) 463 } else { 464 // For PVs with 1 portal, just try one time to find the multipath device. This 465 // avoids a long pause when the multipath device will never get created, and 466 // matches legacy behavior. 467 devicePath = waitForMultiPathToExist(devicePathList, 1, b.deviceUtil) 468 } 469 470 // When no multipath device is found, just use the first (and presumably only) device 471 if devicePath == "" { 472 devicePath = devicePathList[0] 473 } 474 475 klog.V(5).Infof("iscsi: AttachDisk devicePath: %s", devicePath) 476 477 if err = util.persistISCSI(b); err != nil { 478 // Return uncertain error so kubelet calls Unmount / Unmap when the pod 479 // is deleted. 480 return "", types.NewUncertainProgressError(err.Error()) 481 } 482 return devicePath, nil 483 } 484 485 // persistISCSI saves iSCSI volume configuration for DetachDisk into global 486 // mount / map directory. 487 func (util *ISCSIUtil) persistISCSI(b iscsiDiskMounter) error { 488 klog.V(5).Infof("iscsi: AttachDisk volumeMode: %s", b.volumeMode) 489 var globalPDPath string 490 if b.volumeMode == v1.PersistentVolumeBlock { 491 globalPDPath = b.manager.MakeGlobalVDPDName(*b.iscsiDisk) 492 } else { 493 globalPDPath = b.manager.MakeGlobalPDName(*b.iscsiDisk) 494 } 495 496 if err := os.MkdirAll(globalPDPath, 0750); err != nil { 497 klog.Errorf("iscsi: failed to mkdir %s, error", globalPDPath) 498 return err 499 } 500 501 if b.volumeMode == v1.PersistentVolumeFilesystem { 502 notMnt, err := b.mounter.IsLikelyNotMountPoint(globalPDPath) 503 if err != nil { 504 return err 505 } 506 if !notMnt { 507 // The volume is already mounted, therefore the previous WaitForAttach must have 508 // persisted the volume metadata. In addition, the metadata is actually *inside* 509 // globalPDPath and we can't write it here, because it was shadowed by the volume 510 // mount. 511 klog.V(4).Infof("Skipping persistISCSI, the volume is already mounted at %s", globalPDPath) 512 return nil 513 } 514 } 515 516 // Persist iscsi disk config to json file for DetachDisk path 517 return util.persistISCSIFile(*(b.iscsiDisk), globalPDPath) 518 } 519 520 // Delete 1 block device of the form "sd*" 521 func deleteDevice(deviceName string) error { 522 filename := fmt.Sprintf("/sys/block/%s/device/delete", deviceName) 523 fd, err := os.OpenFile(filename, os.O_WRONLY, 0) 524 if err != nil { 525 // The file was not present, so just return without error 526 return nil 527 } 528 defer fd.Close() 529 530 if written, err := fd.WriteString("1"); err != nil { 531 return err 532 } else if 0 == written { 533 return fmt.Errorf("no data written to file: %s", filename) 534 } 535 klog.V(4).Infof("Deleted block device: %s", deviceName) 536 return nil 537 } 538 539 // deleteDevices tries to remove all the block devices and multipath map devices 540 // associated with a given iscsi device 541 func deleteDevices(c iscsiDiskUnmounter) error { 542 lunNumber, err := strconv.Atoi(c.iscsiDisk.Lun) 543 if err != nil { 544 klog.Errorf("iscsi delete devices: lun is not a number: %s\nError: %v", c.iscsiDisk.Lun, err) 545 return err 546 } 547 // Enumerate the devices so we can delete them 548 deviceNames, err := c.deviceUtil.FindDevicesForISCSILun(c.iscsiDisk.Iqn, lunNumber) 549 if err != nil { 550 klog.Errorf("iscsi delete devices: could not get devices associated with LUN %d on target %s\nError: %v", 551 lunNumber, c.iscsiDisk.Iqn, err) 552 return err 553 } 554 // Find the multipath device path(s) 555 mpathDevices := make(map[string]bool) 556 for _, deviceName := range deviceNames { 557 path := "/dev/" + deviceName 558 // check if the dev is using mpio and if so mount it via the dm-XX device 559 if mappedDevicePath := c.deviceUtil.FindMultipathDeviceForDevice(path); mappedDevicePath != "" { 560 mpathDevices[mappedDevicePath] = true 561 } 562 } 563 // Flush any multipath device maps 564 for mpathDevice := range mpathDevices { 565 _, err = c.exec.Command("multipath", "-f", mpathDevice).CombinedOutput() 566 if err != nil { 567 klog.Warningf("Warning: Failed to flush multipath device map: %s\nError: %v", mpathDevice, err) 568 // Fall through -- keep deleting the block devices 569 } 570 klog.V(4).Infof("Flushed multipath device: %s", mpathDevice) 571 } 572 for _, deviceName := range deviceNames { 573 err = deleteDevice(deviceName) 574 if err != nil { 575 klog.Warningf("Warning: Failed to delete block device: %s\nError: %v", deviceName, err) 576 // Fall through -- keep deleting other block devices 577 } 578 } 579 return nil 580 } 581 582 // DetachDisk unmounts and detaches a volume from node 583 func (util *ISCSIUtil) DetachDisk(c iscsiDiskUnmounter, mntPath string) error { 584 if pathExists, pathErr := mount.PathExists(mntPath); pathErr != nil { 585 return fmt.Errorf("error checking if path exists: %w", pathErr) 586 } else if !pathExists { 587 klog.Warningf("Warning: Unmount skipped because path does not exist: %v", mntPath) 588 return nil 589 } 590 591 notMnt, err := c.mounter.IsLikelyNotMountPoint(mntPath) 592 if err != nil { 593 return err 594 } 595 if !notMnt { 596 if err := c.mounter.Unmount(mntPath); err != nil { 597 klog.Errorf("iscsi detach disk: failed to unmount: %s\nError: %v", mntPath, err) 598 return err 599 } 600 } 601 602 // if device is no longer used, see if need to logout the target 603 device, _, err := extractDeviceAndPrefix(mntPath) 604 if err != nil { 605 return err 606 } 607 608 var bkpPortal []string 609 var volName, iqn, iface, initiatorName string 610 found := true 611 612 // load iscsi disk config from json file 613 if err := util.loadISCSI(c.iscsiDisk, mntPath); err == nil { 614 bkpPortal, iqn, iface, volName = c.iscsiDisk.Portals, c.iscsiDisk.Iqn, c.iscsiDisk.Iface, c.iscsiDisk.VolName 615 initiatorName = c.iscsiDisk.InitiatorName 616 } else { 617 // If the iscsi disk config is not found, fall back to the original behavior. 618 // This portal/iqn/iface is no longer referenced, log out. 619 // Extract the portal and iqn from device path. 620 bkpPortal = make([]string, 1) 621 bkpPortal[0], iqn, err = extractPortalAndIqn(device) 622 if err != nil { 623 return err 624 } 625 // Extract the iface from the mountPath and use it to log out. If the iface 626 // is not found, maintain the previous behavior to facilitate kubelet upgrade. 627 // Logout may fail as no session may exist for the portal/IQN on the specified interface. 628 iface, found = extractIface(mntPath) 629 } 630 631 // Delete all the scsi devices and any multipath devices after unmounting 632 if err = deleteDevices(c); err != nil { 633 klog.Warningf("iscsi detach disk: failed to delete devices\nError: %v", err) 634 // Fall through -- even if deleting fails, a logout may fix problems 635 } 636 637 // Lock the target while we determine if we can safely log out or not 638 c.plugin.targetLocks.LockKey(iqn) 639 defer c.plugin.targetLocks.UnlockKey(iqn) 640 641 portals := removeDuplicate(bkpPortal) 642 if len(portals) == 0 { 643 return fmt.Errorf("iscsi detach disk: failed to detach iscsi disk. Couldn't get connected portals from configurations") 644 } 645 646 // If device is no longer used, see if need to logout the target 647 if isSessionBusy(c.iscsiDisk.plugin.host, portals[0], iqn) { 648 return nil 649 } 650 651 err = util.detachISCSIDisk(c.exec, portals, iqn, iface, volName, initiatorName, found) 652 if err != nil { 653 return fmt.Errorf("failed to finish detachISCSIDisk, err: %v", err) 654 } 655 return nil 656 } 657 658 // DetachBlockISCSIDisk removes loopback device for a volume and detaches a volume from node 659 func (util *ISCSIUtil) DetachBlockISCSIDisk(c iscsiDiskUnmapper, mapPath string) error { 660 if pathExists, pathErr := mount.PathExists(mapPath); pathErr != nil { 661 return fmt.Errorf("error checking if path exists: %w", pathErr) 662 } else if !pathExists { 663 klog.Warningf("Warning: Unmap skipped because path does not exist: %v", mapPath) 664 return nil 665 } 666 // If we arrive here, device is no longer used, see if need to logout the target 667 // device: 192.168.0.10:3260-iqn.2017-05.com.example:test-lun-0 668 device, _, err := extractDeviceAndPrefix(mapPath) 669 if err != nil { 670 return err 671 } 672 var bkpPortal []string 673 var volName, iqn, lun, iface, initiatorName string 674 found := true 675 // load iscsi disk config from json file 676 if err := util.loadISCSI(c.iscsiDisk, mapPath); err == nil { 677 bkpPortal, iqn, lun, iface, volName = c.iscsiDisk.Portals, c.iscsiDisk.Iqn, c.iscsiDisk.Lun, c.iscsiDisk.Iface, c.iscsiDisk.VolName 678 initiatorName = c.iscsiDisk.InitiatorName 679 } else { 680 // If the iscsi disk config is not found, fall back to the original behavior. 681 // This portal/iqn/iface is no longer referenced, log out. 682 // Extract the portal and iqn from device path. 683 bkpPortal = make([]string, 1) 684 bkpPortal[0], iqn, err = extractPortalAndIqn(device) 685 if err != nil { 686 return err 687 } 688 arr := strings.Split(device, "-lun-") 689 if len(arr) < 2 { 690 return fmt.Errorf("failed to retrieve lun from mapPath: %v", mapPath) 691 } 692 lun = arr[1] 693 // Extract the iface from the mountPath and use it to log out. If the iface 694 // is not found, maintain the previous behavior to facilitate kubelet upgrade. 695 // Logout may fail as no session may exist for the portal/IQN on the specified interface. 696 iface, found = extractIface(mapPath) 697 } 698 portals := removeDuplicate(bkpPortal) 699 if len(portals) == 0 { 700 return fmt.Errorf("iscsi detach disk: failed to detach iscsi disk. Couldn't get connected portals from configurations") 701 } 702 703 devicePath := getDevByPath(portals[0], iqn, lun) 704 klog.V(5).Infof("iscsi: devicePath: %s", devicePath) 705 if _, err = os.Stat(devicePath); err != nil { 706 return fmt.Errorf("failed to validate devicePath: %s", devicePath) 707 } 708 709 // Lock the target while we determine if we can safely log out or not 710 c.plugin.targetLocks.LockKey(iqn) 711 defer c.plugin.targetLocks.UnlockKey(iqn) 712 713 // If device is no longer used, see if need to logout the target 714 if isSessionBusy(c.iscsiDisk.plugin.host, portals[0], iqn) { 715 return nil 716 } 717 718 // Detach a volume from kubelet node 719 err = util.detachISCSIDisk(c.exec, portals, iqn, iface, volName, initiatorName, found) 720 if err != nil { 721 return fmt.Errorf("failed to finish detachISCSIDisk, err: %v", err) 722 } 723 return nil 724 } 725 726 func (util *ISCSIUtil) detachISCSIDisk(exec utilexec.Interface, portals []string, iqn, iface, volName, initiatorName string, found bool) error { 727 for _, portal := range portals { 728 logoutArgs := []string{"-m", "node", "-p", portal, "-T", iqn, "--logout"} 729 deleteArgs := []string{"-m", "node", "-p", portal, "-T", iqn, "-o", "delete"} 730 if found { 731 logoutArgs = append(logoutArgs, []string{"-I", iface}...) 732 deleteArgs = append(deleteArgs, []string{"-I", iface}...) 733 } 734 klog.Infof("iscsi: log out target %s iqn %s iface %s", portal, iqn, iface) 735 out, err := exec.Command("iscsiadm", logoutArgs...).CombinedOutput() 736 err = ignoreExitCodes(err, exit_ISCSI_ERR_NO_OBJS_FOUND, exit_ISCSI_ERR_SESS_NOT_FOUND) 737 if err != nil { 738 klog.Errorf("iscsi: failed to detach disk Error: %s", string(out)) 739 return err 740 } 741 // Delete the node record 742 klog.Infof("iscsi: delete node record target %s iqn %s", portal, iqn) 743 out, err = exec.Command("iscsiadm", deleteArgs...).CombinedOutput() 744 err = ignoreExitCodes(err, exit_ISCSI_ERR_NO_OBJS_FOUND, exit_ISCSI_ERR_SESS_NOT_FOUND) 745 if err != nil { 746 klog.Errorf("iscsi: failed to delete node record Error: %s", string(out)) 747 return err 748 } 749 } 750 // Delete the iface after all sessions have logged out 751 // If the iface is not created via iscsi plugin, skip to delete 752 if initiatorName != "" && found && iface == (portals[0]+":"+volName) { 753 deleteArgs := []string{"-m", "iface", "-I", iface, "-o", "delete"} 754 out, err := exec.Command("iscsiadm", deleteArgs...).CombinedOutput() 755 err = ignoreExitCodes(err, exit_ISCSI_ERR_NO_OBJS_FOUND, exit_ISCSI_ERR_SESS_NOT_FOUND) 756 if err != nil { 757 klog.Errorf("iscsi: failed to delete iface Error: %s", string(out)) 758 return err 759 } 760 } 761 762 return nil 763 } 764 765 func getDevByPath(portal, iqn, lun string) string { 766 return "/dev/disk/by-path/ip-" + portal + "-iscsi-" + iqn + "-lun-" + lun 767 } 768 769 func extractTransportname(ifaceOutput string) (iscsiTransport string) { 770 rexOutput := ifaceTransportNameRe.FindStringSubmatch(ifaceOutput) 771 if rexOutput == nil { 772 return "" 773 } 774 iscsiTransport = rexOutput[1] 775 776 // While iface.transport_name is a required parameter, handle it being unspecified anyways 777 if iscsiTransport == "<empty>" { 778 iscsiTransport = "tcp" 779 } 780 return iscsiTransport 781 } 782 783 func extractDeviceAndPrefix(mntPath string) (string, string, error) { 784 ind := strings.LastIndex(mntPath, "/") 785 if ind < 0 { 786 return "", "", fmt.Errorf("iscsi detach disk: malformatted mnt path: %s", mntPath) 787 } 788 device := mntPath[(ind + 1):] 789 // strip -lun- from mount path 790 ind = strings.LastIndex(mntPath, "-lun-") 791 if ind < 0 { 792 return "", "", fmt.Errorf("iscsi detach disk: malformatted mnt path: %s", mntPath) 793 } 794 prefix := mntPath[:ind] 795 return device, prefix, nil 796 } 797 798 func extractIface(mntPath string) (string, bool) { 799 reOutput := ifaceRe.FindStringSubmatch(mntPath) 800 if len(reOutput) > 1 { 801 return reOutput[1], true 802 } 803 804 return "", false 805 } 806 807 func extractPortalAndIqn(device string) (string, string, error) { 808 ind1 := strings.Index(device, "-") 809 if ind1 < 0 { 810 return "", "", fmt.Errorf("iscsi detach disk: no portal in %s", device) 811 } 812 portal := device[0:ind1] 813 ind2 := strings.Index(device, "iqn.") 814 if ind2 < 0 { 815 ind2 = strings.Index(device, "eui.") 816 } 817 if ind2 < 0 { 818 return "", "", fmt.Errorf("iscsi detach disk: no iqn in %s", device) 819 } 820 ind := strings.LastIndex(device, "-lun-") 821 iqn := device[ind2:ind] 822 return portal, iqn, nil 823 } 824 825 // Remove duplicates or string 826 func removeDuplicate(s []string) []string { 827 m := map[string]bool{} 828 for _, v := range s { 829 if v != "" && !m[v] { 830 s[len(m)] = v 831 m[v] = true 832 } 833 } 834 s = s[:len(m)] 835 return s 836 } 837 838 func parseIscsiadmShow(output string) (map[string]string, error) { 839 params := make(map[string]string) 840 slice := strings.Split(output, "\n") 841 for _, line := range slice { 842 if !strings.HasPrefix(line, "iface.") || strings.Contains(line, "<empty>") { 843 continue 844 } 845 iface := strings.Fields(line) 846 if len(iface) != 3 || iface[1] != "=" { 847 return nil, fmt.Errorf("error: invalid iface setting: %v", iface) 848 } 849 // iscsi_ifacename is immutable once the iface is created 850 if iface[0] == "iface.iscsi_ifacename" { 851 continue 852 } 853 params[iface[0]] = iface[2] 854 } 855 return params, nil 856 } 857 858 func cloneIface(b iscsiDiskMounter) error { 859 var lastErr error 860 if b.InitIface == b.Iface { 861 return fmt.Errorf("iscsi: cannot clone iface with same name: %s", b.InitIface) 862 } 863 // get pre-configured iface records 864 out, err := execWithLog(b, "iscsiadm", "-m", "iface", "-I", b.InitIface, "-o", "show") 865 if err != nil { 866 lastErr = fmt.Errorf("iscsi: failed to show iface records: %s (%v)", out, err) 867 return lastErr 868 } 869 // parse obtained records 870 params, err := parseIscsiadmShow(out) 871 if err != nil { 872 lastErr = fmt.Errorf("iscsi: failed to parse iface records: %s (%v)", out, err) 873 return lastErr 874 } 875 // update initiatorname 876 params["iface.initiatorname"] = b.InitiatorName 877 // create new iface 878 out, err = execWithLog(b, "iscsiadm", "-m", "iface", "-I", b.Iface, "-o", "new") 879 if err != nil { 880 exit, ok := err.(utilexec.ExitError) 881 if ok && exit.ExitStatus() == iscsiadmErrorSessExists { 882 klog.Infof("iscsi: there is a session already logged in with iface %s", b.Iface) 883 } else { 884 lastErr = fmt.Errorf("iscsi: failed to create new iface: %s (%v)", out, err) 885 return lastErr 886 } 887 } 888 // Get and sort keys to maintain a stable iteration order 889 var keys []string 890 for k := range params { 891 keys = append(keys, k) 892 } 893 sort.Strings(keys) 894 // update new iface records 895 for _, key := range keys { 896 _, err = execWithLog(b, "iscsiadm", "-m", "iface", "-I", b.Iface, "-o", "update", "-n", key, "-v", params[key]) 897 if err != nil { 898 execWithLog(b, "iscsiadm", "-m", "iface", "-I", b.Iface, "-o", "delete") 899 lastErr = fmt.Errorf("iscsi: failed to update iface records: %s (%v). iface(%s) will be used", out, err, b.InitIface) 900 break 901 } 902 } 903 return lastErr 904 } 905 906 // isSessionBusy determines if the iSCSI session is busy by counting both FS and block volumes in use. 907 func isSessionBusy(host volume.VolumeHost, portal, iqn string) bool { 908 fsDir := host.GetPluginDir(iscsiPluginName) 909 countFS, err := getVolCount(fsDir, portal, iqn) 910 if err != nil { 911 klog.Errorf("iscsi: could not determine FS volumes in use: %v", err) 912 return true 913 } 914 915 blockDir := host.GetVolumeDevicePluginDir(iscsiPluginName) 916 countBlock, err := getVolCount(blockDir, portal, iqn) 917 if err != nil { 918 klog.Errorf("iscsi: could not determine block volumes in use: %v", err) 919 return true 920 } 921 922 return countFS+countBlock > 1 923 } 924 925 // getVolCount returns the number of volumes in use by the kubelet. 926 // It does so by counting the number of directories prefixed by the given portal and IQN. 927 func getVolCount(dir, portal, iqn string) (int, error) { 928 // For FileSystem volumes, the topmost dirs are named after the ifaces, e.g., iface-default or iface-127.0.0.1:3260:pv0. 929 // For Block volumes, the default topmost dir is volumeDevices. 930 contents, err := ioutil.ReadDir(dir) 931 if err != nil { 932 if os.IsNotExist(err) { 933 return 0, nil 934 } 935 return 0, err 936 } 937 938 // Inside each iface dir, we look for volume dirs prefixed by the given 939 // portal + iqn, e.g., 127.0.0.1:3260-iqn.2003-01.io.k8s:e2e.volume-1-lun-2 940 var counter int 941 for _, c := range contents { 942 if !c.IsDir() || c.Name() == config.DefaultKubeletVolumeDevicesDirName { 943 continue 944 } 945 946 mounts, err := ioutil.ReadDir(filepath.Join(dir, c.Name())) 947 if err != nil { 948 return 0, err 949 } 950 951 for _, m := range mounts { 952 volumeMount := m.Name() 953 prefix := portal + "-" + iqn 954 if strings.HasPrefix(volumeMount, prefix) { 955 counter++ 956 } 957 } 958 } 959 960 return counter, nil 961 } 962 963 func ignoreExitCodes(err error, ignoredExitCodes ...int) error { 964 exitError, ok := err.(utilexec.ExitError) 965 if !ok { 966 return err 967 } 968 for _, code := range ignoredExitCodes { 969 if exitError.ExitStatus() == code { 970 klog.V(4).Infof("ignored iscsiadm exit code %d", code) 971 return nil 972 } 973 } 974 return err 975 } 976 977 func execWithLog(b iscsiDiskMounter, cmd string, args ...string) (string, error) { 978 start := time.Now() 979 out, err := b.exec.Command(cmd, args...).CombinedOutput() 980 if klogV := klog.V(5); klogV.Enabled() { 981 d := time.Since(start) 982 klogV.Infof("Executed %s %v in %v, err: %v", cmd, args, d, err) 983 klogV.Infof("Output: %s", string(out)) 984 } 985 return string(out), err 986 }