Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mount: unmount on sigint/sigterm, add --options and --mode, improve UI #3855

Merged
merged 9 commits into from
Mar 21, 2019
96 changes: 82 additions & 14 deletions cmd/minikube/cmd/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ package cmd
import (
"net"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"

"github.com/golang/glog"
"github.com/spf13/cobra"
Expand All @@ -34,16 +37,26 @@ import (
"k8s.io/minikube/third_party/go9p/ufs"
)

// nineP is the value of --type used for the 9p filesystem.
const nineP = "9p"

// placeholders for flag values
var mountIP string
var mountVersion string
var mountType string
var isKill bool
var uid int
var gid int
var msize int
var mSize int
var options []string
var mode uint

// supportedFilesystems is a map of filesystem types to not warn against.
var supportedFilesystems = map[string]bool{nineP: true}

// mountCmd represents the mount command
var mountCmd = &cobra.Command{
Use: "mount [flags] MOUNT_DIRECTORY(ex:\"/home\")",
Use: "mount [flags] <source directory>:<target directory>",
Short: "Mounts the specified directory into minikube",
Long: `Mounts the specified directory into minikube.`,
Run: func(cmd *cobra.Command, args []string) {
Expand All @@ -56,13 +69,12 @@ var mountCmd = &cobra.Command{

if len(args) != 1 {
exit.Usage(`Please specify the directory to be mounted:
minikube mount HOST_MOUNT_DIRECTORY:VM_MOUNT_DIRECTORY(ex:"/host-home:/vm-home")`)
minikube mount <source directory>:<target directory> (example: "/host-home:/vm-home")`)
}
mountString := args[0]
idx := strings.LastIndex(mountString, ":")
if idx == -1 { // no ":" was present
exit.Usage(`Mount directory must be in the form:
HOST_MOUNT_DIRECTORY:VM_MOUNT_DIRECTORY`)
exit.Usage(`mount argument %q must be in form: <source directory>:<target directory>`, mountString)
}
hostPath := mountString[:idx]
vmPath := mountString[idx+1:]
Expand All @@ -74,7 +86,7 @@ var mountCmd = &cobra.Command{
}
}
if len(vmPath) == 0 || !strings.HasPrefix(vmPath, "/") {
exit.Usage("The :VM_MOUNT_DIRECTORY must be an absolute path")
exit.Usage("Target directory %q must be an absolute path", vmPath)
}
var debugVal int
if glog.V(1) {
Expand Down Expand Up @@ -104,32 +116,88 @@ var mountCmd = &cobra.Command{
exit.WithCode(exit.Data, "error parsing the input ip address for mount")
}
}
console.OutStyle("mounting", "Mounting %s into %s on the minikube VM", hostPath, vmPath)
console.OutStyle("notice", "This daemon process needs to stay alive for the mount to be accessible ...")
port, err := cmdUtil.GetPort()
if err != nil {
exit.WithError("Error finding port for mount", err)
}

cfg := &cluster.MountConfig{
Type: mountType,
UID: uid,
GID: gid,
Version: mountVersion,
MSize: mSize,
Port: port,
Mode: os.FileMode(mode),
Options: map[string]string{},
}

for _, o := range options {
if !strings.Contains(o, "=") {
cfg.Options[o] = ""
continue
}
parts := strings.Split(o, "=")
cfg.Options[parts[0]] = parts[1]
}

console.OutStyle("mounting", "Mounting host path %s into VM as %s ...", hostPath, vmPath)
console.OutStyle("mount-options", "Mount options:")
console.OutStyle("option", "Type: %s", cfg.Type)
console.OutStyle("option", "UID: %d", cfg.UID)
console.OutStyle("option", "GID: %d", cfg.GID)
console.OutStyle("option", "Version: %s", cfg.Version)
console.OutStyle("option", "MSize: %d", cfg.MSize)
console.OutStyle("option", "Mode: %o (%s)", cfg.Mode, cfg.Mode)
console.OutStyle("option", "Options: %s", cfg.Options)

// An escape valve to allow future hackers to try NFS, VirtFS, or other FS types.
if !supportedFilesystems[cfg.Type] {
console.OutLn("")
console.OutStyle("warning", "%s is not yet a supported filesystem. We will try anyways!", cfg.Type)
}

var wg sync.WaitGroup
wg.Add(1)
if cfg.Type == nineP {
wg.Add(1)
go func() {
console.OutStyle("fileserver", "Userspace file server: ")
ufs.StartServer(net.JoinHostPort(ip.String(), strconv.Itoa(port)), debugVal, hostPath)
wg.Done()
}()
}

// Unmount if Ctrl-C or kill request is received.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
ufs.StartServer(net.JoinHostPort(ip.String(), port), debugVal, hostPath)
wg.Done()
for sig := range c {
console.OutStyle("unmount", "Unmounting %s ...", vmPath)
cluster.Unmount(host, vmPath)
exit.WithCode(exit.Interrupted, "Exiting due to %s signal", sig)
}
}()
err = cluster.MountHost(api, ip, vmPath, port, mountVersion, uid, gid, msize)

err = cluster.Mount(host, ip.String(), vmPath, cfg)
if err != nil {
exit.WithError("failed to mount host", err)
exit.WithError("mount failed", err)
}
console.OutStyle("success", "Successfully mounted %s to %s", hostPath, vmPath)
console.OutLn("")
console.OutStyle("notice", "NOTE: This process must stay alive for the mount to be accessible ...")
wg.Wait()
},
}

func init() {
mountCmd.Flags().StringVar(&mountIP, "ip", "", "Specify the ip that the mount should be setup on")
mountCmd.Flags().StringVar(&mountType, "type", nineP, "Specify the mount filesystem type (supported types: 9p)")
mountCmd.Flags().StringVar(&mountVersion, "9p-version", constants.DefaultMountVersion, "Specify the 9p version that the mount should use")
mountCmd.Flags().BoolVar(&isKill, "kill", false, "Kill the mount process spawned by minikube start")
mountCmd.Flags().IntVar(&uid, "uid", 1001, "Default user id used for the mount")
mountCmd.Flags().IntVar(&gid, "gid", 1001, "Default group id used for the mount")
mountCmd.Flags().IntVar(&msize, "msize", constants.DefaultMsize, "The number of bytes to use for 9p packet payload")
mountCmd.Flags().UintVar(&mode, "mode", 0755, "File permissions used for the mount")
mountCmd.Flags().StringSliceVar(&options, "options", []string{}, "Additional mount options, such as cache=fscache")
mountCmd.Flags().IntVar(&mSize, "msize", constants.DefaultMsize, "The number of bytes to use for 9p packet payload")
RootCmd.AddCommand(mountCmd)
}
6 changes: 3 additions & 3 deletions cmd/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@ import (
)

// Ask the kernel for a free open port that is ready to use
func GetPort() (string, error) {
func GetPort() (int, error) {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
panic(err)
}

l, err := net.ListenTCP("tcp", addr)
if err != nil {
return "", errors.Errorf("Error accessing port %d", addr.Port)
return -1, errors.Errorf("Error accessing port %d", addr.Port)
}
defer l.Close()
return strconv.Itoa(l.Addr().(*net.TCPAddr).Port), nil
return l.Addr().(*net.TCPAddr).Port, nil
}

func KillMountProcess() error {
Expand Down
61 changes: 0 additions & 61 deletions pkg/minikube/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@ limitations under the License.
package cluster

import (
"bytes"
"encoding/json"
"flag"
"fmt"
"html/template"
"net"
"os/exec"
"regexp"
Expand Down Expand Up @@ -338,30 +336,6 @@ func GetHostDockerEnv(api libmachine.API) (map[string]string, error) {
return envMap, nil
}

// MountHost runs the mount command from the 9p client on the VM to the 9p server on the host
func MountHost(api libmachine.API, ip net.IP, path, port, mountVersion string, uid, gid, msize int) error {
host, err := CheckIfHostExistsAndLoad(api, cfg.GetMachineName())
if err != nil {
return errors.Wrap(err, "Error checking that api exists and loading it")
}
if ip == nil {
ip, err = GetVMHostIP(host)
if err != nil {
return errors.Wrap(err, "Error getting the host IP address to use from within the VM")
}
}
host.RunSSHCommand(GetMountCleanupCommand(path))
mountCmd, err := GetMountCommand(ip, path, port, mountVersion, uid, gid, msize)
if err != nil {
return errors.Wrap(err, "mount command")
}
_, err = host.RunSSHCommand(mountCmd)
if err != nil {
return errors.Wrap(err, "running mount")
}
return nil
}

// GetVMHostIP gets the ip address to be used for mapping host -> VM and VM -> host
func GetVMHostIP(host *host.Host) (net.IP, error) {
switch host.DriverName {
Expand Down Expand Up @@ -461,38 +435,3 @@ func EnsureMinikubeRunningOrExit(api libmachine.API, exitStatus int) {
exit.WithCode(exit.Unavailable, "minikube is not running, so the service cannot be accessed")
}
}

func GetMountCleanupCommand(path string) string {
return fmt.Sprintf("sudo umount %s;", path)
}

var mountTemplate = `
sudo mkdir -p {{.Path}} || true;
sudo mount -t 9p -o trans=tcp,port={{.Port}},dfltuid={{.UID}},dfltgid={{.GID}},version={{.Version}},msize={{.Msize}} {{.IP}} {{.Path}};
sudo chmod 775 {{.Path}} || true;`

func GetMountCommand(ip net.IP, path, port, mountVersion string, uid, gid, msize int) (string, error) {
t := template.Must(template.New("mountCommand").Parse(mountTemplate))
buf := bytes.Buffer{}
data := struct {
IP string
Path string
Port string
Version string
UID int
GID int
Msize int
}{
IP: ip.String(),
Path: path,
Port: port,
Version: mountVersion,
UID: uid,
GID: gid,
Msize: msize,
}
if err := t.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
110 changes: 110 additions & 0 deletions pkg/minikube/cluster/mount.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
Copyright 2019 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 cluster

import (
"fmt"
"os"
"sort"
"strconv"
"strings"

"github.com/pkg/errors"
)

// MountConfig defines the options available to the Mount command
type MountConfig struct {
// Type is the filesystem type (Typically 9p)
Type string
// UID is the User ID which this path will be mounted as
UID int
// GID is the Group ID which this path will be mounted as
GID int
// Version is the 9P protocol version. Valid options: 9p2000, 9p200.u, 9p2000.L
Version string
// MSize is the number of bytes to use for 9p packet payload
MSize int
// Port is the port to connect to on the host
Port int
// Mode is the file permissions to set the mount to (octals)
Mode os.FileMode
// Extra mount options. See https://www.kernel.org/doc/Documentation/filesystems/9p.txt
Options map[string]string
}

// hostRunner is the subset of host.Host used for mounting
type hostRunner interface {
RunSSHCommand(cmd string) (string, error)
}

// Mount runs the mount command from the 9p client on the VM to the 9p server on the host
func Mount(h hostRunner, source string, target string, c *MountConfig) error {
if err := Unmount(h, target); err != nil {
return errors.Wrap(err, "umount")
}

cmd := fmt.Sprintf("sudo mkdir -m %o -p %s && %s", c.Mode, target, mntCmd(source, target, c))
out, err := h.RunSSHCommand(cmd)
if err != nil {
return errors.Wrap(err, out)
}
return nil
}

// mntCmd returns a mount command based on a config.
func mntCmd(source string, target string, c *MountConfig) string {
options := map[string]string{
"dfltgid": strconv.Itoa(c.GID),
"dfltuid": strconv.Itoa(c.UID),
}
if c.Port != 0 {
options["port"] = strconv.Itoa(c.Port)
}
if c.Version != "" {
options["version"] = c.Version
}
if c.MSize != 0 {
options["msize"] = strconv.Itoa(c.MSize)
}

// Copy in all of the user-supplied keys and values
for k, v := range c.Options {
options[k] = v
}

// Convert everything into a sorted list for better test results
opts := []string{}
for k, v := range options {
// Mount option with no value, such as "noextend"
if v == "" {
opts = append(opts, k)
continue
}
opts = append(opts, fmt.Sprintf("%s=%s", k, v))
}
sort.Strings(opts)
return fmt.Sprintf("sudo mount -t %s -o %s %s %s", c.Type, strings.Join(opts, ","), source, target)
}

// Unmount unmounts a path
func Unmount(h hostRunner, target string) error {
out, err := h.RunSSHCommand(fmt.Sprintf("findmnt -T %s && sudo umount %s || true", target, target))
if err != nil {
return errors.Wrap(err, out)
}
return nil
}
Loading