2015-03-13 21:31:13 +00:00
/ *
2016-06-03 00:25:58 +00:00
Copyright 2015 The Kubernetes Authors .
2015-03-13 21:31:13 +00:00
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package iscsi
import (
2017-05-31 19:52:00 +00:00
"encoding/json"
2015-10-13 18:50:49 +00:00
"fmt"
2015-03-13 21:31:13 +00:00
"os"
"path"
2015-11-05 19:06:20 +00:00
"path/filepath"
2016-07-25 04:18:38 +00:00
"regexp"
2015-03-13 21:31:13 +00:00
"strings"
"time"
2015-08-05 22:05:17 +00:00
"github.com/golang/glog"
2015-08-05 22:03:47 +00:00
"k8s.io/kubernetes/pkg/util/mount"
"k8s.io/kubernetes/pkg/volume"
2015-03-13 21:31:13 +00:00
)
2017-03-17 20:42:15 +00:00
var (
chap_st = [ ] string {
"discovery.sendtargets.auth.username" ,
"discovery.sendtargets.auth.password" ,
"discovery.sendtargets.auth.username_in" ,
"discovery.sendtargets.auth.password_in" }
chap_sess = [ ] string {
"node.session.auth.username" ,
"node.session.auth.password" ,
"node.session.auth.username_in" ,
"node.session.auth.password_in" }
)
func updateISCSIDiscoverydb ( b iscsiDiskMounter , tp string ) error {
if b . chap_discovery {
2017-05-31 19:52:00 +00:00
out , err := b . plugin . execCommand ( "iscsiadm" , [ ] string { "-m" , "discoverydb" , "-t" , "sendtargets" , "-p" , tp , "-I" , b . Iface , "-o" , "update" , "-n" , "discovery.sendtargets.auth.authmethod" , "-v" , "CHAP" } )
2017-03-17 20:42:15 +00:00
if err != nil {
return fmt . Errorf ( "iscsi: failed to update discoverydb with CHAP, output: %v" , string ( out ) )
}
for _ , k := range chap_st {
v := b . secret [ k ]
if len ( v ) > 0 {
2017-05-31 19:52:00 +00:00
out , err := b . plugin . execCommand ( "iscsiadm" , [ ] string { "-m" , "discoverydb" , "-t" , "sendtargets" , "-p" , tp , "-I" , b . Iface , "-o" , "update" , "-n" , k , "-v" , v } )
2017-03-17 20:42:15 +00:00
if err != nil {
return fmt . Errorf ( "iscsi: failed to update discoverydb key %q with value %q error: %v" , k , v , string ( out ) )
}
}
}
}
return nil
}
func updateISCSINode ( b iscsiDiskMounter , tp string ) error {
if b . chap_session {
2017-05-31 19:52:00 +00:00
out , err := b . plugin . execCommand ( "iscsiadm" , [ ] string { "-m" , "node" , "-p" , tp , "-T" , b . Iqn , "-I" , b . Iface , "-o" , "update" , "-n" , "node.session.auth.authmethod" , "-v" , "CHAP" } )
2017-03-17 20:42:15 +00:00
if err != nil {
return fmt . Errorf ( "iscsi: failed to update node with CHAP, output: %v" , string ( out ) )
}
for _ , k := range chap_sess {
v := b . secret [ k ]
if len ( v ) > 0 {
2017-05-31 19:52:00 +00:00
out , err := b . plugin . execCommand ( "iscsiadm" , [ ] string { "-m" , "node" , "-p" , tp , "-T" , b . Iqn , "-I" , b . Iface , "-o" , "update" , "-n" , k , "-v" , v } )
2017-03-17 20:42:15 +00:00
if err != nil {
return fmt . Errorf ( "iscsi: failed to update node session key %q with value %q error: %v" , k , v , string ( out ) )
}
}
}
}
return nil
}
2015-03-13 21:31:13 +00:00
// stat a path, if not exists, retry maxRetries times
2015-11-05 19:06:20 +00:00
// when iscsi transports other than default are used, use glob instead as pci id of device is unknown
type StatFunc func ( string ) ( os . FileInfo , error )
type GlobFunc func ( string ) ( [ ] string , error )
2016-07-25 04:18:38 +00:00
func waitForPathToExist ( devicePath string , maxRetries int , deviceTransport string ) bool {
2015-11-05 19:06:20 +00:00
// This makes unit testing a lot easier
2016-07-25 04:18:38 +00:00
return waitForPathToExistInternal ( devicePath , maxRetries , deviceTransport , os . Stat , filepath . Glob )
2015-11-05 19:06:20 +00:00
}
2016-07-25 04:18:38 +00:00
func waitForPathToExistInternal ( devicePath string , maxRetries int , deviceTransport string , osStat StatFunc , filepathGlob GlobFunc ) bool {
2015-03-13 21:31:13 +00:00
for i := 0 ; i < maxRetries ; i ++ {
2015-11-05 19:06:20 +00:00
var err error
2016-07-25 04:18:38 +00:00
if deviceTransport == "tcp" {
2015-11-05 19:06:20 +00:00
_ , err = osStat ( devicePath )
} else {
fpath , _ := filepathGlob ( devicePath )
if fpath == nil {
err = os . ErrNotExist
}
}
2015-03-13 21:31:13 +00:00
if err == nil {
return true
}
if err != nil && ! os . IsNotExist ( err ) {
return false
}
2016-12-21 09:52:01 +00:00
if i == maxRetries - 1 {
break
}
2015-03-13 21:31:13 +00:00
time . Sleep ( time . Second )
}
return false
}
// getDevicePrefixRefCount: given a prefix of device path, find its reference count from /proc/mounts
// returns the reference count to the device and error code
// for services like iscsi construct multiple device paths with the same prefix pattern.
// this function aggregates all references to a service based on the prefix pattern
// More specifically, this prefix semantics is to aggregate disk paths that belong to the same iSCSI target/iqn pair.
// an iSCSI target could expose multiple LUNs through the same IQN, and Linux iSCSI initiator creates disk paths that start the same prefix but end with different LUN number
// When we decide whether it is time to logout a target, we have to see if none of the LUNs are used any more.
// That's where the prefix based ref count kicks in. If we only count the disks using exact match, we could log other disks out.
func getDevicePrefixRefCount ( mounter mount . Interface , deviceNamePrefix string ) ( int , error ) {
mps , err := mounter . List ( )
if err != nil {
return - 1 , err
}
// Find the number of references to the device.
refCount := 0
for i := range mps {
2015-10-13 18:50:49 +00:00
if strings . HasPrefix ( mps [ i ] . Path , deviceNamePrefix ) {
2015-03-13 21:31:13 +00:00
refCount ++
}
}
return refCount , nil
}
2016-12-23 14:42:13 +00:00
// make a directory like /var/lib/kubelet/plugins/kubernetes.io/iscsi/iface_name/portal-some_iqn-lun-lun_id
func makePDNameInternal ( host volume . VolumeHost , portal string , iqn string , lun string , iface string ) string {
2017-02-14 20:50:26 +00:00
return path . Join ( host . GetPluginDir ( iscsiPluginName ) , "iface-" + iface , portal + "-" + iqn + "-lun-" + lun )
2015-03-13 21:31:13 +00:00
}
type ISCSIUtil struct { }
func ( util * ISCSIUtil ) MakeGlobalPDName ( iscsi iscsiDisk ) string {
2017-05-31 19:52:00 +00:00
return makePDNameInternal ( iscsi . plugin . host , iscsi . Portals [ 0 ] , iscsi . Iqn , iscsi . lun , iscsi . Iface )
}
func ( util * ISCSIUtil ) persistISCSI ( conf iscsiDisk , mnt string ) error {
file := path . Join ( mnt , "iscsi.json" )
fp , err := os . Create ( file )
if err != nil {
return fmt . Errorf ( "iscsi: create %s err %s" , file , err )
}
defer fp . Close ( )
encoder := json . NewEncoder ( fp )
if err = encoder . Encode ( conf ) ; err != nil {
return fmt . Errorf ( "iscsi: encode err: %v." , err )
}
return nil
}
func ( util * ISCSIUtil ) loadISCSI ( conf * iscsiDisk , mnt string ) error {
// NOTE: The iscsi config json is not deleted after logging out from target portals.
file := path . Join ( mnt , "iscsi.json" )
fp , err := os . Open ( file )
if err != nil {
return fmt . Errorf ( "iscsi: open %s err %s" , file , err )
}
defer fp . Close ( )
decoder := json . NewDecoder ( fp )
if err = decoder . Decode ( conf ) ; err != nil {
return fmt . Errorf ( "iscsi: decode err: %v." , err )
}
return nil
2015-03-13 21:31:13 +00:00
}
2016-03-23 05:12:21 +00:00
func ( util * ISCSIUtil ) AttachDisk ( b iscsiDiskMounter ) error {
2015-11-05 19:06:20 +00:00
var devicePath string
2017-02-06 11:36:33 +00:00
var devicePaths [ ] string
2016-07-25 04:18:38 +00:00
var iscsiTransport string
2017-03-17 20:42:15 +00:00
var lastErr error
2016-12-23 14:42:13 +00:00
2017-05-31 19:52:00 +00:00
out , err := b . plugin . execCommand ( "iscsiadm" , [ ] string { "-m" , "iface" , "-I" , b . Iface , "-o" , "show" } )
2016-07-25 04:18:38 +00:00
if err != nil {
2017-05-31 19:52:00 +00:00
glog . Errorf ( "iscsi: could not read iface %s error: %s" , b . Iface , string ( out ) )
2016-07-25 04:18:38 +00:00
return err
}
iscsiTransport = extractTransportname ( string ( out ) )
2016-12-23 14:42:13 +00:00
2017-05-31 19:52:00 +00:00
bkpPortal := b . Portals
2017-02-06 11:36:33 +00:00
for _ , tp := range bkpPortal {
2016-12-23 14:42:13 +00:00
// Rescan sessions to discover newly mapped LUNs. Do not specify the interface when rescanning
// to avoid establishing additional sessions to the same target.
2017-05-31 19:52:00 +00:00
out , err := b . plugin . execCommand ( "iscsiadm" , [ ] string { "-m" , "node" , "-p" , tp , "-T" , b . Iqn , "-R" } )
2016-12-23 14:42:13 +00:00
if err != nil {
glog . Errorf ( "iscsi: failed to rescan session with error: %s (%v)" , string ( out ) , err )
}
2017-02-06 11:36:33 +00:00
if iscsiTransport == "" {
2017-05-31 19:52:00 +00:00
glog . Errorf ( "iscsi: could not find transport name in iface %s" , b . Iface )
return fmt . Errorf ( "Could not parse iface file for %s" , b . Iface )
2017-02-06 11:36:33 +00:00
} else if iscsiTransport == "tcp" {
2017-05-31 19:52:00 +00:00
devicePath = strings . Join ( [ ] string { "/dev/disk/by-path/ip" , tp , "iscsi" , b . Iqn , "lun" , b . lun } , "-" )
2017-02-06 11:36:33 +00:00
} else {
2017-05-31 19:52:00 +00:00
devicePath = strings . Join ( [ ] string { "/dev/disk/by-path/pci" , "*" , "ip" , tp , "iscsi" , b . Iqn , "lun" , b . lun } , "-" )
2015-03-13 21:31:13 +00:00
}
2017-02-06 11:36:33 +00:00
exist := waitForPathToExist ( devicePath , 1 , iscsiTransport )
if exist == false {
2017-03-17 20:42:15 +00:00
// build discoverydb and discover iscsi target
2017-05-31 19:52:00 +00:00
b . plugin . execCommand ( "iscsiadm" , [ ] string { "-m" , "discoverydb" , "-t" , "sendtargets" , "-p" , tp , "-I" , b . Iface , "-o" , "new" } )
2017-03-17 20:42:15 +00:00
// update discoverydb with CHAP secret
err = updateISCSIDiscoverydb ( b , tp )
if err != nil {
lastErr = fmt . Errorf ( "iscsi: failed to update discoverydb to portal %s error: %v" , tp , err )
continue
}
2017-05-31 19:52:00 +00:00
out , err := b . plugin . execCommand ( "iscsiadm" , [ ] string { "-m" , "discoverydb" , "-t" , "sendtargets" , "-p" , tp , "-I" , b . Iface , "--discover" } )
2017-02-06 11:36:33 +00:00
if err != nil {
2017-03-17 20:42:15 +00:00
// delete discoverydb record
2017-05-31 19:52:00 +00:00
b . plugin . execCommand ( "iscsiadm" , [ ] string { "-m" , "discoverydb" , "-t" , "sendtargets" , "-p" , tp , "-I" , b . Iface , "-o" , "delete" } )
2017-03-17 20:42:15 +00:00
lastErr = fmt . Errorf ( "iscsi: failed to sendtargets to portal %s output: %s, err %v" , tp , string ( out ) , err )
continue
}
err = updateISCSINode ( b , tp )
if err != nil {
// failure to update node db is rare. But deleting record will likely impact those who already start using it.
lastErr = fmt . Errorf ( "iscsi: failed to update iscsi node to portal %s error: %v" , tp , err )
2017-02-06 11:36:33 +00:00
continue
}
// login to iscsi target
2017-05-31 19:52:00 +00:00
out , err = b . plugin . execCommand ( "iscsiadm" , [ ] string { "-m" , "node" , "-p" , tp , "-T" , b . Iqn , "-I" , b . Iface , "--login" } )
2017-02-06 11:36:33 +00:00
if err != nil {
2017-03-17 20:42:15 +00:00
// delete the node record from database
2017-05-31 19:52:00 +00:00
b . plugin . execCommand ( "iscsiadm" , [ ] string { "-m" , "node" , "-p" , tp , "-I" , b . Iface , "-T" , b . Iqn , "-o" , "delete" } )
2017-03-17 20:42:15 +00:00
lastErr = fmt . Errorf ( "iscsi: failed to attach disk: Error: %s (%v)" , string ( out ) , err )
2017-02-06 11:36:33 +00:00
continue
}
exist = waitForPathToExist ( devicePath , 10 , iscsiTransport )
if ! exist {
glog . Errorf ( "Could not attach disk: Timeout after 10s" )
2017-03-17 20:42:15 +00:00
// update last error
lastErr = fmt . Errorf ( "Could not attach disk: Timeout after 10s" )
continue
2017-02-06 11:36:33 +00:00
} else {
devicePaths = append ( devicePaths , devicePath )
}
} else {
glog . V ( 4 ) . Infof ( "iscsi: devicepath (%s) exists" , devicePath )
devicePaths = append ( devicePaths , devicePath )
2015-03-13 21:31:13 +00:00
}
}
2017-02-06 11:36:33 +00:00
if len ( devicePaths ) == 0 {
2017-03-17 20:42:15 +00:00
glog . Errorf ( "iscsi: failed to get any path for iscsi disk, last err seen:\n%v" , lastErr )
return fmt . Errorf ( "failed to get any path for iscsi disk, last err seen:\n%v" , lastErr )
2017-02-06 11:36:33 +00:00
}
//Make sure we use a valid devicepath to find mpio device.
devicePath = devicePaths [ 0 ]
2015-03-13 21:31:13 +00:00
// mount it
2015-07-24 08:55:56 +00:00
globalPDPath := b . manager . MakeGlobalPDName ( * b . iscsiDisk )
2015-04-16 23:49:53 +00:00
notMnt , err := b . mounter . IsLikelyNotMountPoint ( globalPDPath )
if ! notMnt {
2015-03-13 21:31:13 +00:00
glog . Infof ( "iscsi: %s already mounted" , globalPDPath )
return nil
}
if err := os . MkdirAll ( globalPDPath , 0750 ) ; err != nil {
glog . Errorf ( "iscsi: failed to mkdir %s, error" , globalPDPath )
return err
}
2017-05-31 19:52:00 +00:00
// Persist iscsi disk config to json file for DetachDisk path
util . persistISCSI ( * ( b . iscsiDisk ) , globalPDPath )
2017-02-06 11:36:33 +00:00
for _ , path := range devicePaths {
// There shouldnt be any empty device paths. However adding this check
// for safer side to avoid the possibility of an empty entry.
if path == "" {
continue
}
// check if the dev is using mpio and if so mount it via the dm-XX device
if mappedDevicePath := b . deviceUtil . FindMultipathDeviceForDevice ( path ) ; mappedDevicePath != "" {
devicePath = mappedDevicePath
break
}
2016-04-19 05:10:00 +00:00
}
2015-11-05 21:49:40 +00:00
err = b . mounter . FormatAndMount ( devicePath , globalPDPath , b . fsType , nil )
2015-03-13 21:31:13 +00:00
if err != nil {
2015-07-24 08:55:56 +00:00
glog . Errorf ( "iscsi: failed to mount iscsi volume %s [%s] to %s, error %v" , devicePath , b . fsType , globalPDPath , err )
2015-03-13 21:31:13 +00:00
}
return err
}
2016-03-23 05:12:21 +00:00
func ( util * ISCSIUtil ) DetachDisk ( c iscsiDiskUnmounter , mntPath string ) error {
2015-10-13 18:50:49 +00:00
_ , cnt , err := mount . GetDeviceNameFromMount ( c . mounter , mntPath )
2015-03-13 21:31:13 +00:00
if err != nil {
glog . Errorf ( "iscsi detach disk: failed to get device from mnt: %s\nError: %v" , mntPath , err )
return err
}
2015-07-24 08:55:56 +00:00
if err = c . mounter . Unmount ( mntPath ) ; err != nil {
2015-04-03 01:08:04 +00:00
glog . Errorf ( "iscsi detach disk: failed to unmount: %s\nError: %v" , mntPath , err )
2015-03-13 21:31:13 +00:00
return err
}
cnt --
// if device is no longer used, see if need to logout the target
if cnt == 0 {
2015-10-13 18:50:49 +00:00
device , prefix , err := extractDeviceAndPrefix ( mntPath )
if err != nil {
return err
}
2015-07-24 08:55:56 +00:00
refCount , err := getDevicePrefixRefCount ( c . mounter , prefix )
2015-03-13 21:31:13 +00:00
if err == nil && refCount == 0 {
2017-05-31 19:52:00 +00:00
var bkpPortal [ ] string
var iqn , iface string
found := true
// load iscsi disk config from json file
if err := util . loadISCSI ( c . iscsiDisk , mntPath ) ; err == nil {
bkpPortal , iqn , iface = c . iscsiDisk . Portals , c . iscsiDisk . Iqn , c . iscsiDisk . Iface
} else {
// If the iscsi disk config is not found, fall back to the original behavior.
// This portal/iqn/iface is no longer referenced, log out.
// Extract the portal and iqn from device path.
2017-06-05 14:22:57 +00:00
bkpPortal = make ( [ ] string , 1 )
2017-05-31 19:52:00 +00:00
bkpPortal [ 0 ] , iqn , err = extractPortalAndIqn ( device )
2017-02-14 20:50:26 +00:00
if err != nil {
2017-05-31 19:52:00 +00:00
return err
2017-02-14 20:50:26 +00:00
}
2017-05-31 19:52:00 +00:00
// Extract the iface from the mountPath and use it to log out. If the iface
// is not found, maintain the previous behavior to facilitate kubelet upgrade.
// Logout may fail as no session may exist for the portal/IQN on the specified interface.
iface , found = extractIface ( mntPath )
}
for _ , portal := range removeDuplicate ( bkpPortal ) {
logout := [ ] string { "-m" , "node" , "-p" , portal , "-T" , iqn , "--logout" }
delete := [ ] string { "-m" , "node" , "-p" , portal , "-T" , iqn , "-o" , "delete" }
if found {
logout = append ( logout , [ ] string { "-I" , iface } ... )
delete = append ( delete , [ ] string { "-I" , iface } ... )
2017-03-17 20:42:15 +00:00
}
2017-05-31 19:52:00 +00:00
glog . Infof ( "iscsi: log out target %s iqn %s iface %s" , portal , iqn , iface )
out , err := c . plugin . execCommand ( "iscsiadm" , logout )
2017-02-14 20:50:26 +00:00
if err != nil {
glog . Errorf ( "iscsi: failed to detach disk Error: %s" , string ( out ) )
}
2017-03-17 20:42:15 +00:00
// Delete the node record
glog . Infof ( "iscsi: delete node record target %s iqn %s" , portal , iqn )
2017-05-31 19:52:00 +00:00
out , err = c . plugin . execCommand ( "iscsiadm" , delete )
2017-03-17 20:42:15 +00:00
if err != nil {
glog . Errorf ( "iscsi: failed to delete node record Error: %s" , string ( out ) )
}
2015-03-13 21:31:13 +00:00
}
}
}
return nil
}
2015-10-13 18:50:49 +00:00
2016-07-25 04:18:38 +00:00
func extractTransportname ( ifaceOutput string ) ( iscsiTransport string ) {
re := regexp . MustCompile ( ` iface.transport_name = (.*)\n ` )
rex_output := re . FindStringSubmatch ( ifaceOutput )
if rex_output != nil {
iscsiTransport = rex_output [ 1 ]
} else {
return ""
}
// While iface.transport_name is a required parameter, handle it being unspecified anyways
if iscsiTransport == "<empty>" {
iscsiTransport = "tcp"
}
return iscsiTransport
}
2015-10-13 18:50:49 +00:00
func extractDeviceAndPrefix ( mntPath string ) ( string , string , error ) {
ind := strings . LastIndex ( mntPath , "/" )
if ind < 0 {
return "" , "" , fmt . Errorf ( "iscsi detach disk: malformatted mnt path: %s" , mntPath )
}
device := mntPath [ ( ind + 1 ) : ]
2016-12-23 14:42:13 +00:00
// strip -lun- from mount path
ind = strings . LastIndex ( mntPath , "-lun-" )
2015-10-13 18:50:49 +00:00
if ind < 0 {
return "" , "" , fmt . Errorf ( "iscsi detach disk: malformatted mnt path: %s" , mntPath )
}
2016-12-23 14:42:13 +00:00
prefix := mntPath [ : ind ]
2015-10-13 18:50:49 +00:00
return device , prefix , nil
}
2017-02-14 20:50:26 +00:00
func extractIface ( mntPath string ) ( string , bool ) {
re := regexp . MustCompile ( ` .+/iface-([^/]+)/.+ ` )
2016-12-23 14:42:13 +00:00
2017-02-14 20:50:26 +00:00
re_output := re . FindStringSubmatch ( mntPath )
if re_output != nil {
return re_output [ 1 ] , true
2016-12-23 14:42:13 +00:00
}
2017-02-14 20:50:26 +00:00
return "" , false
2016-12-23 14:42:13 +00:00
}
2015-10-13 18:50:49 +00:00
func extractPortalAndIqn ( device string ) ( string , string , error ) {
ind1 := strings . Index ( device , "-" )
if ind1 < 0 {
return "" , "" , fmt . Errorf ( "iscsi detach disk: no portal in %s" , device )
}
portal := device [ 0 : ind1 ]
ind2 := strings . Index ( device , "iqn." )
2015-11-24 08:13:59 +00:00
if ind2 < 0 {
ind2 = strings . Index ( device , "eui." )
}
2015-10-13 18:50:49 +00:00
if ind2 < 0 {
return "" , "" , fmt . Errorf ( "iscsi detach disk: no iqn in %s" , device )
}
ind := strings . LastIndex ( device , "-lun-" )
iqn := device [ ind2 : ind ]
return portal , iqn , nil
}
2017-05-31 19:52:00 +00:00
// Remove duplicates or string
func removeDuplicate ( s [ ] string ) [ ] string {
m := map [ string ] bool { }
for _ , v := range s {
if v != "" && ! m [ v ] {
s [ len ( m ) ] = v
m [ v ] = true
}
}
s = s [ : len ( m ) ]
return s
}