iscsi_util.go 35 KB

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