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