1/*
2Copyright 2014 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package mount
18
19import (
20	"fmt"
21	"io/ioutil"
22	"os"
23	"runtime"
24	"strings"
25	"testing"
26
27	"k8s.io/utils/exec"
28	testingexec "k8s.io/utils/exec/testing"
29)
30
31type ErrorMounter struct {
32	*FakeMounter
33	errIndex int
34	err      []error
35}
36
37func (mounter *ErrorMounter) Mount(source string, target string, fstype string, options []string) error {
38	return mounter.MountSensitive(source, target, fstype, options, nil /* sensitiveOptions */)
39}
40
41func (mounter *ErrorMounter) MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error {
42	i := mounter.errIndex
43	mounter.errIndex++
44	if mounter.err != nil && mounter.err[i] != nil {
45		return mounter.err[i]
46	}
47	return mounter.FakeMounter.Mount(source, target, fstype, options)
48}
49
50type ExecArgs struct {
51	command string
52	args    []string
53	output  string
54	err     error
55}
56
57func TestSafeFormatAndMount(t *testing.T) {
58	if runtime.GOOS == "darwin" || runtime.GOOS == "windows" {
59		t.Skipf("not supported on GOOS=%s", runtime.GOOS)
60	}
61	mntDir, err := ioutil.TempDir(os.TempDir(), "mount")
62	if err != nil {
63		t.Fatalf("failed to create tmp dir: %v", err)
64	}
65	defer os.RemoveAll(mntDir)
66	tests := []struct {
67		description           string
68		fstype                string
69		mountOptions          []string
70		sensitiveMountOptions []string
71		execScripts           []ExecArgs
72		mountErrs             []error
73		expErrorType          MountErrorType
74	}{
75		{
76			description:  "Test a read only mount of an already formatted device",
77			fstype:       "ext4",
78			mountOptions: []string{"ro"},
79			execScripts: []ExecArgs{
80				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "DEVNAME=/dev/foo\nTYPE=ext4\n", nil},
81			},
82		},
83		{
84			description: "Test a normal mount of an already formatted device",
85			fstype:      "ext4",
86			execScripts: []ExecArgs{
87				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "DEVNAME=/dev/foo\nTYPE=ext4\n", nil},
88				{"fsck", []string{"-a", "/dev/foo"}, "", nil},
89			},
90		},
91		{
92			description:  "Test a read only mount of unformatted device",
93			fstype:       "ext4",
94			mountOptions: []string{"ro"},
95			execScripts: []ExecArgs{
96				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}},
97			},
98			expErrorType: UnformattedReadOnly,
99		},
100		{
101			description: "Test a normal mount of unformatted device",
102			fstype:      "ext4",
103			execScripts: []ExecArgs{
104				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}},
105				{"mkfs.ext4", []string{"-F", "-m0", "/dev/foo"}, "", nil},
106			},
107		},
108		{
109			description: "Test 'fsck' fails with exit status 4",
110			fstype:      "ext4",
111			execScripts: []ExecArgs{
112				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "DEVNAME=/dev/foo\nTYPE=ext4\n", nil},
113				{"fsck", []string{"-a", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 4}},
114			},
115			expErrorType: HasFilesystemErrors,
116		},
117		{
118			description: "Test 'fsck' fails with exit status 1 (errors found and corrected)",
119			fstype:      "ext4",
120			execScripts: []ExecArgs{
121				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "DEVNAME=/dev/foo\nTYPE=ext4\n", nil},
122				{"fsck", []string{"-a", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 1}},
123			},
124		},
125		{
126			description: "Test 'fsck' fails with exit status other than 1 and 4 (likely unformatted device)",
127			fstype:      "ext4",
128			execScripts: []ExecArgs{
129				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "DEVNAME=/dev/foo\nTYPE=ext4\n", nil},
130				{"fsck", []string{"-a", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 8}},
131			},
132		},
133		{
134			description: "Test that 'blkid' is called and fails",
135			fstype:      "ext4",
136			mountErrs:   []error{fmt.Errorf("unknown filesystem type '(null)'")},
137			execScripts: []ExecArgs{
138				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "DEVNAME=/dev/foo\nPTTYPE=dos\n", nil},
139				{"fsck", []string{"-a", "/dev/foo"}, "", nil},
140			},
141			expErrorType: FilesystemMismatch,
142		},
143		{
144			description: "Test that 'blkid' is called and confirms unformatted disk, format fails",
145			fstype:      "ext4",
146			mountErrs:   []error{fmt.Errorf("unknown filesystem type '(null)'")},
147			execScripts: []ExecArgs{
148				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}},
149				{"mkfs.ext4", []string{"-F", "-m0", "/dev/foo"}, "", fmt.Errorf("formatting failed")},
150			},
151			expErrorType: FormatFailed,
152		},
153		{
154			description: "Test that 'blkid' is called and confirms unformatted disk, format passes, second mount fails",
155			fstype:      "ext4",
156			mountErrs:   []error{fmt.Errorf("unknown filesystem type '(null)'")},
157			execScripts: []ExecArgs{
158				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}},
159				{"mkfs.ext4", []string{"-F", "-m0", "/dev/foo"}, "", nil},
160			},
161			expErrorType: UnknownMountError,
162		},
163		{
164			description: "Test that 'blkid' is called and confirms unformatted disk, format passes, mount passes",
165			fstype:      "ext4",
166			execScripts: []ExecArgs{
167				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}},
168				{"mkfs.ext4", []string{"-F", "-m0", "/dev/foo"}, "", nil},
169			},
170		},
171		{
172			description: "Test that 'blkid' is called and confirms unformatted disk, format passes, mount passes with ext3",
173			fstype:      "ext3",
174			execScripts: []ExecArgs{
175				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}},
176				{"mkfs.ext3", []string{"-F", "-m0", "/dev/foo"}, "", nil},
177			},
178		},
179		{
180			description: "test that none ext4 fs does not get called with ext4 options.",
181			fstype:      "xfs",
182			execScripts: []ExecArgs{
183				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}},
184				{"mkfs.xfs", []string{"/dev/foo"}, "", nil},
185			},
186		},
187		{
188			description: "Test that 'blkid' is called and reports ext4 partition",
189			fstype:      "ext4",
190			execScripts: []ExecArgs{
191				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "DEVNAME=/dev/foo\nTYPE=ext4\n", nil},
192				{"fsck", []string{"-a", "/dev/foo"}, "", nil},
193			},
194		},
195		{
196			description: "Test that 'blkid' is called but has some usage or other errors (an exit code of 4 is returned)",
197			fstype:      "xfs",
198			mountErrs:   []error{fmt.Errorf("unknown filesystem type '(null)'"), nil},
199			execScripts: []ExecArgs{
200				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 4}},
201				{"mkfs.xfs", []string{"/dev/foo"}, "", nil},
202			},
203			expErrorType: GetDiskFormatFailed,
204		},
205		{
206			description:           "Test that 'blkid' is called and confirms unformatted disk, format fails with sensitive options",
207			fstype:                "ext4",
208			sensitiveMountOptions: []string{"mySecret"},
209			mountErrs:             []error{fmt.Errorf("unknown filesystem type '(null)'")},
210			execScripts: []ExecArgs{
211				{"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}},
212				{"mkfs.ext4", []string{"-F", "-m0", "/dev/foo"}, "", fmt.Errorf("formatting failed")},
213			},
214			expErrorType: FormatFailed,
215		},
216	}
217
218	for _, test := range tests {
219		fakeMounter := ErrorMounter{NewFakeMounter(nil), 0, test.mountErrs}
220		fakeExec := &testingexec.FakeExec{ExactOrder: true}
221		for _, script := range test.execScripts {
222			fakeCmd := &testingexec.FakeCmd{}
223			cmdAction := makeFakeCmd(fakeCmd, script.command, script.args...)
224			outputAction := makeFakeOutput(script.output, script.err)
225			fakeCmd.CombinedOutputScript = append(fakeCmd.CombinedOutputScript, outputAction)
226			fakeExec.CommandScript = append(fakeExec.CommandScript, cmdAction)
227		}
228		mounter := SafeFormatAndMount{
229			Interface: &fakeMounter,
230			Exec:      fakeExec,
231		}
232
233		device := "/dev/foo"
234		dest := mntDir
235		var err error
236		if len(test.sensitiveMountOptions) == 0 {
237			err = mounter.FormatAndMount(device, dest, test.fstype, test.mountOptions)
238		} else {
239			err = mounter.FormatAndMountSensitive(device, dest, test.fstype, test.mountOptions, test.sensitiveMountOptions)
240		}
241		if len(test.expErrorType) == 0 {
242			if err != nil {
243				t.Errorf("test \"%s\" unexpected non-error: %v", test.description, err)
244			}
245
246			// Check that something was mounted on the directory
247			isNotMountPoint, err := fakeMounter.IsLikelyNotMountPoint(dest)
248			if err != nil || isNotMountPoint {
249				t.Errorf("test \"%s\" the directory was not mounted", test.description)
250			}
251
252			//check that the correct device was mounted
253			mountedDevice, _, err := GetDeviceNameFromMount(fakeMounter.FakeMounter, dest)
254			if err != nil || mountedDevice != device {
255				t.Errorf("test \"%s\" the correct device was not mounted", test.description)
256			}
257		} else {
258			mntErr, ok := err.(MountError)
259			if !ok {
260				t.Errorf("mount error not of mount error type: %v", err)
261			}
262			if mntErr.Type != test.expErrorType {
263				t.Errorf("test \"%s\" unexpected error: \n          [%v]. \nExpecting err type[%v]", test.description, err, test.expErrorType)
264			}
265			if len(test.sensitiveMountOptions) == 0 {
266				if strings.Contains(mntErr.Error(), sensitiveOptionsRemoved) {
267					t.Errorf("test \"%s\" returned an error unexpectedly containing the string %q: %v", test.description, sensitiveOptionsRemoved, err)
268				}
269			} else {
270				if !strings.Contains(err.Error(), sensitiveOptionsRemoved) {
271					t.Errorf("test \"%s\" returned an error without the string %q: %v", test.description, sensitiveOptionsRemoved, err)
272				}
273				for _, sensitiveOption := range test.sensitiveMountOptions {
274					if strings.Contains(err.Error(), sensitiveOption) {
275						t.Errorf("test \"%s\" returned an error with a sensitive string (%q): %v", test.description, sensitiveOption, err)
276					}
277				}
278			}
279		}
280	}
281}
282
283func makeFakeCmd(fakeCmd *testingexec.FakeCmd, cmd string, args ...string) testingexec.FakeCommandAction {
284	c := cmd
285	a := args
286	return func(cmd string, args ...string) exec.Cmd {
287		command := testingexec.InitFakeCmd(fakeCmd, c, a...)
288		return command
289	}
290}
291
292func makeFakeOutput(output string, err error) testingexec.FakeAction {
293	o := output
294	return func() ([]byte, []byte, error) {
295		return []byte(o), nil, err
296	}
297}
298