1// +build windows 2 3/* 4Copyright 2017 The Kubernetes Authors. 5 6Licensed under the Apache License, Version 2.0 (the "License"); 7you may not use this file except in compliance with the License. 8You may obtain a copy of the License at 9 10 http://www.apache.org/licenses/LICENSE-2.0 11 12Unless required by applicable law or agreed to in writing, software 13distributed under the License is distributed on an "AS IS" BASIS, 14WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15See the License for the specific language governing permissions and 16limitations under the License. 17*/ 18 19package mount 20 21import ( 22 "fmt" 23 "os" 24 "os/exec" 25 "path/filepath" 26 "strings" 27 28 "k8s.io/klog/v2" 29 "k8s.io/utils/keymutex" 30) 31 32const ( 33 accessDenied string = "access is denied" 34) 35 36// Mounter provides the default implementation of mount.Interface 37// for the windows platform. This implementation assumes that the 38// kubelet is running in the host's root mount namespace. 39type Mounter struct { 40 mounterPath string 41} 42 43// New returns a mount.Interface for the current system. 44// It provides options to override the default mounter behavior. 45// mounterPath allows using an alternative to `/bin/mount` for mounting. 46func New(mounterPath string) Interface { 47 return &Mounter{ 48 mounterPath: mounterPath, 49 } 50} 51 52// acquire lock for smb mount 53var getSMBMountMutex = keymutex.NewHashed(0) 54 55// Mount : mounts source to target with given options. 56// currently only supports cifs(smb), bind mount(for disk) 57func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error { 58 return mounter.MountSensitive(source, target, fstype, options, nil /* sensitiveOptions */) 59} 60 61// MountSensitive is the same as Mount() but this method allows 62// sensitiveOptions to be passed in a separate parameter from the normal 63// mount options and ensures the sensitiveOptions are never logged. This 64// method should be used by callers that pass sensitive material (like 65// passwords) as mount options. 66func (mounter *Mounter) MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error { 67 target = NormalizeWindowsPath(target) 68 sanitizedOptionsForLogging := sanitizedOptionsForLogging(options, sensitiveOptions) 69 70 if source == "tmpfs" { 71 klog.V(3).Infof("mounting source (%q), target (%q), with options (%q)", source, target, sanitizedOptionsForLogging) 72 return os.MkdirAll(target, 0755) 73 } 74 75 parentDir := filepath.Dir(target) 76 if err := os.MkdirAll(parentDir, 0755); err != nil { 77 return err 78 } 79 80 klog.V(4).Infof("mount options(%q) source:%q, target:%q, fstype:%q, begin to mount", 81 sanitizedOptionsForLogging, source, target, fstype) 82 bindSource := source 83 84 if bind, _, _, _ := MakeBindOptsSensitive(options, sensitiveOptions); bind { 85 bindSource = NormalizeWindowsPath(source) 86 } else { 87 allOptions := []string{} 88 allOptions = append(allOptions, options...) 89 allOptions = append(allOptions, sensitiveOptions...) 90 if len(allOptions) < 2 { 91 return fmt.Errorf("mount options(%q) should have at least 2 options, current number:%d, source:%q, target:%q", 92 sanitizedOptionsForLogging, len(allOptions), source, target) 93 } 94 95 // currently only cifs mount is supported 96 if strings.ToLower(fstype) != "cifs" { 97 return fmt.Errorf("only cifs mount is supported now, fstype: %q, mounting source (%q), target (%q), with options (%q)", fstype, source, target, sanitizedOptionsForLogging) 98 } 99 100 // lock smb mount for the same source 101 getSMBMountMutex.LockKey(source) 102 defer getSMBMountMutex.UnlockKey(source) 103 104 username := allOptions[0] 105 password := allOptions[1] 106 if output, err := newSMBMapping(username, password, source); err != nil { 107 klog.Warningf("SMB Mapping(%s) returned with error(%v), output(%s)", source, err, string(output)) 108 if isSMBMappingExist(source) { 109 valid, err := isValidPath(source) 110 if !valid { 111 if err == nil || isAccessDeniedError(err) { 112 klog.V(2).Infof("SMB Mapping(%s) already exists while it's not valid, return error: %v, now begin to remove and remount", source, err) 113 if output, err = removeSMBMapping(source); err != nil { 114 return fmt.Errorf("Remove-SmbGlobalMapping failed: %v, output: %q", err, output) 115 } 116 if output, err := newSMBMapping(username, password, source); err != nil { 117 return fmt.Errorf("New-SmbGlobalMapping(%s) failed: %v, output: %q", source, err, output) 118 } 119 } 120 } else { 121 klog.V(2).Infof("SMB Mapping(%s) already exists and is still valid, skip error(%v)", source, err) 122 } 123 } else { 124 return fmt.Errorf("New-SmbGlobalMapping(%s) failed: %v, output: %q", source, err, output) 125 } 126 } 127 } 128 129 output, err := exec.Command("cmd", "/c", "mklink", "/D", target, bindSource).CombinedOutput() 130 if err != nil { 131 klog.Errorf("mklink failed: %v, source(%q) target(%q) output: %q", err, bindSource, target, string(output)) 132 return err 133 } 134 klog.V(2).Infof("mklink source(%q) on target(%q) successfully, output: %q", bindSource, target, string(output)) 135 136 return nil 137} 138 139// do the SMB mount with username, password, remotepath 140// return (output, error) 141func newSMBMapping(username, password, remotepath string) (string, error) { 142 if username == "" || password == "" || remotepath == "" { 143 return "", fmt.Errorf("invalid parameter(username: %s, password: %s, remoteapth: %s)", username, sensitiveOptionsRemoved, remotepath) 144 } 145 146 // use PowerShell Environment Variables to store user input string to prevent command line injection 147 // https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-5.1 148 cmdLine := `$PWord = ConvertTo-SecureString -String $Env:smbpassword -AsPlainText -Force` + 149 `;$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Env:smbuser, $PWord` + 150 `;New-SmbGlobalMapping -RemotePath $Env:smbremotepath -Credential $Credential` 151 cmd := exec.Command("powershell", "/c", cmdLine) 152 cmd.Env = append(os.Environ(), 153 fmt.Sprintf("smbuser=%s", username), 154 fmt.Sprintf("smbpassword=%s", password), 155 fmt.Sprintf("smbremotepath=%s", remotepath)) 156 157 output, err := cmd.CombinedOutput() 158 return string(output), err 159} 160 161// check whether remotepath is already mounted 162func isSMBMappingExist(remotepath string) bool { 163 cmd := exec.Command("powershell", "/c", `Get-SmbGlobalMapping -RemotePath $Env:smbremotepath`) 164 cmd.Env = append(os.Environ(), fmt.Sprintf("smbremotepath=%s", remotepath)) 165 _, err := cmd.CombinedOutput() 166 return err == nil 167} 168 169// check whether remotepath is valid 170// return (true, nil) if remotepath is valid 171func isValidPath(remotepath string) (bool, error) { 172 cmd := exec.Command("powershell", "/c", `Test-Path $Env:remoteapth`) 173 cmd.Env = append(os.Environ(), fmt.Sprintf("remoteapth=%s", remotepath)) 174 output, err := cmd.CombinedOutput() 175 if err != nil { 176 return false, fmt.Errorf("returned output: %s, error: %v", string(output), err) 177 } 178 179 return strings.HasPrefix(strings.ToLower(string(output)), "true"), nil 180} 181 182func isAccessDeniedError(err error) bool { 183 return err != nil && strings.Contains(strings.ToLower(err.Error()), accessDenied) 184} 185 186// remove SMB mapping 187func removeSMBMapping(remotepath string) (string, error) { 188 cmd := exec.Command("powershell", "/c", `Remove-SmbGlobalMapping -RemotePath $Env:smbremotepath -Force`) 189 cmd.Env = append(os.Environ(), fmt.Sprintf("smbremotepath=%s", remotepath)) 190 output, err := cmd.CombinedOutput() 191 return string(output), err 192} 193 194// Unmount unmounts the target. 195func (mounter *Mounter) Unmount(target string) error { 196 klog.V(4).Infof("azureMount: Unmount target (%q)", target) 197 target = NormalizeWindowsPath(target) 198 if output, err := exec.Command("cmd", "/c", "rmdir", target).CombinedOutput(); err != nil { 199 klog.Errorf("rmdir failed: %v, output: %q", err, string(output)) 200 return err 201 } 202 return nil 203} 204 205// List returns a list of all mounted filesystems. todo 206func (mounter *Mounter) List() ([]MountPoint, error) { 207 return []MountPoint{}, nil 208} 209 210// IsLikelyNotMountPoint determines if a directory is not a mountpoint. 211func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) { 212 stat, err := os.Lstat(file) 213 if err != nil { 214 return true, err 215 } 216 217 if stat.Mode()&os.ModeSymlink != 0 { 218 return false, err 219 } 220 return true, nil 221} 222 223// GetMountRefs : empty implementation here since there is no place to query all mount points on Windows 224func (mounter *Mounter) GetMountRefs(pathname string) ([]string, error) { 225 windowsPath := NormalizeWindowsPath(pathname) 226 pathExists, pathErr := PathExists(windowsPath) 227 if !pathExists { 228 return []string{}, nil 229 } else if IsCorruptedMnt(pathErr) { 230 klog.Warningf("GetMountRefs found corrupted mount at %s, treating as unmounted path", windowsPath) 231 return []string{}, nil 232 } else if pathErr != nil { 233 return nil, fmt.Errorf("error checking path %s: %v", windowsPath, pathErr) 234 } 235 return []string{pathname}, nil 236} 237 238func (mounter *SafeFormatAndMount) formatAndMountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error { 239 // Try to mount the disk 240 klog.V(4).Infof("Attempting to formatAndMount disk: %s %s %s", fstype, source, target) 241 242 if err := ValidateDiskNumber(source); err != nil { 243 klog.Errorf("diskMount: formatAndMount failed, err: %v", err) 244 return err 245 } 246 247 if len(fstype) == 0 { 248 // Use 'NTFS' as the default 249 fstype = "NTFS" 250 } 251 252 // format disk if it is unformatted(raw) 253 cmd := fmt.Sprintf("Get-Disk -Number %s | Where partitionstyle -eq 'raw' | Initialize-Disk -PartitionStyle MBR -PassThru"+ 254 " | New-Partition -UseMaximumSize | Format-Volume -FileSystem %s -Confirm:$false", source, fstype) 255 if output, err := mounter.Exec.Command("powershell", "/c", cmd).CombinedOutput(); err != nil { 256 return fmt.Errorf("diskMount: format disk failed, error: %v, output: %q", err, string(output)) 257 } 258 klog.V(4).Infof("diskMount: Disk successfully formatted, disk: %q, fstype: %q", source, fstype) 259 260 volumeIds, err := listVolumesOnDisk(source) 261 if err != nil { 262 return err 263 } 264 driverPath := volumeIds[0] 265 target = NormalizeWindowsPath(target) 266 output, err := mounter.Exec.Command("cmd", "/c", "mklink", "/D", target, driverPath).CombinedOutput() 267 if err != nil { 268 klog.Errorf("mklink(%s, %s) failed: %v, output: %q", target, driverPath, err, string(output)) 269 return err 270 } 271 klog.V(2).Infof("formatAndMount disk(%s) fstype(%s) on(%s) with output(%s) successfully", driverPath, fstype, target, string(output)) 272 return nil 273} 274 275// ListVolumesOnDisk - returns back list of volumes(volumeIDs) in the disk (requested in diskID). 276func listVolumesOnDisk(diskID string) (volumeIDs []string, err error) { 277 cmd := fmt.Sprintf("(Get-Disk -DeviceId %s | Get-Partition | Get-Volume).UniqueId", diskID) 278 output, err := exec.Command("powershell", "/c", cmd).CombinedOutput() 279 klog.V(4).Infof("listVolumesOnDisk id from %s: %s", diskID, string(output)) 280 if err != nil { 281 return []string{}, fmt.Errorf("error list volumes on disk. cmd: %s, output: %s, error: %v", cmd, string(output), err) 282 } 283 284 volumeIds := strings.Split(strings.TrimSpace(string(output)), "\r\n") 285 return volumeIds, nil 286} 287 288// getAllParentLinks walks all symbolic links and return all the parent targets recursively 289func getAllParentLinks(path string) ([]string, error) { 290 const maxIter = 255 291 links := []string{} 292 for { 293 links = append(links, path) 294 if len(links) > maxIter { 295 return links, fmt.Errorf("unexpected length of parent links: %v", links) 296 } 297 298 fi, err := os.Lstat(path) 299 if err != nil { 300 return links, fmt.Errorf("Lstat: %v", err) 301 } 302 if fi.Mode()&os.ModeSymlink == 0 { 303 break 304 } 305 306 path, err = os.Readlink(path) 307 if err != nil { 308 return links, fmt.Errorf("Readlink error: %v", err) 309 } 310 } 311 312 return links, nil 313} 314