diff --git a/pkg/util/mount/mount.go b/pkg/util/mount/mount.go index 97d5f7d134c..83a8b0e7a60 100644 --- a/pkg/util/mount/mount.go +++ b/pkg/util/mount/mount.go @@ -18,7 +18,12 @@ limitations under the License. // an alternate platform, we will need to abstract further. package mount -import "github.com/golang/glog" +import ( + "strings" + + "github.com/golang/glog" + "k8s.io/kubernetes/pkg/util/exec" +) type Interface interface { // Mount mounts source to target as fstype with given options. @@ -44,6 +49,69 @@ type MountPoint struct { Pass int } +// SafeFormatAndMount probes a device to see if it is formatted. If +// so it mounts it otherwise it formats it and mounts it +type SafeFormatAndMount struct { + Interface + Runner exec.Interface +} + +// Mount mounts the given disk. If the disk is not formatted and the disk is not being mounted as read only +// it will format the disk first then mount it. +func (mounter *SafeFormatAndMount) Mount(source string, target string, fstype string, options []string) error { + // Don't attempt to format if mounting as readonly. Go straight to mounting. + for _, option := range options { + if option == "ro" { + return mounter.Interface.Mount(source, target, fstype, options) + } + } + return mounter.formatAndMount(source, target, fstype, options) +} + +// formatAndMount uses unix utils to format and mount the given disk +func (mounter *SafeFormatAndMount) formatAndMount(source string, target string, fstype string, options []string) error { + options = append(options, "defaults") + + // Try to mount the disk + err := mounter.Interface.Mount(source, target, fstype, options) + if err != nil { + // It is possible that this disk is not formatted. Double check using 'file' + notFormatted, err := mounter.diskLooksUnformatted(source) + if err == nil && notFormatted { + // Disk is unformatted so format it. + // Use 'ext4' as the default + if len(fstype) == 0 { + fstype = "ext4" + } + args := []string{"-E", "lazy_itable_init=0,lazy_journal_init=0", "-F", source} + cmd := mounter.Runner.Command("mkfs."+fstype, args...) + _, err := cmd.CombinedOutput() + if err == nil { + // the disk has been formatted sucessfully try to mount it again. + return mounter.Interface.Mount(source, target, fstype, options) + } + return err + } + } + return err +} + +// diskLooksUnformatted uses 'file' to see if the given disk is unformated +func (mounter *SafeFormatAndMount) diskLooksUnformatted(disk string) (bool, error) { + args := []string{"-L", "--special-files", disk} + cmd := mounter.Runner.Command("file", args...) + dataOut, err := cmd.CombinedOutput() + + // TODO (#13212): check if this disk has partitions and return false, and + // an error if so. + + if err != nil { + return false, err + } + + return !strings.Contains(string(dataOut), "filesystem"), nil +} + // New returns a mount.Interface for the current system. func New() Interface { return &Mounter{} diff --git a/pkg/util/mount/safe_format_and_mount_test.go b/pkg/util/mount/safe_format_and_mount_test.go new file mode 100644 index 00000000000..9833f7ba4f8 --- /dev/null +++ b/pkg/util/mount/safe_format_and_mount_test.go @@ -0,0 +1,172 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 mount + +import ( + "fmt" + "testing" + + "k8s.io/kubernetes/pkg/util/exec" +) + +type ErrorMounter struct { + *FakeMounter + errIndex int + err []error +} + +func (mounter *ErrorMounter) Mount(source string, target string, fstype string, options []string) error { + i := mounter.errIndex + mounter.errIndex++ + if mounter.err != nil && mounter.err[i] != nil { + return mounter.err[i] + } + return mounter.FakeMounter.Mount(source, target, fstype, options) +} + +type ExecArgs struct { + command string + args []string + output string + err error +} + +func TestSafeFormatAndMount(t *testing.T) { + tests := []struct { + fstype string + mountOptions []string + execScripts []ExecArgs + mountErrs []error + expectedError error + }{ + { // Test a read only mount + fstype: "ext4", + mountOptions: []string{"ro"}, + }, + { // Test a normal mount + fstype: "ext4", + }, + + { // Test that 'file' is called and fails + fstype: "ext4", + mountErrs: []error{fmt.Errorf("unknown filesystem type '(null)'")}, + execScripts: []ExecArgs{ + {"file", []string{"-L", "--special-files", "/dev/foo"}, "ext4 filesystem", nil}, + }, + expectedError: fmt.Errorf("unknown filesystem type '(null)'"), + }, + { // Test that 'file' is called and confirms unformatted disk, format fails + fstype: "ext4", + mountErrs: []error{fmt.Errorf("unknown filesystem type '(null)'")}, + execScripts: []ExecArgs{ + {"file", []string{"-L", "--special-files", "/dev/foo"}, "data", nil}, + {"mkfs.ext4", []string{"-E", "lazy_itable_init=0,lazy_journal_init=0", "-F", "/dev/foo"}, "", fmt.Errorf("formatting failed")}, + }, + expectedError: fmt.Errorf("formatting failed"), + }, + { // Test that 'file' is called and confirms unformatted disk, format passes, second mount fails + fstype: "ext4", + mountErrs: []error{fmt.Errorf("unknown filesystem type '(null)'"), fmt.Errorf("Still cannot mount")}, + execScripts: []ExecArgs{ + {"file", []string{"-L", "--special-files", "/dev/foo"}, "data", nil}, + {"mkfs.ext4", []string{"-E", "lazy_itable_init=0,lazy_journal_init=0", "-F", "/dev/foo"}, "", nil}, + }, + expectedError: fmt.Errorf("Still cannot mount"), + }, + { // Test that 'file' is called and confirms unformatted disk, format passes, second mount passes + fstype: "ext4", + mountErrs: []error{fmt.Errorf("unknown filesystem type '(null)'"), nil}, + execScripts: []ExecArgs{ + {"file", []string{"-L", "--special-files", "/dev/foo"}, "data", nil}, + {"mkfs.ext4", []string{"-E", "lazy_itable_init=0,lazy_journal_init=0", "-F", "/dev/foo"}, "", nil}, + }, + expectedError: nil, + }, + { // Test that 'file' is called and confirms unformatted disk, format passes, second mount passes with ext3 + fstype: "ext3", + mountErrs: []error{fmt.Errorf("unknown filesystem type '(null)'"), nil}, + execScripts: []ExecArgs{ + {"file", []string{"-L", "--special-files", "/dev/foo"}, "data", nil}, + {"mkfs.ext3", []string{"-E", "lazy_itable_init=0,lazy_journal_init=0", "-F", "/dev/foo"}, "", nil}, + }, + expectedError: nil, + }, + } + + for _, test := range tests { + commandScripts := []exec.FakeCommandAction{} + for _, expected := range test.execScripts { + ecmd := expected.command + eargs := expected.args + output := expected.output + err := expected.err + commandScript := func(cmd string, args ...string) exec.Cmd { + if cmd != ecmd { + t.Errorf("Unexpected command %s. Expecting %s", cmd, ecmd) + } + + for j := range args { + if args[j] != eargs[j] { + t.Errorf("Unexpected args %v. Expecting %v", args, eargs) + } + } + fake := exec.FakeCmd{ + CombinedOutputScript: []exec.FakeCombinedOutputAction{ + func() ([]byte, error) { return []byte(output), err }, + }, + } + return exec.InitFakeCmd(&fake, cmd, args...) + } + commandScripts = append(commandScripts, commandScript) + } + + fake := exec.FakeExec{ + CommandScript: commandScripts, + } + + fakeMounter := ErrorMounter{&FakeMounter{}, 0, test.mountErrs} + mounter := SafeFormatAndMount{ + Interface: &fakeMounter, + Runner: &fake, + } + + device := "/dev/foo" + dest := "/mnt/bar" + err := mounter.Mount(device, dest, test.fstype, test.mountOptions) + if test.expectedError == nil { + if err != nil { + t.Errorf("unexpected non-error: %v", err) + } + + // Check that something was mounted on the directory + isNotMountPoint, err := fakeMounter.IsLikelyNotMountPoint(dest) + if err != nil || isNotMountPoint { + t.Errorf("the directory was not mounted") + } + + //check that the correct device was mounted + mountedDevice, _, err := GetDeviceNameFromMount(fakeMounter.FakeMounter, dest) + if err != nil || mountedDevice != device { + t.Errorf("the correct device was not mounted") + } + } else { + if err == nil || test.expectedError.Error() != err.Error() { + t.Errorf("unexpected error: %v. Expecting %v", err, test.expectedError) + } + } + } +} diff --git a/pkg/volume/aws_ebs/aws_ebs.go b/pkg/volume/aws_ebs/aws_ebs.go index 1936bd88eec..1fbc8e6d8dd 100644 --- a/pkg/volume/aws_ebs/aws_ebs.go +++ b/pkg/volume/aws_ebs/aws_ebs.go @@ -106,7 +106,7 @@ func (plugin *awsElasticBlockStorePlugin) newBuilderInternal(spec *volume.Spec, fsType: fsType, partition: partition, readOnly: readOnly, - diskMounter: &awsSafeFormatAndMount{mounter, exec.New()}}, nil + diskMounter: &mount.SafeFormatAndMount{mounter, exec.New()}}, nil } func (plugin *awsElasticBlockStorePlugin) NewCleaner(volName string, podUID types.UID, mounter mount.Interface) (volume.Cleaner, error) { diff --git a/pkg/volume/aws_ebs/aws_util.go b/pkg/volume/aws_ebs/aws_util.go index 22647e8e5cc..6af88d1a1dc 100644 --- a/pkg/volume/aws_ebs/aws_util.go +++ b/pkg/volume/aws_ebs/aws_util.go @@ -18,13 +18,10 @@ package aws_ebs import ( "errors" - "fmt" "os" "time" "github.com/golang/glog" - "k8s.io/kubernetes/pkg/util/exec" - "k8s.io/kubernetes/pkg/util/mount" ) type AWSDiskUtil struct{} @@ -110,37 +107,3 @@ func (util *AWSDiskUtil) DetachDisk(c *awsElasticBlockStoreCleaner) error { } return nil } - -// safe_format_and_mount is a utility script on AWS VMs that probes a persistent disk, and if -// necessary formats it before mounting it. -// This eliminates the necessity to format a PD before it is used with a Pod on AWS. -// TODO: port this script into Go and use it for all Linux platforms -type awsSafeFormatAndMount struct { - mount.Interface - runner exec.Interface -} - -// uses /usr/share/google/safe_format_and_mount to optionally mount, and format a disk -func (mounter *awsSafeFormatAndMount) Mount(source string, target string, fstype string, options []string) error { - // Don't attempt to format if mounting as readonly. Go straight to mounting. - // Don't attempt to format if mounting as readonly. Go straight to mounting. - for _, option := range options { - if option == "ro" { - return mounter.Interface.Mount(source, target, fstype, options) - } - } - args := []string{} - // ext4 is the default for safe_format_and_mount - if len(fstype) > 0 && fstype != "ext4" { - args = append(args, "-m", fmt.Sprintf("mkfs.%s", fstype)) - } - args = append(args, options...) - args = append(args, source, target) - glog.V(5).Infof("exec-ing: /usr/share/google/safe_format_and_mount %v", args) - cmd := mounter.runner.Command("/usr/share/google/safe_format_and_mount", args...) - dataOut, err := cmd.CombinedOutput() - if err != nil { - glog.V(5).Infof("error running /usr/share/google/safe_format_and_mount\n%s", string(dataOut)) - } - return err -} diff --git a/pkg/volume/aws_ebs/aws_util_test.go b/pkg/volume/aws_ebs/aws_util_test.go deleted file mode 100644 index 45652a2438d..00000000000 --- a/pkg/volume/aws_ebs/aws_util_test.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2014 The Kubernetes Authors All rights reserved. - -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 aws_ebs - -import ( - "fmt" - "testing" - - "k8s.io/kubernetes/pkg/util/exec" -) - -func TestSafeFormatAndMount(t *testing.T) { - tests := []struct { - fstype string - expectedArgs []string - err error - }{ - { - fstype: "ext4", - expectedArgs: []string{"/dev/foo", "/mnt/bar"}, - }, - { - fstype: "vfat", - expectedArgs: []string{"-m", "mkfs.vfat", "/dev/foo", "/mnt/bar"}, - }, - { - err: fmt.Errorf("test error"), - }, - } - for _, test := range tests { - - var cmdOut string - var argsOut []string - fake := exec.FakeExec{ - CommandScript: []exec.FakeCommandAction{ - func(cmd string, args ...string) exec.Cmd { - cmdOut = cmd - argsOut = args - fake := exec.FakeCmd{ - CombinedOutputScript: []exec.FakeCombinedOutputAction{ - func() ([]byte, error) { return []byte{}, test.err }, - }, - } - return exec.InitFakeCmd(&fake, cmd, args...) - }, - }, - } - - mounter := awsSafeFormatAndMount{ - runner: &fake, - } - - err := mounter.Mount("/dev/foo", "/mnt/bar", test.fstype, nil) - if test.err == nil && err != nil { - t.Errorf("unexpected error: %v", err) - } - if test.err != nil { - if err == nil { - t.Errorf("unexpected non-error") - } - return - } - if cmdOut != "/usr/share/google/safe_format_and_mount" { - t.Errorf("unexpected command: %s", cmdOut) - } - if len(argsOut) != len(test.expectedArgs) { - t.Errorf("unexpected args: %v, expected: %v", argsOut, test.expectedArgs) - } - } -} diff --git a/pkg/volume/gce_pd/gce_pd.go b/pkg/volume/gce_pd/gce_pd.go index fb77cccb529..e01a8aa1396 100644 --- a/pkg/volume/gce_pd/gce_pd.go +++ b/pkg/volume/gce_pd/gce_pd.go @@ -103,7 +103,7 @@ func (plugin *gcePersistentDiskPlugin) newBuilderInternal(spec *volume.Spec, pod }, fsType: fsType, readOnly: readOnly, - diskMounter: &gceSafeFormatAndMount{mounter, exec.New()}}, nil + diskMounter: &mount.SafeFormatAndMount{mounter, exec.New()}}, nil } func (plugin *gcePersistentDiskPlugin) NewCleaner(volName string, podUID types.UID, mounter mount.Interface) (volume.Cleaner, error) { diff --git a/pkg/volume/gce_pd/gce_util.go b/pkg/volume/gce_pd/gce_util.go index 437107a9b10..261e1a04ca9 100644 --- a/pkg/volume/gce_pd/gce_util.go +++ b/pkg/volume/gce_pd/gce_util.go @@ -29,7 +29,6 @@ import ( "k8s.io/kubernetes/pkg/cloudprovider/providers/gce" "k8s.io/kubernetes/pkg/util" "k8s.io/kubernetes/pkg/util/exec" - "k8s.io/kubernetes/pkg/util/mount" "k8s.io/kubernetes/pkg/util/operationmanager" ) @@ -332,36 +331,3 @@ func udevadmChangeToDrive(drivePath string) error { } return nil } - -// safe_format_and_mount is a utility script on GCE VMs that probes a persistent disk, and if -// necessary formats it before mounting it. -// This eliminates the necesisty to format a PD before it is used with a Pod on GCE. -// TODO: port this script into Go and use it for all Linux platforms -type gceSafeFormatAndMount struct { - mount.Interface - runner exec.Interface -} - -// uses /usr/share/google/safe_format_and_mount to optionally mount, and format a disk -func (mounter *gceSafeFormatAndMount) Mount(source string, target string, fstype string, options []string) error { - // Don't attempt to format if mounting as readonly. Go straight to mounting. - for _, option := range options { - if option == "ro" { - return mounter.Interface.Mount(source, target, fstype, options) - } - } - args := []string{} - // ext4 is the default for safe_format_and_mount - if len(fstype) > 0 && fstype != "ext4" { - args = append(args, "-m", fmt.Sprintf("mkfs.%s", fstype)) - } - args = append(args, options...) - args = append(args, source, target) - glog.V(5).Infof("exec-ing: /usr/share/google/safe_format_and_mount %v", args) - cmd := mounter.runner.Command("/usr/share/google/safe_format_and_mount", args...) - dataOut, err := cmd.CombinedOutput() - if err != nil { - glog.Errorf("error running /usr/share/google/safe_format_and_mount\n%s", string(dataOut)) - } - return err -} diff --git a/pkg/volume/gce_pd/gce_util_test.go b/pkg/volume/gce_pd/gce_util_test.go deleted file mode 100644 index a7275ac3768..00000000000 --- a/pkg/volume/gce_pd/gce_util_test.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2014 The Kubernetes Authors All rights reserved. - -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 gce_pd - -import ( - "fmt" - "testing" - - "k8s.io/kubernetes/pkg/util/exec" -) - -func TestSafeFormatAndMount(t *testing.T) { - tests := []struct { - fstype string - expectedArgs []string - err error - }{ - { - fstype: "ext4", - expectedArgs: []string{"/dev/foo", "/mnt/bar"}, - }, - { - fstype: "vfat", - expectedArgs: []string{"-m", "mkfs.vfat", "/dev/foo", "/mnt/bar"}, - }, - { - err: fmt.Errorf("test error"), - }, - } - for _, test := range tests { - - var cmdOut string - var argsOut []string - fake := exec.FakeExec{ - CommandScript: []exec.FakeCommandAction{ - func(cmd string, args ...string) exec.Cmd { - cmdOut = cmd - argsOut = args - fake := exec.FakeCmd{ - CombinedOutputScript: []exec.FakeCombinedOutputAction{ - func() ([]byte, error) { return []byte{}, test.err }, - }, - } - return exec.InitFakeCmd(&fake, cmd, args...) - }, - }, - } - - mounter := gceSafeFormatAndMount{ - runner: &fake, - } - - err := mounter.Mount("/dev/foo", "/mnt/bar", test.fstype, nil) - if test.err == nil && err != nil { - t.Errorf("unexpected error: %v", err) - } - if test.err != nil { - if err == nil { - t.Errorf("unexpected non-error") - } - return - } - if cmdOut != "/usr/share/google/safe_format_and_mount" { - t.Errorf("unexpected command: %s", cmdOut) - } - if len(argsOut) != len(test.expectedArgs) { - t.Errorf("unexpected args: %v, expected: %v", argsOut, test.expectedArgs) - } - } -}