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