From 3d5d2088699d76b67dc96bdd55e42acf25c1c79c Mon Sep 17 00:00:00 2001 From: Hardy Ferentschik Date: Wed, 1 Nov 2017 11:12:25 +0100 Subject: [PATCH] Issue #317 Refactoring existing hostfolder code and implementing sshfs hostfolders - Add hidden sftpd command - Refactor existing hostfolder actions - Introducing hostfolder.Manager as entry point for hostfolder operations - Introducing HostFolder interface as common abstraction for any host folder type - Split impl details between CIFS and SSHFS - Allow to configure host folder interactively as well as non-interactively (Issue #959) - Adding docs --- Gopkg.lock | 82 +-- Gopkg.toml | 5 + cmd/minishift/cmd/config/config.go | 1 + cmd/minishift/cmd/hostfolder/add.go | 226 +++++++- cmd/minishift/cmd/hostfolder/add_test.go | 129 +++++ cmd/minishift/cmd/hostfolder/hostfolder.go | 6 +- cmd/minishift/cmd/hostfolder/list.go | 52 +- cmd/minishift/cmd/hostfolder/mount.go | 17 +- cmd/minishift/cmd/hostfolder/remove.go | 18 +- cmd/minishift/cmd/hostfolder/sftpd.go | 193 +++++++ cmd/minishift/cmd/hostfolder/umount.go | 22 +- cmd/minishift/cmd/hostfolder/util.go | 79 +++ cmd/minishift/cmd/hostfolder/util_test.go | 27 + cmd/minishift/cmd/root.go | 2 +- cmd/minishift/cmd/start.go | 14 +- docs/source/using/host-folders.adoc | 122 ++--- pkg/minishift/config/allinstancesconfig.go | 6 +- pkg/minishift/config/hostfolder.go | 52 -- pkg/minishift/config/hostfolder_test.go | 53 -- pkg/minishift/config/instanceconfig.go | 5 +- pkg/minishift/hostfolder/cifs_hostfolder.go | 184 +++++++ .../hostfolder/config/hostfolder_config.go | 24 + .../config/hostfolder_config_test.go | 41 ++ pkg/minishift/hostfolder/hostfolder.go | 482 +----------------- .../hostfolder/hostfolder_manager.go | 262 ++++++++++ pkg/minishift/hostfolder/hostfolder_test.go | 66 +-- pkg/minishift/hostfolder/sshfs_hostfolder.go | 182 +++++++ pkg/util/strings/strings.go | 4 + 28 files changed, 1553 insertions(+), 803 deletions(-) create mode 100644 cmd/minishift/cmd/hostfolder/add_test.go create mode 100644 cmd/minishift/cmd/hostfolder/sftpd.go create mode 100644 cmd/minishift/cmd/hostfolder/util.go create mode 100644 cmd/minishift/cmd/hostfolder/util_test.go delete mode 100644 pkg/minishift/config/hostfolder.go delete mode 100644 pkg/minishift/config/hostfolder_test.go create mode 100644 pkg/minishift/hostfolder/cifs_hostfolder.go create mode 100644 pkg/minishift/hostfolder/config/hostfolder_config.go create mode 100644 pkg/minishift/hostfolder/config/hostfolder_config_test.go create mode 100644 pkg/minishift/hostfolder/hostfolder_manager.go create mode 100644 pkg/minishift/hostfolder/sshfs_hostfolder.go diff --git a/Gopkg.lock b/Gopkg.lock index 0b7ad23ac8..0b9b0ab9c3 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -22,14 +22,14 @@ [[projects]] name = "github.com/Microsoft/go-winio" packages = [".","archive/tar","backuptar"] - revision = "78439966b38d69bf38227fbf57ac8a6fee70f69a" - version = "v0.4.5" + revision = "7da180ee92d8bd8bb8c37fc560e673e6557c392f" + version = "v0.4.7" [[projects]] name = "github.com/Microsoft/hcsshim" packages = ["."] - revision = "34a629f78a5d50f7de07727e41a948685c45e026" - version = "v0.6.7" + revision = "45ef15484298b76abeb9513ea0ea0abd2b5b84b3" + version = "v0.6.8" [[projects]] name = "github.com/asaskevich/govalidator" @@ -52,13 +52,13 @@ branch = "master" name = "github.com/containers/storage" packages = [".","drivers","drivers/aufs","drivers/btrfs","drivers/devmapper","drivers/overlay","drivers/overlayutils","drivers/quota","drivers/register","drivers/vfs","drivers/windows","drivers/zfs","pkg/archive","pkg/chrootarchive","pkg/devicemapper","pkg/directory","pkg/dmesg","pkg/fileutils","pkg/fsutils","pkg/homedir","pkg/idtools","pkg/ioutils","pkg/locker","pkg/longpath","pkg/loopback","pkg/mount","pkg/parsers","pkg/parsers/kernel","pkg/pools","pkg/promise","pkg/reexec","pkg/stringid","pkg/system","pkg/truncindex"] - revision = "46ef35348492d492e4671938a1993a315e4ad30f" + revision = "477e551dd493e5c80999d3690d3a201fd26ba2f1" [[projects]] name = "github.com/cpuguy83/go-md2man" packages = ["md2man"] - revision = "1d903dcb749992f3741d744c0f8376b4bd7eb3e1" - version = "v1.0.7" + revision = "20f5889cbdc3c73dbd2862796665e7c465ade7d1" + version = "v1.0.8" [[projects]] name = "github.com/davecgh/go-spew" @@ -113,8 +113,8 @@ [[projects]] name = "github.com/fsnotify/fsnotify" packages = ["."] - revision = "629574ca2a5df945712d3079857300b5e4da0236" - version = "v1.4.2" + revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" + version = "v1.4.7" [[projects]] name = "github.com/gbraad/go-hvkvp" @@ -131,8 +131,8 @@ [[projects]] name = "github.com/gogo/protobuf" packages = ["proto"] - revision = "342cbe0a04158f6dcb03ca0079991a51a4248c02" - version = "v0.5" + revision = "1adfc126b41513cc696b209667c8656ea7aac67c" + version = "v1.0.0" [[projects]] name = "github.com/golang/glog" @@ -141,10 +141,10 @@ source = "https://github.com/openshift/glog.git" [[projects]] - branch = "master" name = "github.com/golang/protobuf" packages = ["proto"] - revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845" + revision = "925541529c1fa6821df4e44ce2723319eb2be768" + version = "v1.0.0" [[projects]] name = "github.com/google/go-github" @@ -166,8 +166,8 @@ [[projects]] name = "github.com/gorilla/mux" packages = ["."] - revision = "7f08801859139f86dfafd1c296e2cba9a80d292e" - version = "v1.6.0" + revision = "53c1911da2b537f792e7cafcb446b05ffe33b996" + version = "v1.6.1" [[projects]] name = "github.com/gorillalabs/go-powershell" @@ -183,8 +183,8 @@ [[projects]] name = "github.com/imdario/mergo" packages = ["."] - revision = "7fe0c75c13abdee74b09fcacef5ea1c6bba6a874" - version = "0.2.4" + revision = "163f41321a19dd09362d4c63cc2489db2015f1f4" + version = "0.3.2" [[projects]] name = "github.com/inconshreveable/go-update" @@ -208,11 +208,17 @@ packages = ["."] revision = "ae77be60afb1dcacde03767a8c37337fad28ac14" +[[projects]] + branch = "master" + name = "github.com/kr/fs" + packages = ["."] + revision = "2788f0dbd16903de03cb8186e5c7d97b69ad387b" + [[projects]] name = "github.com/magiconair/properties" packages = ["."] - revision = "be5ece7dd465ab0765a9682137865547526d1dfb" - version = "v1.7.3" + revision = "c3beff4c2358b44d0493c7dda585e7db7ff28ae6" + version = "v1.7.6" [[projects]] name = "github.com/mattn/go-runewidth" @@ -236,7 +242,7 @@ branch = "master" name = "github.com/mitchellh/mapstructure" packages = ["."] - revision = "06020f85339e21b2478f756a78e295255ffa4d6a" + revision = "00c29f56e2386353d58c599509e8dc3801b0d716" [[projects]] branch = "master" @@ -287,8 +293,8 @@ [[projects]] name = "github.com/pelletier/go-toml" packages = ["."] - revision = "16398bac157da96aa88f98a2df640c7f32af1da2" - version = "v1.0.1" + revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8" + version = "v1.1.0" [[projects]] name = "github.com/pkg/browser" @@ -301,6 +307,12 @@ revision = "645ef00459ed84a119197bfb8d8205042c6df63d" version = "v0.8.0" +[[projects]] + name = "github.com/pkg/sftp" + packages = ["."] + revision = "0159c83e42a88e5c35180f73b84298a6bdde1ec1" + version = "1.4.0" + [[projects]] name = "github.com/pmezard/go-difflib" packages = ["difflib"] @@ -316,8 +328,8 @@ [[projects]] name = "github.com/russross/blackfriday" packages = ["."] - revision = "4048872b16cc0fc2c5fd9eacf0ed2c2fedaa0c8c" - version = "v1.5" + revision = "55d61fa8aa702f59229e6cff85793c22e580eaf5" + version = "v1.5.1" [[projects]] branch = "master" @@ -334,14 +346,14 @@ [[projects]] name = "github.com/spf13/afero" packages = [".","mem"] - revision = "8d919cbe7e2627e417f3e45c3c0e489a5b7e2536" - version = "v1.0.0" + revision = "bb8f1927f2a9d3ab41c9340aa034f6b803f4359c" + version = "v1.0.2" [[projects]] name = "github.com/spf13/cast" packages = ["."] - revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4" - version = "v1.1.0" + revision = "8965335b8c7107321228e3e3702cab9832751bac" + version = "v1.2.0" [[projects]] name = "github.com/spf13/cobra" @@ -352,7 +364,7 @@ branch = "master" name = "github.com/spf13/jwalterweatherman" packages = ["."] - revision = "12bd96e66386c1960ab0f74ced1362f66f552f7b" + revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394" [[projects]] name = "github.com/spf13/pflag" @@ -391,7 +403,7 @@ branch = "master" name = "golang.org/x/net" packages = ["context","context/ctxhttp","http2","http2/hpack","idna","lex/httplex","proxy"] - revision = "d866cfc389cec985d6fda2859936a575a55a3ab6" + revision = "cbe0f9307d0156177f9dd5dc85da1a31abc5f2fb" [[projects]] name = "golang.org/x/oauth2" @@ -404,10 +416,10 @@ revision = "43e60d72a8e2bd92ee98319ba9a384a0e9837c08" [[projects]] - branch = "master" name = "golang.org/x/text" packages = ["collate","collate/build","internal/colltab","internal/gen","internal/tag","internal/triegen","internal/ucd","language","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"] - revision = "3b24cac7bc3a458991ab409aa2a339ac9e0d60d6" + revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" + version = "v0.3.0" [[projects]] name = "google.golang.org/appengine" @@ -429,12 +441,12 @@ [[projects]] name = "k8s.io/client-go" packages = ["util/homedir"] - revision = "2ae454230481a7cb5544325e12ad7658ecccd19b" - version = "v5.0.1" + revision = "78700dec6369ba22221b72770783300f143df150" + version = "v6.0.0" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "aedc48e3f2b2aa6a4d8a06551c985859e75ee66a41a29ff5608f3378011f0fec" + inputs-digest = "56f18a532b86c7741de018c586555a7e21d193213d341c08dad3a3c296eaaa90" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 1d55ebda1e..e4f5478cba 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -174,3 +174,8 @@ ignored = ["github.com/Sirupsen/logrus"] [[constraint]] name = "github.com/stretchr/testify" version = "=v1.2.0" + +[[constraint]] + name = "github.com/pkg/sftp" + version = "1.0.0" + diff --git a/cmd/minishift/cmd/config/config.go b/cmd/minishift/cmd/config/config.go index edd45d25b7..4de5be4695 100644 --- a/cmd/minishift/cmd/config/config.go +++ b/cmd/minishift/cmd/config/config.go @@ -95,6 +95,7 @@ var ( // Host Folders HostFoldersMountPath = createConfigSetting("hostfolders-mountpath", SetString, nil, nil, true, nil) HostFoldersAutoMount = createConfigSetting("hostfolders-automount", SetBool, nil, nil, true, nil) + HostFoldersSftpPort = createConfigSetting("hostfolders-sftp-port", SetInt, []setFn{validations.IsPositive}, nil, true, nil) // Image caching ImageCaching = createConfigSetting("image-caching", SetBool, nil, nil, true, true) diff --git a/cmd/minishift/cmd/hostfolder/add.go b/cmd/minishift/cmd/hostfolder/add.go index fbb65d454e..e9435c43a6 100644 --- a/cmd/minishift/cmd/hostfolder/add.go +++ b/cmd/minishift/cmd/hostfolder/add.go @@ -20,45 +20,223 @@ import ( "fmt" "runtime" - hostfolderActions "github.com/minishift/minishift/pkg/minishift/hostfolder" + "errors" + miniConfig "github.com/minishift/minishift/pkg/minishift/config" + hf "github.com/minishift/minishift/pkg/minishift/hostfolder" + "github.com/minishift/minishift/pkg/minishift/hostfolder/config" + "github.com/minishift/minishift/pkg/util" "github.com/minishift/minishift/pkg/util/os/atexit" + "github.com/minishift/minishift/pkg/util/strings" "github.com/spf13/cobra" ) +const ( + usage = "Usage: minishift hostfolder add --type TYPE --source SOURCE --target TARGET HOST_FOLDER_NAME" + noSource = "you need to specify the source of the host folder" + noTarget = "you need to specify the target of the host folder" + noUserName = "you need to specify a username" + noPassword = "you need to specify a password" + noDomain = "you need to specify the Windows domain" + unknownType = "'%s' is an unknown host folder type" + nonSupportedTtyError = "not a tty supported terminal" + shareTypeFlag = "type" + sourceFlag = "source" + targetFlag = "target" + optionsFlag = "options" + interactiveFlag = "interactive" + instanceOnlyFlag = "instance-only" + usersShareFlag = "users-share" +) + var ( instanceOnly bool usersShare bool + interactive bool + shareType string + source string + target string + options string ) -var hostfolderAddCmd = &cobra.Command{ - Use: "add HOSTFOLDER_NAME", - Short: "Adds a host folder definition.", - Long: `Adds a host folder definition. The defined host folder can be mounted to a running OpenShift cluster.`, - Run: func(cmd *cobra.Command, args []string) { +var addCmd = &cobra.Command{ + Use: "add HOST_FOLDER_NAME", + Short: "Adds a host folder config.", + Long: `Adds a host folder config. The defined host folder can be mounted into the Minishift VM file system.`, + Run: addHostFolder, +} - var err error = nil - if usersShare && runtime.GOOS == "windows" { - // Windows-only (CIFS), all instances - err = hostfolderActions.SetupUsers(true) - } else { - if len(args) < 1 { - atexit.ExitWithMessage(1, "Usage: minishift hostfolder add HOSTFOLDER_NAME") - } - err = hostfolderActions.Add(args[0], !instanceOnly) +func init() { + HostFolderCmd.AddCommand(addCmd) + addCmd.Flags().StringVarP(&shareType, shareTypeFlag, "t", "cifs", "The host folder type.") + addCmd.Flags().StringVar(&source, sourceFlag, "", "The source of the host folder.") + addCmd.Flags().StringVar(&target, targetFlag, "", "The target (mount point) of the host folder.") + addCmd.Flags().StringVar(&options, optionsFlag, "", "Host folder type specific options.") + addCmd.Flags().BoolVar(&instanceOnly, instanceOnlyFlag, false, "Defines the host folder only for the current Minishift instance.") + addCmd.Flags().BoolVarP(&interactive, interactiveFlag, "i", false, "Allows to interactively provide the required parameters.") + + // Windows-only + if runtime.GOOS == "windows" { + addCmd.Flags().BoolVar(&usersShare, usersShareFlag, false, "Defines the shared Users folder as the host folder on a Windows host.") + } +} + +func addHostFolder(cmd *cobra.Command, args []string) { + hostFolderManager := getHostFolderManager() + + var name string + if usersShare && runtime.GOOS == "windows" { + // Windows-only (CIFS), all instances + name = "Users" + } else { + if len(args) < 1 { + atexit.ExitWithMessage(1, usage) } + name = args[0] + } + + if hostFolderManager.Exist(name) { + atexit.ExitWithMessage(1, fmt.Sprintf("there is already a host folder with the name '%s' defined", name)) + } - if err != nil { - atexit.ExitWithMessage(1, fmt.Sprintf("Failed to mount host folder: %s", err.Error())) + switch shareType { + case hf.CIFS.String(): + if interactive { + addCIFSInteractive(hostFolderManager, name) + } else { + addCIFSNonInteractive(hostFolderManager, name) + } + case hf.SSHFS.String(): + if interactive { + addSSHFSInteractive(hostFolderManager, name) + } else { + addSSHFSNonInteractive(hostFolderManager, name) } - }, + default: + atexit.ExitWithMessage(1, fmt.Sprintf(unknownType, shareType)) + } } -func init() { - HostfolderCmd.AddCommand(hostfolderAddCmd) - hostfolderAddCmd.Flags().BoolVarP(&instanceOnly, "instance-only", "", false, "Defines the host folder only for the current OpenShift cluster.") +func addSSHFSInteractive(manager *hf.Manager, name string) error { + source := util.ReadInputFromStdin("source path") + mountPath := readInputForMountPoint(name) - // Windows-only - if runtime.GOOS == "windows" { - hostfolderAddCmd.Flags().BoolVarP(&usersShare, "users-share", "", false, "Defines the shared Users folder as the host folder on a Windows host.") + config := config.HostFolderConfig{ + Name: name, + Type: hf.SSHFS.String(), + Options: map[string]string{ + config.Source: source, + config.MountPoint: mountPath, + }, + } + hostFolder := hf.NewSSHFSHostFolder(config, miniConfig.AllInstancesConfig) + manager.Add(hostFolder, !instanceOnly) + + return nil +} + +func addSSHFSNonInteractive(manager *hf.Manager, name string) error { + if source == "" { + atexit.ExitWithMessage(1, noSource) + } + + if target == "" { + atexit.ExitWithMessage(1, noTarget) + } + + config := config.HostFolderConfig{ + Name: name, + Type: hf.SSHFS.String(), + Options: map[string]string{ + config.Source: source, + config.MountPoint: target, + }, + } + hostFolder := hf.NewSSHFSHostFolder(config, miniConfig.AllInstancesConfig) + manager.Add(hostFolder, !instanceOnly) + + return nil +} + +func addCIFSInteractive(manager *hf.Manager, name string) error { + var uncPath string + if usersShare { + uncPath = "[determined on startup]" + } else { + uncPath = util.ReadInputFromStdin("UNC path") + } + + if len(uncPath) == 0 { + return errors.New("no remote path has been specified") + } + + mountPoint := readInputForMountPoint(name) + + username := util.ReadInputFromStdin("Username") + if !util.IsTtySupported() { + return fmt.Errorf(nonSupportedTtyError) + } + + password := util.ReadPasswordFromStdin("Password") + password, err := util.EncryptText(password) + if err != nil { + return err + } + + domain := util.ReadInputFromStdin("Domain") + + config := config.HostFolderConfig{ + Name: name, + Type: hf.CIFS.String(), + Options: map[string]string{ + config.MountPoint: mountPoint, + config.UncPath: strings.ConvertSlashes(uncPath), + config.UserName: username, + config.Password: password, + config.Domain: domain, + }, + } + hostFolder := hf.NewCifsHostFolder(config) + manager.Add(hostFolder, !instanceOnly) + + return nil +} + +func addCIFSNonInteractive(manager *hf.Manager, name string) { + if source == "" { + atexit.ExitWithMessage(1, noSource) + } + + if target == "" { + atexit.ExitWithMessage(1, noTarget) + } + + optionsMap := getOptions(options) + + if _, ok := optionsMap[config.UserName]; !ok { + atexit.ExitWithMessage(1, noUserName) + } + + if _, ok := optionsMap[config.Password]; !ok { + atexit.ExitWithMessage(1, noPassword) + } + + var domain string + var ok bool + if domain, ok = optionsMap[config.Domain]; !ok { + domain = "" + } + + config := config.HostFolderConfig{ + Name: name, + Type: hf.CIFS.String(), + Options: map[string]string{ + config.UncPath: strings.ConvertSlashes(source), + config.MountPoint: target, + config.UserName: optionsMap[config.UserName], + config.Password: optionsMap[config.Password], + config.Domain: domain, + }, } + hostFolder := hf.NewCifsHostFolder(config) + manager.Add(hostFolder, !instanceOnly) } diff --git a/cmd/minishift/cmd/hostfolder/add_test.go b/cmd/minishift/cmd/hostfolder/add_test.go new file mode 100644 index 0000000000..49d75b4112 --- /dev/null +++ b/cmd/minishift/cmd/hostfolder/add_test.go @@ -0,0 +1,129 @@ +package hostfolder + +import ( + "fmt" + "github.com/minishift/minishift/cmd/testing/cli" + "github.com/minishift/minishift/pkg/minishift/config" + "github.com/minishift/minishift/pkg/util/os/atexit" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "path/filepath" + "testing" +) + +func Test_host_folder_name_required(t *testing.T) { + var err error + tmpMinishiftHomeDir := cli.SetupTmpMinishiftHome(t) + config.InstanceConfig, err = config.NewInstanceConfig(filepath.Join(tmpMinishiftHomeDir, "config")) + config.AllInstancesConfig, err = config.NewAllInstancesConfig(filepath.Join(tmpMinishiftHomeDir, "config")) + assert.NoError(t, err, "Unexpected error setting instance config") + + tee := cli.CreateTee(t, true) + defer cli.TearDown(tmpMinishiftHomeDir, tee) + defer viper.Reset() + + atexit.RegisterExitHandler(cli.VerifyExitCodeAndMessage(t, tee, 1, usage)) + addHostFolder(nil, nil) +} + +func Test_source_required(t *testing.T) { + var err error + tmpMinishiftHomeDir := cli.SetupTmpMinishiftHome(t) + config.InstanceConfig, err = config.NewInstanceConfig(filepath.Join(tmpMinishiftHomeDir, "config")) + config.AllInstancesConfig, err = config.NewAllInstancesConfig(filepath.Join(tmpMinishiftHomeDir, "config")) + assert.NoError(t, err, "Unexpected error setting instance config") + + tee := cli.CreateTee(t, true) + defer cli.TearDown(tmpMinishiftHomeDir, tee) + defer viper.Reset() + + atexit.RegisterExitHandler(cli.VerifyExitCodeAndMessage(t, tee, 1, noSource)) + addHostFolder(nil, []string{"foo"}) +} + +func Test_target_required(t *testing.T) { + var err error + tmpMinishiftHomeDir := cli.SetupTmpMinishiftHome(t) + config.InstanceConfig, err = config.NewInstanceConfig(filepath.Join(tmpMinishiftHomeDir, "config")) + config.AllInstancesConfig, err = config.NewAllInstancesConfig(filepath.Join(tmpMinishiftHomeDir, "config")) + assert.NoError(t, err, "Unexpected error setting instance config") + + tee := cli.CreateTee(t, true) + defer cli.TearDown(tmpMinishiftHomeDir, tee) + defer viper.Reset() + + atexit.RegisterExitHandler(cli.VerifyExitCodeAndMessage(t, tee, 1, noTarget)) + source = "/home/johndoe" + addHostFolder(nil, []string{"foo"}) +} + +func Test_username_required(t *testing.T) { + var err error + tmpMinishiftHomeDir := cli.SetupTmpMinishiftHome(t) + config.InstanceConfig, err = config.NewInstanceConfig(filepath.Join(tmpMinishiftHomeDir, "config")) + config.AllInstancesConfig, err = config.NewAllInstancesConfig(filepath.Join(tmpMinishiftHomeDir, "config")) + assert.NoError(t, err, "Unexpected error setting instance config") + + tee := cli.CreateTee(t, true) + defer cli.TearDown(tmpMinishiftHomeDir, tee) + defer viper.Reset() + + atexit.RegisterExitHandler(cli.VerifyExitCodeAndMessage(t, tee, 1, noUserName)) + source = "/home/johndoe" + target = "/var/tmp" + addHostFolder(nil, []string{"foo"}) +} + +func Test_password_required(t *testing.T) { + var err error + tmpMinishiftHomeDir := cli.SetupTmpMinishiftHome(t) + config.InstanceConfig, err = config.NewInstanceConfig(filepath.Join(tmpMinishiftHomeDir, "config")) + config.AllInstancesConfig, err = config.NewAllInstancesConfig(filepath.Join(tmpMinishiftHomeDir, "config")) + assert.NoError(t, err, "Unexpected error setting instance config") + + tee := cli.CreateTee(t, true) + defer cli.TearDown(tmpMinishiftHomeDir, tee) + defer viper.Reset() + + atexit.RegisterExitHandler(cli.VerifyExitCodeAndMessage(t, tee, 1, noPassword)) + source = "/home/johndoe" + target = "/var/tmp" + options = "username=johndoe" + addHostFolder(nil, []string{"foo"}) +} + +func Test_domain_required(t *testing.T) { + var err error + tmpMinishiftHomeDir := cli.SetupTmpMinishiftHome(t) + config.InstanceConfig, err = config.NewInstanceConfig(filepath.Join(tmpMinishiftHomeDir, "config")) + config.AllInstancesConfig, err = config.NewAllInstancesConfig(filepath.Join(tmpMinishiftHomeDir, "config")) + assert.NoError(t, err, "Unexpected error setting instance config") + + tee := cli.CreateTee(t, true) + defer cli.TearDown(tmpMinishiftHomeDir, tee) + defer viper.Reset() + + atexit.RegisterExitHandler(cli.VerifyExitCodeAndMessage(t, tee, 1, noDomain)) + source = "/home/johndoe" + target = "/var/tmp" + options = "username=johndoe,password==,123" + addHostFolder(nil, []string{"foo"}) +} + +func Test_unknown_host_folder_type_exits(t *testing.T) { + var err error + tmpMinishiftHomeDir := cli.SetupTmpMinishiftHome(t) + config.InstanceConfig, err = config.NewInstanceConfig(filepath.Join(tmpMinishiftHomeDir, "config")) + config.AllInstancesConfig, err = config.NewAllInstancesConfig(filepath.Join(tmpMinishiftHomeDir, "config")) + assert.NoError(t, err, "Unexpected error setting instance config") + + tee := cli.CreateTee(t, true) + defer cli.TearDown(tmpMinishiftHomeDir, tee) + defer viper.Reset() + + atexit.RegisterExitHandler(cli.VerifyExitCodeAndMessage(t, tee, 1, fmt.Sprintf(unknownType, "snafu"))) + source = "/home/johndoe" + target = "/var/tmp" + shareType = "snafu" + addHostFolder(nil, []string{"foo"}) +} diff --git a/cmd/minishift/cmd/hostfolder/hostfolder.go b/cmd/minishift/cmd/hostfolder/hostfolder.go index 6e6492d715..9cfeabb305 100644 --- a/cmd/minishift/cmd/hostfolder/hostfolder.go +++ b/cmd/minishift/cmd/hostfolder/hostfolder.go @@ -20,10 +20,10 @@ import ( "github.com/spf13/cobra" ) -var HostfolderCmd = &cobra.Command{ +var HostFolderCmd = &cobra.Command{ Use: "hostfolder SUBCOMMAND [flags]", - Short: "Manages host folders for the OpenShift cluster.", - Long: `Manages host folders for the OpenShift cluster. Use the sub-commands to define, mount, unmount, and list host folders.`, + Short: "Manages host folders for the Minishift VM.", + Long: `Manages host folders for the Minishift VM. Use the sub-commands to define, mount, umount, and list host folders.`, Run: func(cmd *cobra.Command, args []string) { cmd.Help() }, diff --git a/cmd/minishift/cmd/hostfolder/list.go b/cmd/minishift/cmd/hostfolder/list.go index 02b1a574b1..db79f4e82d 100644 --- a/cmd/minishift/cmd/hostfolder/list.go +++ b/cmd/minishift/cmd/hostfolder/list.go @@ -17,38 +17,68 @@ limitations under the License. package hostfolder import ( + "fmt" "github.com/docker/machine/libmachine" + "github.com/docker/machine/libmachine/drivers" "github.com/minishift/minishift/cmd/minishift/cmd/util" "github.com/minishift/minishift/cmd/minishift/state" "github.com/minishift/minishift/pkg/minikube/constants" - hostfolderActions "github.com/minishift/minishift/pkg/minishift/hostfolder" "github.com/minishift/minishift/pkg/util/os/atexit" "github.com/spf13/cobra" + "os" + "text/tabwriter" ) -var hostfolderListCmd = &cobra.Command{ +var listCmd = &cobra.Command{ Use: "list", Short: "Lists the defined host folders.", - Long: `Lists an overview of the defined host folders that can be mounted to a running OpenShift cluster.`, + Long: `Lists an overview of the defined host folders that can be mounted into the Minishift VM.`, Run: func(cmd *cobra.Command, args []string) { + hostFolderManager := getHostFolderManager() + api := libmachine.NewClient(state.InstanceDirs.Home, state.InstanceDirs.Certs) defer api.Close() - util.ExitIfUndefined(api, constants.MachineName) - - host, err := api.Load(constants.MachineName) + mountInfos, err := hostFolderManager.List(getDriver(api)) if err != nil { atexit.ExitWithMessage(1, err.Error()) } - isRunning := util.IsHostRunning(host.Driver) - err = hostfolderActions.List(host.Driver, isRunning) - if err != nil { - atexit.ExitWithMessage(1, err.Error()) + w := tabwriter.NewWriter(os.Stdout, 4, 8, 3, ' ', 0) + fmt.Fprintln(w, "Name\tType\tSource\tMountpoint\tMounted") + + for _, info := range mountInfos { + mounted := "N" + if info.Mounted { + mounted = "Y" + } + + fmt.Fprintln(w, + fmt.Sprintf("%s\t%s\t%s\t%s\t%s", + info.Name, + info.Type, + info.Source, + info.MountPoint, + mounted)) } + + w.Flush() }, } +func getDriver(api *libmachine.Client) drivers.Driver { + if !util.VMExists(api, constants.MachineName) { + return nil + } + + host, err := api.Load(constants.MachineName) + if err != nil { + return nil + } + + return host.Driver +} + func init() { - HostfolderCmd.AddCommand(hostfolderListCmd) + HostFolderCmd.AddCommand(listCmd) } diff --git a/cmd/minishift/cmd/hostfolder/mount.go b/cmd/minishift/cmd/hostfolder/mount.go index 5f4e487d45..64ccf8ed21 100644 --- a/cmd/minishift/cmd/hostfolder/mount.go +++ b/cmd/minishift/cmd/hostfolder/mount.go @@ -23,7 +23,6 @@ import ( "github.com/minishift/minishift/cmd/minishift/cmd/util" "github.com/minishift/minishift/cmd/minishift/state" "github.com/minishift/minishift/pkg/minikube/constants" - hostfolderActions "github.com/minishift/minishift/pkg/minishift/hostfolder" "github.com/minishift/minishift/pkg/util/os/atexit" "github.com/spf13/cobra" ) @@ -32,8 +31,8 @@ var ( mountAll bool ) -var hostfolderMountCmd = &cobra.Command{ - Use: "mount HOSTFOLDER_NAME", +var mountCmd = &cobra.Command{ + Use: "mount HOST_FOLDER_NAME", Short: "Mounts the specified host folder to the running OpenShift cluster.", Long: `Mounts the specified host folder to the running OpenShift cluster. You can set the 'all' flag to mount all of the defined host folders.`, Run: func(cmd *cobra.Command, args []string) { @@ -49,14 +48,16 @@ var hostfolderMountCmd = &cobra.Command{ util.ExitIfNotRunning(host.Driver, constants.MachineName) + hostFolderManager := getHostFolderManager() err = nil if mountAll { - err = hostfolderActions.MountHostfolders(host.Driver) + fmt.Println("-- Mounting host folders") + err = hostFolderManager.MountAll(host.Driver) } else { if len(args) < 1 { - atexit.ExitWithMessage(1, "Usage: minishift hostfolder mount [HOSTFOLDER_NAME|--all]") + atexit.ExitWithMessage(1, "Usage: minishift hostfolder mount [HOST_FOLDER_NAME|--all]") } - err = hostfolderActions.Mount(host.Driver, args[0]) + err = hostFolderManager.Mount(host.Driver, args[0]) } if err != nil { @@ -67,6 +68,6 @@ var hostfolderMountCmd = &cobra.Command{ } func init() { - HostfolderCmd.AddCommand(hostfolderMountCmd) - hostfolderMountCmd.Flags().BoolVarP(&mountAll, "all", "a", false, "Mounts all defined host folders to the running OpenShift cluster.") + HostFolderCmd.AddCommand(mountCmd) + mountCmd.Flags().BoolVarP(&mountAll, "all", "a", false, "Mounts all defined host folders into the Minishift VM.") } diff --git a/cmd/minishift/cmd/hostfolder/remove.go b/cmd/minishift/cmd/hostfolder/remove.go index 6e7c95b131..7af0ee3893 100644 --- a/cmd/minishift/cmd/hostfolder/remove.go +++ b/cmd/minishift/cmd/hostfolder/remove.go @@ -17,21 +17,23 @@ limitations under the License. package hostfolder import ( - hostfolderActions "github.com/minishift/minishift/pkg/minishift/hostfolder" "github.com/minishift/minishift/pkg/util/os/atexit" "github.com/spf13/cobra" ) -var hostfolderRemoveCmd = &cobra.Command{ - Use: "remove HOSTFOLDER_NAME", - Short: "Removes the specified host folder definition.", - Long: `Removes the specified host folder definition. This command does not remove the host folder or any data.`, +var removeCmd = &cobra.Command{ + Use: "remove HOST_FOLDER_NAME", + Short: "Removes the specified host folder config.", + Long: `Removes the specified host folder config. This command does not remove the host folder or any data.`, Run: func(cmd *cobra.Command, args []string) { if len(args) < 1 { - atexit.ExitWithMessage(1, "Usage: minishift hostfolder remove HOSTFOLDER_NAME") + atexit.ExitWithMessage(1, "Usage: minishift hostfolder remove HOST_FOLDER_NAME") } - err := hostfolderActions.Remove(args[0]) + hostFolderManager := getHostFolderManager() + + name := args[0] + err := hostFolderManager.Remove(name) if err != nil { atexit.ExitWithMessage(1, err.Error()) } @@ -39,5 +41,5 @@ var hostfolderRemoveCmd = &cobra.Command{ } func init() { - HostfolderCmd.AddCommand(hostfolderRemoveCmd) + HostFolderCmd.AddCommand(removeCmd) } diff --git a/cmd/minishift/cmd/hostfolder/sftpd.go b/cmd/minishift/cmd/hostfolder/sftpd.go new file mode 100644 index 0000000000..3e73f8e94d --- /dev/null +++ b/cmd/minishift/cmd/hostfolder/sftpd.go @@ -0,0 +1,193 @@ +/* +Copyright (C) 2017 Red Hat, Inc. + +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 hostfolder + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "github.com/golang/glog" + "github.com/minishift/minishift/cmd/minishift/cmd/config" + "github.com/minishift/minishift/pkg/util/os/atexit" + "github.com/pkg/sftp" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "golang.org/x/crypto/ssh" + "io" + "net" + "os" +) + +const ( + serverPortFlag = "port" +) + +var ( + serverPort int + + hostFolderSSHDCmd = &cobra.Command{ + Use: "sftpd", + Short: "Starts sftp server on host for sshfs based host folders.", + Long: `Starts sftp server on host for sshfs based host folders.`, + Run: runSftp, + Hidden: true, + } +) + +func init() { + hostFolderSSHDCmd.Flags().IntVarP(&serverPort, serverPortFlag, "p", 2022, "The server port.") + HostFolderCmd.AddCommand(hostFolderSSHDCmd) +} + +func runSftp(cmd *cobra.Command, args []string) { + serverConfig := serverConfig() + + port := viper.GetInt(config.HostFoldersSftpPort.Name) + if port == 0 { + port = serverPort + } + + // Once a ServerConfig has been configured, connections can be accepted. + listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", port)) + if err != nil { + glog.Fatal("failed to listen for connection", err) + } + glog.Infof("listening on %v", listener.Addr()) + + serveConnections(listener, serverConfig) +} + +func serveConnections(listener net.Listener, serverConfig *ssh.ServerConfig) { + for { + nConn, err := listener.Accept() + if err != nil { + glog.Fatal("failed to accept incoming connection", err) + } + + // Before use, a handshake must be performed on the incoming + // net.Conn. + _, channels, requests, err := ssh.NewServerConn(nConn, serverConfig) + if err != nil { + glog.Fatal("failed to handshake", err) + } + glog.Info("SSH server established") + + // The incoming Request channel must be serviced. + go ssh.DiscardRequests(requests) + + // Service the incoming Channel channel. + for newChannel := range channels { + // Channels have a type, depending on the application level + // protocol intended. In the case of an SFTP session, this is "subsystem" + // with a payload string of "sftp" + glog.Infof("Incoming channel: %s", newChannel.ChannelType()) + if newChannel.ChannelType() != "session" { + newChannel.Reject(ssh.UnknownChannelType, "unknown channel type") + continue + } + channel, requests, err := newChannel.Accept() + if err != nil { + glog.Fatal("could not accept channel.", err) + } + + // Sessions have out-of-band requests such as "shell", + // "pty-req" and "env". Here we handle only the + // "subsystem" request. + go func(in <-chan *ssh.Request) { + for req := range in { + glog.Infof("Request: %v", req.Type) + ok := false + switch req.Type { + case "subsystem": + glog.Infof("Subsystem: %s", req.Payload[4:]) + if string(req.Payload[4:]) == "sftp" { + ok = true + } + } + req.Reply(ok, nil) + } + }(requests) + + serverOptions := []sftp.ServerOption{ + sftp.WithDebug(os.Stderr), + } + server, err := sftp.NewServer( + channel, + serverOptions..., + ) + if err != nil { + glog.Fatal(err) + } + if err := server.Serve(); err == io.EOF { + server.Close() + glog.Info("sftp client exited session.") + } else if err != nil { + glog.Fatal("sftp server completed with error:", err) + } + } + } +} + +func serverConfig() *ssh.ServerConfig { + // An SSH server is represented by a ServerConfig, which holds certificate details and handles authentication + config := ssh.ServerConfig{ + Config: ssh.Config{ + MACs: []string{"hmac-sha1"}, + }, + PublicKeyCallback: keyAuth, + } + + data, err := createPrivateKey() + if err != nil { + atexit.ExitWithMessage(1, fmt.Sprintf("Unable to create private key: %s", err)) + } + hostPrivateKeySigner, err := ssh.ParsePrivateKey(data) + if err != nil { + atexit.ExitWithMessage(1, fmt.Sprintf("Unable to parse private key: %s", err)) + } + config.AddHostKey(hostPrivateKeySigner) + return &config +} + +func keyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { + // TODO Issue #317 Do proper key based authentication + // See also https://github.com/golang/crypto/blob/master/ssh/example_test.go + // As part of the key generation w/i the VM we create a public key which we need to store in a authorized_keys file + // for all profiles + permissions := &ssh.Permissions{ + CriticalOptions: map[string]string{}, + Extensions: map[string]string{}, + } + return permissions, nil +} + +func createPrivateKey() ([]byte, error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + pem := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }, + ) + return pem, nil +} diff --git a/cmd/minishift/cmd/hostfolder/umount.go b/cmd/minishift/cmd/hostfolder/umount.go index 26557d04cd..0cd9527057 100644 --- a/cmd/minishift/cmd/hostfolder/umount.go +++ b/cmd/minishift/cmd/hostfolder/umount.go @@ -17,24 +17,21 @@ limitations under the License. package hostfolder import ( - "fmt" - "github.com/docker/machine/libmachine" "github.com/minishift/minishift/cmd/minishift/cmd/util" "github.com/minishift/minishift/cmd/minishift/state" "github.com/minishift/minishift/pkg/minikube/constants" - hostfolderActions "github.com/minishift/minishift/pkg/minishift/hostfolder" "github.com/minishift/minishift/pkg/util/os/atexit" "github.com/spf13/cobra" ) -var hostfolderUmountCmd = &cobra.Command{ - Use: "umount HOSTFOLDER_NAME", - Short: "Unmount a host folder from the running OpenShift cluster.", - Long: `Unmount a host folder from the running OpenShift cluster. This command does not remove the host folder definition or the host folder itself.`, +var umountCmd = &cobra.Command{ + Use: "umount HOST_FOLDER_NAME", + Short: "Umount a host folder from the running Minishift VM.", + Long: `Umount a host folder from the running Minishift VM. This command does not remove the host folder config or the host folder itself.`, Run: func(cmd *cobra.Command, args []string) { if len(args) < 1 { - atexit.ExitWithMessage(1, "Usage: minishift hostfolder umount HOSTFOLDER_NAME") + atexit.ExitWithMessage(1, "Usage: minishift hostfolder umount HOST_FOLDER_NAME") } api := libmachine.NewClient(state.InstanceDirs.Home, state.InstanceDirs.Certs) @@ -49,13 +46,16 @@ var hostfolderUmountCmd = &cobra.Command{ util.ExitIfNotRunning(host.Driver, constants.MachineName) - err = hostfolderActions.Umount(host.Driver, args[0]) + hostFolderManager := getHostFolderManager() + + name := args[0] + err = hostFolderManager.Umount(host.Driver, name) if err != nil { - atexit.ExitWithMessage(1, fmt.Sprintf("Error unmounting the host folder: %s", err.Error())) + atexit.ExitWithMessage(1, err.Error()) } }, } func init() { - HostfolderCmd.AddCommand(hostfolderUmountCmd) + HostFolderCmd.AddCommand(umountCmd) } diff --git a/cmd/minishift/cmd/hostfolder/util.go b/cmd/minishift/cmd/hostfolder/util.go new file mode 100644 index 0000000000..5d76de94fc --- /dev/null +++ b/cmd/minishift/cmd/hostfolder/util.go @@ -0,0 +1,79 @@ +package hostfolder + +import ( + "fmt" + cmdConfig "github.com/minishift/minishift/cmd/minishift/cmd/config" + "github.com/minishift/minishift/pkg/minishift/config" + "github.com/minishift/minishift/pkg/minishift/hostfolder" + "github.com/minishift/minishift/pkg/util" + "github.com/minishift/minishift/pkg/util/os/atexit" + "github.com/spf13/viper" + "regexp" + "strings" +) + +const ( + HostfoldersDefaultPath = "/mnt/sda1" + HostfoldersMountPathKey = "hostfolders-mountpath" +) + +var ( + optionsPattern = regexp.MustCompile(`^([a-z]+=.*,)?([a-z]+=.*)$`) + keyValuePattern = regexp.MustCompile(`^([a-z]+)=(.*)$`) +) + +func getHostFolderManager() *hostfolder.Manager { + hostFolderManager, err := hostfolder.NewManager(config.InstanceConfig, config.AllInstancesConfig) + if err != nil { + atexit.ExitWithMessage(1, err.Error()) + } + + port := viper.GetInt(cmdConfig.HostFoldersSftpPort.Name) + if port != 0 { + hostfolder.SftpPort = port + } + + return hostFolderManager +} + +func readInputForMountPoint(name string) string { + defaultMountPoint := getHostFolderMountPath(name) + mountPointText := fmt.Sprintf("Mountpoint [%s]", defaultMountPoint) + + mountPoint := util.ReadInputFromStdin(mountPointText) + if len(mountPoint) == 0 { + mountPoint = defaultMountPoint + } + return mountPoint +} + +func getHostFolderMountPath(name string) string { + overrideMountPath := viper.GetString(HostfoldersMountPathKey) + if len(overrideMountPath) > 0 { + return fmt.Sprintf("%s/%s", overrideMountPath, name) + } + + return fmt.Sprintf("%s/%s", HostfoldersDefaultPath, name) +} + +func getOptions(optionString string) map[string]string { + options := make(map[string]string) + + var key, value, remainder string + remainder = optionString + for remainder != "" { + result := optionsPattern.FindAllStringSubmatch(remainder, -1) + for _, match := range result { + key, value, remainder = extractMatch(match) + options[key] = value + } + } + + return options +} + +func extractMatch(match []string) (string, string, string) { + result := keyValuePattern.FindAllStringSubmatch(match[2], -1) + remainder := strings.TrimRight(match[1], ",") + return result[0][1], result[0][2], remainder +} diff --git a/cmd/minishift/cmd/hostfolder/util_test.go b/cmd/minishift/cmd/hostfolder/util_test.go new file mode 100644 index 0000000000..fa9bbf26e5 --- /dev/null +++ b/cmd/minishift/cmd/hostfolder/util_test.go @@ -0,0 +1,27 @@ +package hostfolder + +import ( + "github.com/magiconair/properties/assert" + "testing" +) + +var testOptions = []struct { + options string + expectedOptions map[string]string +}{ + {"", map[string]string{}}, + {"user=foo", map[string]string{"user": "foo"}}, + {"user=foo,password=bar", map[string]string{"user": "foo", "password": "bar"}}, + {"user=foo,password=sna,fu", map[string]string{"user": "foo", "password": "sna,fu"}}, + {"user=foo,password=snafu,", map[string]string{"user": "foo", "password": "snafu,"}}, + {"user=foo,password=sna=fu", map[string]string{"user": "foo", "password": "sna=fu"}}, + {"user=foo,password=sna,fu,domain=WORKGROUP", map[string]string{"user": "foo", "password": "sna,fu", "domain": "WORKGROUP"}}, + {"user=foo,password=sn=a,fu,domain=WORKGROUP", map[string]string{"user": "foo", "password": "sn=a,fu", "domain": "WORKGROUP"}}, +} + +func Test_get_options(t *testing.T) { + for _, testOption := range testOptions { + actualOptions := getOptions(testOption.options) + assert.Equal(t, actualOptions, testOption.expectedOptions, "The extracted options don't match") + } +} diff --git a/cmd/minishift/cmd/root.go b/cmd/minishift/cmd/root.go index 198f0e81e8..e03761fef3 100644 --- a/cmd/minishift/cmd/root.go +++ b/cmd/minishift/cmd/root.go @@ -188,7 +188,7 @@ func init() { RootCmd.PersistentFlags().String(profileFlag, constants.DefaultProfileName, "Profile name") RootCmd.AddCommand(configCmd.ConfigCmd) RootCmd.AddCommand(cmdOpenshift.OpenShiftCmd) - RootCmd.AddCommand(hostfolderCmd.HostfolderCmd) + RootCmd.AddCommand(hostfolderCmd.HostFolderCmd) RootCmd.AddCommand(addon.AddonsCmd) RootCmd.AddCommand(image.ImageCmd) RootCmd.AddCommand(cmdProfile.ProfileCmd) diff --git a/cmd/minishift/cmd/start.go b/cmd/minishift/cmd/start.go index 0d49c2de5d..a6145fcacb 100644 --- a/cmd/minishift/cmd/start.go +++ b/cmd/minishift/cmd/start.go @@ -61,6 +61,7 @@ import ( const ( commandName = "start" defaultInsecureRegistry = "172.30.0.0/16" + hostfoldersAutoMountKey = "hostfolders-automount" ) var ( @@ -381,11 +382,20 @@ func startHost(libMachineClient *libmachine.Client) *host.Host { } func autoMountHostFolders(driver drivers.Driver) { - if hostfolder.IsAutoMount() && hostfolder.IsHostfoldersDefined() { - hostfolder.MountHostfolders(driver) + hostFolderManager, err := hostfolder.NewManager(minishiftConfig.InstanceConfig, minishiftConfig.AllInstancesConfig) + if err != nil { + atexit.ExitWithMessage(1, err.Error()) + } + if isAutoMount() && hostFolderManager.ExistAny() { + fmt.Println("-- Mounting host folders") + hostFolderManager.MountAll(driver) } } +func isAutoMount() bool { + return viper.GetBool(hostfoldersAutoMountKey) +} + func addActiveProfileInformation() { if constants.ProfileName != profileActions.GetActiveProfile() { fmt.Println(fmt.Sprintf("-- Switching active profile to '%s'", constants.ProfileName)) diff --git a/docs/source/using/host-folders.adoc b/docs/source/using/host-folders.adoc index e7ffd11761..95b8e76556 100644 --- a/docs/source/using/host-folders.adoc +++ b/docs/source/using/host-folders.adoc @@ -23,15 +23,15 @@ You can use the `hostfolder` command to mount multiple shared folders onto custo [NOTE] ==== -Currently only link:https://en.wikipedia.org/wiki/Server_Message_Block[CIFS] is supported as a host folder type. -Support for link:https://en.wikipedia.org/wiki/SSHFS[SSHFS]-based host folders is in progress, as described in GitHub issue link:https://github.com/minishift/minishift/issues/317[#317]. -If you want to manually set up SSHFS, see xref:sshfs-folder-mount[SSHFS Host Folders]. +Currently link:https://en.wikipedia.org/wiki/Server_Message_Block[CIFS] and link:https://en.wikipedia.org/wiki/SSHFS[SSHFS] based host folders are supported. ==== [[host-folder-prerequisite]] === Prerequisites -To use the `minishift hostfolder` command, you need to be able to share directories using CIFS. +==== CIFS + +To use the `minishift hostfolder` command for CIFS based host folders, you need to be able to share directories using CIFS. On Windows, CIFS is the default technology for sharing directories. For example, on Windows 10 the *_C:\Users_* directory is shared by default and can be accessed by locally-authenticated users. @@ -41,6 +41,10 @@ See link:https://support.apple.com/en-us/HT204445[How to connect with File Shari On Linux, follow your distribution-specific instructions to install link:https://www.samba.org[Samba]. +==== SSHFS + +SSHFS based host folders work without prerequisite. + [[displaying-host-folders]] === Displaying Host Folders @@ -50,43 +54,40 @@ An example output could look like: ---- $ minishift hostfolder list -Name Mountpoint Remote path Mounted -myshare /mnt/sda1/myshare //192.168.1.82/MYSHARE N +Name Type Source Mountpoint Mounted +test sshfs /Users/john/test /mnt/sda1/test N ---- -In this example, there is a host folder with the name **myshare** which mounts *_//192.168.1.82/MYSHARE_* onto *_/mnt/sda1/myshare_* in the {project} VM. +In this example, there is a sshfs based host folder with the name **test** which mounts *_/Users/john/test_* onto *_/mnt/sda1/test_* in the {project} VM. The share is currently not mounted. -[NOTE] -==== -The remote path must be reachable from within the VM. -In the example above, *192.168.1.82* is the IP of the host within the LAN, which is one option you can use. -You can use `ifconfig` (or `Get-NetIPAddress | Format-Table` on Windows) to determine a routable IP address. -==== - [[adding-host-folders]] === Adding Host Folders The xref:../command-ref/minishift_hostfolder_add.adoc#[`minishift hostfolder add`] command allows you to define a new host folder. -This is an interactive process that queries the relevant details for a host folder based on CIFS. + +The exact syntax to use depends on the host folder type. +Independent of the type you can choose between non-interactive and interactive configuration. +The default it non-interactive. +By specifying the `--interactive` you can select the interactive configuration mode. + +The following sections give examples for configuring CIFS and SSHFS host folders. + +==== CIFS [[adding-cifs-hostfolder]] .Adding a CIFS based hostfolder ---- -$ minishift hostfolder add myshare // <1> -UNC path: //192.168.99.1/MYSHARE // <2> -Mountpoint [/mnt/sda1/myshare]: // <3> -Username: john // <4> -Password: [HIDDEN] // <5> -Domain: // <6> -Added: myshare ----- -<1> (Required) Actual `minishift hostfolder add` command that specifies a host folder with the name of **myshare**. -<2> (Required) The UNC path for the share. -<3> The mount point within the VM. The default is *_/mnt/sda1/_*. -<4> (Required) The user name for the CIFS share. -<5> (Required) The password for the CIFS share. -<6> The domain of the share. Often this can be left blank, but for example on Windows, when your account is linked to a Microsoft account, you must use the Microsoft account email address as user name as well as your machine name as displayed by `$env:COMPUTERNAME` as a domain. +$ minishift hostfolder add -t cifs --source //192.168.99.1/MYSHARE --target /mnt/sda1/myshare --options username=john,password=mysecret,domain=MYDOMAIN myshare +---- + +The above command will create a new host folder with the name of **myshare**. +The source of the host folder is the UNC path *_//192.168.99.1/MYSHARE_* which is required. +The target or mount point is *_/mnt/sda1/myshare_* within the {project} VM. +Using the `--options` flag host folder type specific options can be specified using a list of comma separated key/value pairs. +In the case of a CIFS host folder the required options are the user name for the CIFS share as well as the password. +Via the the `--options` flag the domain of the share can be specified as well. +Often this can be left out, but for example on Windows, when your account is linked to a Microsoft account, you must use the Microsoft account email address as user name as well as your machine name as displayed by `$env:COMPUTERNAME` as a domain. [TIP] ==== @@ -99,6 +100,16 @@ When this option is specified, no UNC path needs to be specified and *_C:\Users_ When you use the Boot2Docker ISO with the VirtualBox driver, VirtualBox guest additions are automatically enabled and occupy the *_/Users_* mount point. ==== +==== SSHFS + +[[adding-cifs-hostfolder]] +.Adding a SSHFS based hostfolder +---- +$ minishift hostfolder add -t sshfs --source /Users/john/myshare --target /mnt/sda1/myshare myshare +---- + +For the case of a SSHFS based host folder only the source and target of the host folder need to be specified. + [[instance-host-folders]] ==== Instance-Specific Host Folders @@ -112,11 +123,10 @@ Host folder definitions that are created with the `--instance-only` flag will be [[mounting-host-folders]] === Mounting Host Folders -After you add host folders, you use the xref:../command-ref/minishift_hostfolder_mount.adoc#[`minishift hostfolder mount`] command to mount a host folder by its name: +After you have added host folders, you use the xref:../command-ref/minishift_hostfolder_mount.adoc#[`minishift hostfolder mount`] command to mount a host folder by its name: ---- $ minishift hostfolder mount myshare -Mounting 'myshare': '//192.168.99.1/MYSHARE' as '/mnt/sda1/myshare' ... OK ---- You can verify that the host folder is mounted by running: @@ -133,6 +143,16 @@ Alternatively, you can list the actual content of the mounted host folder: $ minishift ssh "ls -al /mnt/sda1/myshare" ---- +[TIP] +==== +When mounting SSHFS based host folders a SFTP server process is started on port 2022 of the host. +If you need to configure this port you can make use of {project}'s xref:../using/basic-usage.adoc#persistent-configuration[persistent configuration] using the key `hostfolders-sftp-port`, for example: + +---- +$ minishift config set hostfolders-sftp-port 2222 +---- +==== + [[auto-mounting-host-folders]] ==== Auto-Mounting Host Folders @@ -152,7 +172,6 @@ You use the xref:../command-ref/minishift_hostfolder_umount.adoc#[`minishift hos ---- $ minishift hostfolder umount myshare -Unmounting 'myshare' ... OK $ minishift hostfolder list Name Mountpoint Remote path Mounted @@ -170,46 +189,7 @@ Name Mountpoint Remote path Mounted myshare /mnt/sda1/myshare //192.168.1.82/MYSHARE N $ minishift hostfolder remove myshare -Removed: myshare $ minishift hostfolder list No host folders defined ---- - -[[sshfs-folder-mount]] -=== SSHFS Host Folders - -[NOTE] -==== -This host folder type is not supported by the `minishift hostfolder` command and requires manual configuration. -==== - -You can use SSHFS-based host folders if you have an SSH daemon running on your host. -Normally, this prerequisite is met by default on Linux and macOS. - -Most Linux distributions have an SSH daemon installed. -If not, follow the instructions for your specific distribution to install an SSH daemon. - -macOS also has a built-in SSH server. -To use it, make sure that *Remote Login* is enabled in *System Preferences > Sharing*. - -On Windows, you can install link:https://winscp.net/eng/docs/guide_windows_openssh_server[OpenSSH for Windows]. - -The following steps demonstrate how to mount host folders with SSHFS. - -. Run `ifconfig` (or `Get-NetIPAddress` on Windows) to determine the local IP address from the same network segment as your {project} instance. - -. Create a mountpoint and mount the shared folder. -+ ----- -$ minishift ssh "sudo mkdir -p /Users/" -$ minishift ssh "sudo chown -R docker /Users" -$ minishift ssh -$ sshfs @:/Users// /Users ----- - -. Verify the share mount. -+ ----- -$ minishift ssh "ls -al /Users/" ----- diff --git a/pkg/minishift/config/allinstancesconfig.go b/pkg/minishift/config/allinstancesconfig.go index 040bcce23d..31e8c406ae 100644 --- a/pkg/minishift/config/allinstancesconfig.go +++ b/pkg/minishift/config/allinstancesconfig.go @@ -18,6 +18,7 @@ package config import ( "encoding/json" + "github.com/minishift/minishift/pkg/minishift/hostfolder/config" "io/ioutil" "os" ) @@ -27,14 +28,15 @@ var AllInstancesConfig *GlobalConfigType type GlobalConfigType struct { FilePath string `json:"-"` - HostFolders []HostFolder + HostFolders []config.HostFolderConfig ActiveProfile string + SftpdPID int } // Create new object with data if file exists or // Create json file and return object if doesn't exists func NewAllInstancesConfig(path string) (*GlobalConfigType, error) { - cfg := &GlobalConfigType{HostFolders: []HostFolder{}} + cfg := &GlobalConfigType{HostFolders: []config.HostFolderConfig{}} cfg.FilePath = path // Check json file existence diff --git a/pkg/minishift/config/hostfolder.go b/pkg/minishift/config/hostfolder.go deleted file mode 100644 index d157a66368..0000000000 --- a/pkg/minishift/config/hostfolder.go +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright (C) 2017 Red Hat, Inc. - -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 config - -import ( - "fmt" - - "github.com/spf13/viper" -) - -type HostFolder struct { - Name string - Type string - Options map[string]string -} - -const ( - HostfoldersDefaultPath = "/mnt/sda1" - HostfoldersMountPathKey = "hostfolders-mountpath" -) - -func GetHostfoldersMountPath(name string) string { - overrideMountPath := viper.GetString(HostfoldersMountPathKey) - if len(overrideMountPath) > 0 { - return fmt.Sprintf("%s/%s", overrideMountPath, name) - } - - return fmt.Sprintf("%s/%s", HostfoldersDefaultPath, name) -} - -func (hf *HostFolder) Mountpoint() string { - overrideMountPoint := hf.Options["mountpoint"] - if len(overrideMountPoint) > 0 { - return overrideMountPoint - } - - return GetHostfoldersMountPath(hf.Name) -} diff --git a/pkg/minishift/config/hostfolder_test.go b/pkg/minishift/config/hostfolder_test.go deleted file mode 100644 index 4325fc5c8f..0000000000 --- a/pkg/minishift/config/hostfolder_test.go +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright (C) 2017 Red Hat, Inc. - -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 config - -import ( - "fmt" - "testing" - - "github.com/spf13/viper" - "github.com/stretchr/testify/assert" -) - -func TestHostfolderConfig(t *testing.T) { - setup(t) - defer teardown() - - hostfolderActual := HostFolder{ - Name: "Users", - Type: "cifs", - Options: map[string]string{ - "mountpoint": "", - "uncpath": "//127.0.0.1/Users", - "username": "joe@pillow.us", - "password": "am!g@4ever", - "domain": "DESKTOP-RHAIMSWIN", - }, - } - - hostfolderExpectedMountpoint := fmt.Sprintf("%s/%s", HostfoldersDefaultPath, "Users") - assert.Equal(t, hostfolderExpectedMountpoint, hostfolderActual.Mountpoint()) - - viper.Set(HostfoldersMountPathKey, "/mnt/data") - hostfolderExpectedMountpoint = "/mnt/data/Users" - assert.Equal(t, hostfolderExpectedMountpoint, hostfolderActual.Mountpoint()) - - hostfolderActual.Options["mountpoint"] = "/c/Users" - hostfolderExpectedMountpoint = "/c/Users" - assert.Equal(t, hostfolderExpectedMountpoint, hostfolderActual.Mountpoint()) -} diff --git a/pkg/minishift/config/instanceconfig.go b/pkg/minishift/config/instanceconfig.go index 7887faf0cd..6e2a086ab9 100644 --- a/pkg/minishift/config/instanceconfig.go +++ b/pkg/minishift/config/instanceconfig.go @@ -18,6 +18,7 @@ package config import ( "encoding/json" + "github.com/minishift/minishift/pkg/minishift/hostfolder/config" "io/ioutil" "os" ) @@ -30,13 +31,13 @@ type InstanceConfigType struct { IsRegistered bool IsRHELBased bool - HostFolders []HostFolder + HostFolders []config.HostFolderConfig } // Create new object with data if file exists or // Create json file and return object if doesn't exists func NewInstanceConfig(path string) (*InstanceConfigType, error) { - cfg := &InstanceConfigType{HostFolders: []HostFolder{}} + cfg := &InstanceConfigType{HostFolders: []config.HostFolderConfig{}} cfg.FilePath = path // Check json file existence diff --git a/pkg/minishift/hostfolder/cifs_hostfolder.go b/pkg/minishift/hostfolder/cifs_hostfolder.go new file mode 100644 index 0000000000..6b3f42e094 --- /dev/null +++ b/pkg/minishift/hostfolder/cifs_hostfolder.go @@ -0,0 +1,184 @@ +/* +Copyright (C) 2017 Red Hat, Inc. + +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 hostfolder + +import ( + "errors" + "fmt" + "github.com/docker/machine/libmachine/drivers" + + miniconfig "github.com/minishift/minishift/pkg/minishift/config" + "github.com/minishift/minishift/pkg/minishift/hostfolder/config" + miniutil "github.com/minishift/minishift/pkg/minishift/util" + "github.com/minishift/minishift/pkg/util" + "net" + "strings" +) + +type CifsHostFolder struct { + config config.HostFolderConfig +} + +func NewCifsHostFolder(config config.HostFolderConfig) HostFolder { + return &CifsHostFolder{config: config} +} + +func (h *CifsHostFolder) Config() config.HostFolderConfig { + return h.config +} + +func (h *CifsHostFolder) Mount(driver drivers.Driver) error { + // If "Users" is used as name, determine the IP of host for UNC path on startup + if h.config.Name == "Users" { + hostIP, _ := h.determineHostIP(driver) + h.config.Options[config.UncPath] = fmt.Sprintf("//%s/Users", hostIP) + } + + print(fmt.Sprintf(" Mounting '%s': '%s' as '%s' ... ", + h.config.Name, + h.config.Options[config.UncPath], + h.config.MountPoint())) + + if isMounted, err := h.isHostFolderMounted(driver); isMounted { + fmt.Println("Already mounted") + return fmt.Errorf("host folder is already mounted. %s", err) + } + + if !h.isCifsHostReachable(driver) { + fmt.Print("Unreachable\n") + return errors.New("host folder is unreachable") + } + + password, err := util.DecryptText(h.config.Options[config.Password]) + if err != nil { + return err + } + + cmd := fmt.Sprintf( + "sudo mount -t cifs %s %s -o username=%s,password=%s", + h.config.Options[config.UncPath], + h.config.MountPoint(), + h.config.Options[config.UserName], + password) + + if miniconfig.InstanceConfig.IsRHELBased { + cmd = fmt.Sprintf("%s,context=system_u:object_r:svirt_sandbox_file_t:s0", cmd) + } + + if len(h.config.Options[config.Domain]) > 0 { + cmd = fmt.Sprintf("%s,domain=%s", cmd, h.config.Options["domain"]) + } + + if err := h.ensureMountPointExists(driver); err != nil { + fmt.Println("FAIL") + return fmt.Errorf("error occured while creating mountpoint. %s", err) + } + + if _, err := drivers.RunSSHCommandFromDriver(driver, cmd); err != nil { + fmt.Println("FAIL") + return fmt.Errorf("error occured while mounting host folder: %s", err) + } else { + fmt.Println("OK") + } + + return nil +} + +func (h *CifsHostFolder) Umount(driver drivers.Driver) error { + if isMounted, err := h.isHostFolderMounted(driver); !isMounted { + fmt.Print("Not mounted\n") + return fmt.Errorf("host folder not mounted: %s", err) + } + + cmd := fmt.Sprintf( + "sudo umount %s", + h.config.MountPoint()) + + if _, err := drivers.RunSSHCommandFromDriver(driver, cmd); err != nil { + fmt.Println("FAIL") + return fmt.Errorf("error during umounting of host folder: %s", err) + } else { + fmt.Println("OK") + } + + return nil +} + +func (h *CifsHostFolder) isCifsHostReachable(driver drivers.Driver) bool { + uncPath := h.config.Options[config.UncPath] + + host := "" + + splitHost := strings.Split(uncPath, "/") + if len(splitHost) > 2 { + host = splitHost[2] + } + + if host == "" { + return false + } + + return miniutil.IsIPReachable(driver, host, false) +} + +func (h *CifsHostFolder) determineHostIP(driver drivers.Driver) (string, error) { + instanceIP, err := driver.GetIP() + if err != nil { + return "", err + } + + for _, hostaddr := range miniutil.HostIPs() { + + if miniutil.NetworkContains(hostaddr, instanceIP) { + hostip, _, _ := net.ParseCIDR(hostaddr) + if miniutil.IsIPReachable(driver, hostip.String(), false) { + return hostip.String(), nil + } + return "", errors.New("unreachable") + } + } + + return "", errors.New("unknown error occurred") +} + +func (h *CifsHostFolder) isHostFolderMounted(driver drivers.Driver) (bool, error) { + cmd := fmt.Sprintf( + "if grep -qs %s /proc/mounts; then echo '1'; else echo '0'; fi", + h.config.MountPoint()) + + out, err := drivers.RunSSHCommandFromDriver(driver, cmd) + + if err != nil { + return false, err + } + if strings.Trim(out, "\n") == "0" { + return false, nil + } + + return true, nil +} + +func (h *CifsHostFolder) ensureMountPointExists(driver drivers.Driver) error { + cmd := fmt.Sprintf( + "sudo mkdir -p %s", + h.config.MountPoint()) + + if _, err := drivers.RunSSHCommandFromDriver(driver, cmd); err != nil { + return err + } + + return nil +} diff --git a/pkg/minishift/hostfolder/config/hostfolder_config.go b/pkg/minishift/hostfolder/config/hostfolder_config.go new file mode 100644 index 0000000000..0184e753b6 --- /dev/null +++ b/pkg/minishift/hostfolder/config/hostfolder_config.go @@ -0,0 +1,24 @@ +package config + +const ( + Source = "source" + UncPath = "uncpath" + MountPoint = "mountpoint" + UserName = "username" + Password = "password" + Domain = "domain" +) + +type HostFolderConfig struct { + Name string + Type string + Options map[string]string +} + +func (hf *HostFolderConfig) Option(key string) string { + return hf.Options[key] +} + +func (hf *HostFolderConfig) MountPoint() string { + return hf.Options[MountPoint] +} diff --git a/pkg/minishift/hostfolder/config/hostfolder_config_test.go b/pkg/minishift/hostfolder/config/hostfolder_config_test.go new file mode 100644 index 0000000000..18a5c07910 --- /dev/null +++ b/pkg/minishift/hostfolder/config/hostfolder_config_test.go @@ -0,0 +1,41 @@ +/* +Copyright (C) 2017 Red Hat, Inc. + +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 config + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestHostFolderConfig(t *testing.T) { + hostFolderConfigActual := HostFolderConfig{ + Name: "Users", + Type: "cifs", + Options: map[string]string{ + MountPoint: "/mnt/data", + UncPath: "//127.0.0.1/Users", + UserName: "joe@pillow.us", + Password: "am!g@4ever", + Domain: "DESKTOP-RHAIMSWIN", + }, + } + + assert.Equal(t, "/mnt/data", hostFolderConfigActual.MountPoint()) + assert.Equal(t, "joe@pillow.us", hostFolderConfigActual.Option(UserName)) + assert.Equal(t, "am!g@4ever", hostFolderConfigActual.Option(Password)) + assert.Equal(t, "DESKTOP-RHAIMSWIN", hostFolderConfigActual.Option(Domain)) +} diff --git a/pkg/minishift/hostfolder/hostfolder.go b/pkg/minishift/hostfolder/hostfolder.go index eb846253ee..6f526e72de 100644 --- a/pkg/minishift/hostfolder/hostfolder.go +++ b/pkg/minishift/hostfolder/hostfolder.go @@ -1,473 +1,41 @@ -/* -Copyright (C) 2017 Red Hat, Inc. - -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 hostfolder import ( - "errors" - "fmt" - "net" - "os" - "strings" - "text/tabwriter" - "github.com/docker/machine/libmachine/drivers" - "github.com/docker/machine/libmachine/state" - "github.com/spf13/viper" - - "github.com/minishift/minishift/pkg/minishift/config" - minishiftConfig "github.com/minishift/minishift/pkg/minishift/config" - miniutil "github.com/minishift/minishift/pkg/minishift/util" - "github.com/minishift/minishift/pkg/util" - minishiftStrings "github.com/minishift/minishift/pkg/util/strings" + "github.com/minishift/minishift/pkg/minishift/hostfolder/config" ) -const ( - HostfoldersAutoMountKey = "hostfolders-automount" - NonSupportedTtyError = "Not a tty supported terminal" -) - -func IsAutoMount() bool { - return viper.GetBool(HostfoldersAutoMountKey) -} - -func isHostRunning(driver drivers.Driver) bool { - return drivers.MachineInState(driver, state.Running)() -} - -func IsHostfoldersDefined() bool { - return len(config.InstanceConfig.HostFolders) > 0 || - len(config.AllInstancesConfig.HostFolders) > 0 -} - -func isHostfolderDefinedByName(name string) bool { - return getHostfolderByName(name) != nil -} - -func List(driver drivers.Driver, isRunning bool) error { - if !IsHostfoldersDefined() { - return errors.New("No host folders defined") - } - - procMounts := "" - if isRunning { - cmd := "cat /proc/mounts" - procMounts, _ = drivers.RunSSHCommandFromDriver(driver, cmd) - } - - w := tabwriter.NewWriter(os.Stdout, 4, 8, 3, ' ', 0) - fmt.Fprintln(w, "Name\tMountpoint\tRemote path\tMounted") - - hostfolders := config.AllInstancesConfig.HostFolders - hostfolders = append(hostfolders, config.InstanceConfig.HostFolders...) - for i := range hostfolders { - hostfolder := hostfolders[i] - - remotePath := "" - switch hostfolder.Type { - case "cifs": - remotePath = hostfolder.Options["uncpath"] - } - - mounted := "N" - if isRunning && strings.Contains(procMounts, hostfolder.Mountpoint()) { - mounted = "Y" - } - - fmt.Fprintln(w, - (fmt.Sprintf("%s\t%s\t%s\t%s", - hostfolder.Name, - hostfolder.Mountpoint(), - remotePath, - mounted))) - } - - w.Flush() - return nil -} - -func readInputForMountpoint(name string) string { - defaultMountpoint := config.GetHostfoldersMountPath(name) - mountpointText := fmt.Sprintf("Mountpoint [%s]", defaultMountpoint) - return util.ReadInputFromStdin(mountpointText) -} - -func SetupUsers(allInstances bool) error { - name := "Users" - if isHostfolderDefinedByName(name) { - return fmt.Errorf("Already have a host folder defined for: '%s'", name) - } - - mountpoint := readInputForMountpoint(name) - username := util.ReadInputFromStdin("Username") - if !util.IsTtySupported() { - return fmt.Errorf(NonSupportedTtyError) - } - password := util.ReadPasswordFromStdin("Password") - domain := util.ReadInputFromStdin("Domain") - password, err := util.EncryptText(password) - if err != nil { - return err - } - - // We only store this record for credentials purpose - addToConfig(newCifsHostFolder( - name, - "[determined on startup]", - mountpoint, - username, password, domain), - allInstances) - - return nil -} - -func Add(name string, allInstances bool) error { - if isHostfolderDefinedByName(name) { - return fmt.Errorf("Already have a host folder defined for: '%s'", name) - } - - uncpath := util.ReadInputFromStdin("UNC path") - if len(uncpath) == 0 { - return fmt.Errorf("No remote path has been given") - } - mountpoint := readInputForMountpoint(name) - username := util.ReadInputFromStdin("Username") - if !util.IsTtySupported() { - return fmt.Errorf(NonSupportedTtyError) - } - password := util.ReadPasswordFromStdin("Password") - domain := util.ReadInputFromStdin("Domain") - password, err := util.EncryptText(password) - if err != nil { - return err - } - - addToConfig(newCifsHostFolder( - name, - uncpath, - mountpoint, - username, password, domain), - allInstances) - - return nil -} - -func newCifsHostFolder(name string, uncpath string, mountpoint string, username string, password string, domain string) config.HostFolder { - return config.HostFolder{ - Name: name, - Type: "cifs", - Options: map[string]string{ - "mountpoint": mountpoint, - "uncpath": convertSlashes(uncpath), - "username": username, - "password": password, - "domain": domain, - }, - } -} - -func addToConfig(hostfolder config.HostFolder, allInstances bool) { - if allInstances { - config.AllInstancesConfig.HostFolders = append(config.AllInstancesConfig.HostFolders, hostfolder) - config.AllInstancesConfig.Write() - } else { - config.InstanceConfig.HostFolders = append(config.InstanceConfig.HostFolders, hostfolder) - config.InstanceConfig.Write() - } - - fmt.Printf("Added: %s\n", hostfolder.Name) -} - -func Remove(name string) error { - if !isHostfolderDefinedByName(name) { - return fmt.Errorf("No host folder defined as: '%s'", name) - } - - config.InstanceConfig.HostFolders = removeFromHostFoldersByName(name, config.InstanceConfig.HostFolders) - config.InstanceConfig.Write() - - config.AllInstancesConfig.HostFolders = removeFromHostFoldersByName(name, config.AllInstancesConfig.HostFolders) - config.AllInstancesConfig.Write() - - fmt.Printf("Removed: %s\n", name) - - return nil -} - -func Mount(driver drivers.Driver, name string) error { - if !isHostRunning(driver) { - return errors.New("Host is in the wrong state.") - } - - if !IsHostfoldersDefined() { - return errors.New("No host folders defined.") - } - - hostfolder := getHostfolderByName(name) - if hostfolder == nil { - return fmt.Errorf("No host folder defined as: '%s'", name) - } else { - ensureMountPointExists(driver, hostfolder) - mountHostfolder(driver, hostfolder) - } - return nil -} - -// Performs mounting of host folders -func MountHostfolders(driver drivers.Driver) error { - if !isHostRunning(driver) { - return errors.New("Host is in the wrong state.") - } - - if !IsHostfoldersDefined() { - return errors.New("No host folders defined.") - } - - fmt.Println("-- Mounting hostfolders") - - hostfolders := config.AllInstancesConfig.HostFolders - hostfolders = append(hostfolders, config.InstanceConfig.HostFolders...) - for i := range hostfolders { - mountHostfolder(driver, &hostfolders[i]) - } - - return nil -} - -func Umount(driver drivers.Driver, name string) error { - if !isHostRunning(driver) { - return errors.New("Host is in the wrong state.") - } +type Type int - if !IsHostfoldersDefined() { - return errors.New("No host folders defined") - } - - hostfolder := getHostfolderByName(name) - if hostfolder == nil { - return fmt.Errorf("No host folder defined as: '%s'", name) - } else { - umountHostfolder(driver, hostfolder) - } - return nil -} - -func mountHostfolder(driver drivers.Driver, hostfolder *config.HostFolder) error { - if hostfolder == nil { - return errors.New("Host folder not defined") - } - - switch hostfolder.Type { - case "cifs": - if err := mountCifsHostfolder(driver, hostfolder); err != nil { - return err - } - default: - return errors.New("Unsupported host folder type") - } - - return nil -} - -func determineHostIp(driver drivers.Driver) (string, error) { - instanceip, err := driver.GetIP() - if err != nil { - return "", err - } - - for _, hostaddr := range miniutil.HostIPs() { - - if miniutil.NetworkContains(hostaddr, instanceip) { - hostip, _, _ := net.ParseCIDR(hostaddr) - if miniutil.IsIPReachable(driver, hostip.String(), false) { - return hostip.String(), nil - } - return "", errors.New("Unreachable") - } - } - - return "", errors.New("Unknown error occured") -} - -func mountCifsHostfolder(driver drivers.Driver, hostfolder *config.HostFolder) error { - // If "Users" is used as name, determine the IP of host for UNC path on startup - if hostfolder.Name == "Users" { - hostip, _ := determineHostIp(driver) - hostfolder.Options["uncpath"] = fmt.Sprintf("//%s/Users", hostip) - } - - print(fmt.Sprintf(" Mounting '%s': '%s' as '%s' ... ", - hostfolder.Name, - hostfolder.Options["uncpath"], - hostfolder.Mountpoint())) - - if isMounted, err := isHostfolderMounted(driver, hostfolder); isMounted { - fmt.Println("Already mounted") - return fmt.Errorf("Host folder is already mounted. %s", err) - } - - if !isCifsHostReachable(driver, hostfolder.Options["uncpath"]) { - fmt.Print("Unreachable\n") - return errors.New("Host folder is unreachable") - } - - password, err := util.DecryptText(hostfolder.Options["password"]) - if err != nil { - return err - } - - cmd := fmt.Sprintf( - "sudo mount -t cifs %s %s -o username=%s,password='%s'", - hostfolder.Options["uncpath"], - hostfolder.Mountpoint(), - hostfolder.Options["username"], - minishiftStrings.EscapeSingleQuote(password)) - - if minishiftConfig.InstanceConfig.IsRHELBased { - cmd = fmt.Sprintf("%s,context=system_u:object_r:svirt_sandbox_file_t:s0", cmd) - } - - if len(hostfolder.Options["domain"]) > 0 { // != "" - cmd = fmt.Sprintf("%s,domain=%s", cmd, hostfolder.Options["domain"]) - } - - if err := ensureMountPointExists(driver, hostfolder); err != nil { - fmt.Println("FAIL") - return fmt.Errorf("Error occured while creating mountpoint. %s", err) - } - - if _, err := drivers.RunSSHCommandFromDriver(driver, cmd); err != nil { - fmt.Println("FAIL") - return fmt.Errorf("Error occured while mounting host folder. %s", err) - } else { - fmt.Println("OK") - } - - return nil -} - -func umountHostfolder(driver drivers.Driver, hostfolder *config.HostFolder) error { - if hostfolder == nil { - return errors.New("Host folder not defined") - } - - fmt.Printf(" Unmounting '%s' ... ", hostfolder.Name) - - if isMounted, err := isHostfolderMounted(driver, hostfolder); !isMounted { - fmt.Print("Not mounted\n") - return fmt.Errorf("Host folder not mounted. %s", err) - } - - cmd := fmt.Sprintf( - "sudo umount %s", - hostfolder.Mountpoint()) - - if _, err := drivers.RunSSHCommandFromDriver(driver, cmd); err != nil { - fmt.Println("FAIL") - return fmt.Errorf("Error occured while unmounting host folder. %s", err) - } else { - fmt.Println("OK") - } - - return nil -} - -func isHostfolderMounted(driver drivers.Driver, hostfolder *config.HostFolder) (bool, error) { - cmd := fmt.Sprintf( - "if grep -qs %s /proc/mounts; then echo '1'; else echo '0'; fi", - hostfolder.Mountpoint()) - - out, err := drivers.RunSSHCommandFromDriver(driver, cmd) - - if err != nil { - return false, err - } - if strings.Trim(out, "\n") == "0" { - return false, nil - } - - return true, nil -} - -func convertSlashes(input string) string { - return strings.Replace(input, "\\", "/", -1) -} - -func isCifsHostReachable(driver drivers.Driver, uncpath string) bool { - host := "" - - splithost := strings.Split(uncpath, "/") - if len(splithost) > 2 { - host = splithost[2] - } - - if host == "" { - return false - } - - return miniutil.IsIPReachable(driver, host, false) -} - -func ensureMountPointExists(driver drivers.Driver, hostfolder *config.HostFolder) error { - if hostfolder == nil { - return errors.New("Host folder is not defined") - } - - cmd := fmt.Sprintf( - "sudo mkdir -p %s", - hostfolder.Mountpoint()) - - if _, err := drivers.RunSSHCommandFromDriver(driver, cmd); err != nil { - return err - } - - return nil -} +const ( + // SSHFS defines the constant to be used for the SSFS host folder type. + SSHFS Type = iota -func removeFromHostFoldersByName(name string, hostfolders []config.HostFolder) []config.HostFolder { - for i := range hostfolders { + // CIFS defines the constant to be used for the CIFS host folder type. + CIFS +) - hostfolder := hostfolders[i] +func (t Type) String() string { + names := [...]string{ + "sshfs", + "cifs"} - if hostfolder.Name == name { - hostfolders = append(hostfolders[:i], hostfolders[i+1:]...) - break - } + // prevent panicking + if t < SSHFS || t > CIFS { + return "unknown" } - return hostfolders + return names[t] } -func getHostfolderByName(name string) *config.HostFolder { - hostfolder := getHostfolderByNameFromList(name, config.InstanceConfig.HostFolders) - if hostfolder != nil { - return hostfolder - } +type HostFolder interface { + // Config returns the host folder configuration for this HostFolder. + Config() config.HostFolderConfig - return getHostfolderByNameFromList(name, config.AllInstancesConfig.HostFolders) -} - -func getHostfolderByNameFromList(name string, hostfolders []config.HostFolder) *config.HostFolder { - for i := range hostfolders { - - hostfolder := hostfolders[i] - - if hostfolder.Name == name { - return &hostfolder - } - } + // Mount mounts the host folder specified by name into the running VM. nil is returned on success. + // An error is returned, if the VM is not running, the specified host folder does not exist or the mount fails. + Mount(driver drivers.Driver) error - return nil + // Umount umounts the host folder specified by name. nil is returned on success. + // An error is returned, if the VM is not running, the specified host folder does not exist or the mount fails. + Umount(driver drivers.Driver) error } diff --git a/pkg/minishift/hostfolder/hostfolder_manager.go b/pkg/minishift/hostfolder/hostfolder_manager.go new file mode 100644 index 0000000000..f80b870968 --- /dev/null +++ b/pkg/minishift/hostfolder/hostfolder_manager.go @@ -0,0 +1,262 @@ +/* +Copyright (C) 2017 Red Hat, Inc. + +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 hostfolder + +import ( + "errors" + "fmt" + "github.com/docker/machine/libmachine/drivers" + "github.com/docker/machine/libmachine/state" + miniConfig "github.com/minishift/minishift/pkg/minishift/config" + "github.com/minishift/minishift/pkg/minishift/hostfolder/config" + "strings" +) + +type MountInfo struct { + Name string + Type string + Source string + MountPoint string + Mounted bool +} + +// Manager is the central point for all operations around managing hostfolders. +type Manager struct { + instanceConfig *miniConfig.InstanceConfigType + allInstancesConfig *miniConfig.GlobalConfigType +} + +// NewAddOnManager creates a new add-on manager for the specified add-on directory. +func NewManager(instanceConfig *miniConfig.InstanceConfigType, allInstancesConfig *miniConfig.GlobalConfigType) (*Manager, error) { + return &Manager{ + instanceConfig: instanceConfig, + allInstancesConfig: allInstancesConfig}, nil +} + +// ExistAny returns true if at least one host folder configuration exists, false otherwise. +func (m *Manager) ExistAny() bool { + return len(m.instanceConfig.HostFolders) > 0 || + len(m.allInstancesConfig.HostFolders) > 0 +} + +// Exist returns true if the host folder with the specified name exist, false otherwise. +func (m *Manager) Exist(name string) bool { + return m.getHostFolder(name) != nil +} + +// Add adds teh specified host folder to the configuration. Depending on the allInstances flag the configuration is either +// saved to the instance configuration or the global all instances configuration. +func (m *Manager) Add(hostFolder HostFolder, allInstances bool) { + if allInstances { + m.allInstancesConfig.HostFolders = append(m.allInstancesConfig.HostFolders, hostFolder.Config()) + m.allInstancesConfig.Write() + } else { + m.instanceConfig.HostFolders = append(m.instanceConfig.HostFolders, hostFolder.Config()) + m.instanceConfig.Write() + } +} + +// Remove removes the specified host folder from the configuration. If the host folder does not exist an error is returned. +func (m *Manager) Remove(name string) error { + if !m.Exist(name) { + return fmt.Errorf("no host folder defined with name '%s'", name) + } + + m.instanceConfig.HostFolders = m.removeFromHostFolders(name, miniConfig.InstanceConfig.HostFolders) + m.instanceConfig.Write() + + m.allInstancesConfig.HostFolders = m.removeFromHostFolders(name, miniConfig.AllInstancesConfig.HostFolders) + m.allInstancesConfig.Write() + + return nil +} + +// List returns a list of MountInfo instances for the configured host folders. If an error occurs nil is returned +// together with the error. +func (m *Manager) List(driver drivers.Driver) ([]MountInfo, error) { + var isRunning bool + if driver != nil && drivers.MachineInState(driver, state.Running)() { + isRunning = true + } else { + isRunning = false + } + + if !m.ExistAny() { + return nil, errors.New("no host folders defined") + } + + procMounts := "" + if isRunning { + cmd := fmt.Sprint("cat /proc/mounts") + procMounts, _ = drivers.RunSSHCommandFromDriver(driver, cmd) + } + + hostfolders := miniConfig.AllInstancesConfig.HostFolders + hostfolders = append(hostfolders, miniConfig.InstanceConfig.HostFolders...) + var mounts []MountInfo + for _, hostFolder := range hostfolders { + + source := "" + switch hostFolder.Type { + case CIFS.String(): + source = hostFolder.Options[config.UncPath] + case SSHFS.String(): + source = hostFolder.Options[config.Source] + } + + mounted := false + if isRunning && strings.Contains(procMounts, hostFolder.MountPoint()) { + mounted = true + } + + mount := MountInfo{ + Name: hostFolder.Name, + Type: hostFolder.Type, + Source: source, + MountPoint: hostFolder.MountPoint(), + Mounted: mounted, + } + + mounts = append(mounts, mount) + } + + return mounts, nil +} + +// Mount mounts the host folder specified by name into the running VM. nil is returned on success. +// An error is returned, if the VM is not running, the specified host folder does not exist or the mount fails. +func (m *Manager) Mount(driver drivers.Driver, name string) error { + if !m.isHostRunning(driver) { + return errors.New("host is in the wrong state") + } + + hostFolder := m.getHostFolder(name) + if hostFolder == nil { + return fmt.Errorf("no host folder with name '%s' defined", name) + } + + m.ensureMountPointExists(driver, hostFolder.Config()) + err := hostFolder.Mount(driver) + if err != nil { + return err + } + return nil +} + +// MountAll mounts all defined host folders. +func (m *Manager) MountAll(driver drivers.Driver) error { + if !m.isHostRunning(driver) { + return errors.New("host is in the wrong state") + } + + if !m.ExistAny() { + return errors.New("no host folders defined") + } + + hostFolderConfigs := m.allInstancesConfig.HostFolders + hostFolderConfigs = append(hostFolderConfigs, m.instanceConfig.HostFolders...) + for _, hostFolderConfig := range hostFolderConfigs { + m.Mount(driver, hostFolderConfig.Name) + } + return nil +} + +// Umount umounts the host folder specified by name. nil is returned on success. +// An error is returned, if the VM is not running, the specified host folder does not exist or the mount fails. +func (m *Manager) Umount(driver drivers.Driver, name string) error { + if !m.isHostRunning(driver) { + return errors.New("host is in the wrong state") + } + + if !m.ExistAny() { + return errors.New("no host folders defined") + } + + hostFolder := m.getHostFolder(name) + if hostFolder == nil { + return fmt.Errorf("no host folder with the name '%s' defined", name) + } else { + err := hostFolder.Umount(driver) + if err != nil { + return err + } + } + return nil +} + +func (m *Manager) getHostFolder(name string) HostFolder { + config := m.getHostFolderConfig(name, miniConfig.InstanceConfig.HostFolders) + if config != nil { + return m.hostFolderForConfig(config) + } + + config = m.getHostFolderConfig(name, miniConfig.AllInstancesConfig.HostFolders) + if config != nil { + return m.hostFolderForConfig(config) + } + + return nil +} + +func (m *Manager) hostFolderForConfig(config *config.HostFolderConfig) HostFolder { + switch config.Type { + case CIFS.String(): + return NewCifsHostFolder(*config) + case SSHFS.String(): + return NewSSHFSHostFolder(*config, m.allInstancesConfig) + default: + return nil + } +} + +func (m *Manager) getHostFolderConfig(name string, hostFolderConfigs []config.HostFolderConfig) *config.HostFolderConfig { + for i := range hostFolderConfigs { + hostFolderConfig := hostFolderConfigs[i] + if hostFolderConfig.Name == name { + return &hostFolderConfig + } + } + + return nil +} + +func (m *Manager) isHostRunning(driver drivers.Driver) bool { + return drivers.MachineInState(driver, state.Running)() +} + +func (m *Manager) ensureMountPointExists(driver drivers.Driver, hostFolder config.HostFolderConfig) error { + cmd := fmt.Sprintf("sudo mkdir -p %s", hostFolder.MountPoint()) + + if _, err := drivers.RunSSHCommandFromDriver(driver, cmd); err != nil { + return err + } + + return nil +} + +func (m *Manager) removeFromHostFolders(name string, hostfolders []config.HostFolderConfig) []config.HostFolderConfig { + for i := range hostfolders { + + hostFolder := hostfolders[i] + + if hostFolder.Name == name { + hostfolders = append(hostfolders[:i], hostfolders[i+1:]...) + break + } + } + return hostfolders +} diff --git a/pkg/minishift/hostfolder/hostfolder_test.go b/pkg/minishift/hostfolder/hostfolder_test.go index f4ce974d40..5586fe9637 100644 --- a/pkg/minishift/hostfolder/hostfolder_test.go +++ b/pkg/minishift/hostfolder/hostfolder_test.go @@ -1,70 +1,10 @@ -/* -Copyright (C) 2017 Red Hat, Inc. - -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 hostfolder import ( - "testing" - - "github.com/docker/machine/libmachine/drivers" - "github.com/minishift/minishift/pkg/minikube/tests" "github.com/stretchr/testify/assert" - - instanceState "github.com/minishift/minishift/pkg/minishift/config" + "testing" ) -func setupMock() (*tests.SSHServer, *tests.MockDriver) { - mockSsh, _ := tests.NewSSHServer() - mockSsh.CommandToOutput = make(map[string]string) - port, _ := mockSsh.Start() - - mockDriver := &tests.MockDriver{ - Port: port, - BaseDriver: drivers.BaseDriver{ - IPAddress: "127.0.0.1", - SSHKeyPath: "", - }, - } - return mockSsh, mockDriver -} - -func setupHostFolder() *instanceState.HostFolder { - return &instanceState.HostFolder{ - Name: "Users", - Type: "cifs", - Options: map[string]string{ - "mountpoint": "", - "uncpath": "//127.0.0.1/Users", - "username": "joe@pillow.us", - "password": "am!g@4ever", - "domain": "DESKTOP-RHAIMSWIN", - }, - } -} - -func TestHostfolderIsMounted(t *testing.T) { - mockSsh, mockDriver := setupMock() - hostfolder := setupHostFolder() - - state := false - mockSsh.CommandToOutput["if grep -qs /mnt/sda1/Users /proc/mounts; then echo '1'; else echo '0'; fi"] = `0` - state, _ = isHostfolderMounted(mockDriver, hostfolder) - assert.False(t, state) - - mockSsh.CommandToOutput["if grep -qs /mnt/sda1/Users /proc/mounts; then echo '1'; else echo '0'; fi"] = `1` - state, _ = isHostfolderMounted(mockDriver, hostfolder) - assert.True(t, state) +func Test_type_string(t *testing.T) { + assert.Equal(t, CIFS.String(), "cifs", "unexpected string representation of host folder type") } diff --git a/pkg/minishift/hostfolder/sshfs_hostfolder.go b/pkg/minishift/hostfolder/sshfs_hostfolder.go new file mode 100644 index 0000000000..bb8c358a10 --- /dev/null +++ b/pkg/minishift/hostfolder/sshfs_hostfolder.go @@ -0,0 +1,182 @@ +/* +Copyright (C) 2017 Red Hat, Inc. + +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 hostfolder + +import ( + "fmt" + "github.com/docker/machine/libmachine/drivers" + "github.com/golang/glog" + miniConfig "github.com/minishift/minishift/pkg/minishift/config" + "github.com/minishift/minishift/pkg/minishift/hostfolder/config" + "github.com/minishift/minishift/pkg/util/os" + "github.com/minishift/minishift/pkg/util/os/process" + goos "os" + "os/exec" + "strings" + "syscall" +) + +const ( + keyFile = "/home/docker/.ssh/id_rsa" +) + +var ( + SftpPort = 2022 +) + +type SSHFSHostFolder struct { + config config.HostFolderConfig + globalConfig *miniConfig.GlobalConfigType +} + +func NewSSHFSHostFolder(config config.HostFolderConfig, globalConfig *miniConfig.GlobalConfigType) HostFolder { + return &SSHFSHostFolder{config: config, globalConfig: globalConfig} +} + +func (h *SSHFSHostFolder) Config() config.HostFolderConfig { + return h.config +} + +func (h *SSHFSHostFolder) Mount(driver drivers.Driver) error { + err := h.ensureSFTPDDaemonRunning() + if err != nil { + return err + } + + err = h.ensureRSAKeyExists(driver) + if err != nil { + return err + } + + ip, err := h.hostIP(driver) + if err != nil { + return err + } + + cmd := fmt.Sprintf( + "sudo sshfs docker@%s:%s %s -o IdentityFile=%s -o 'StrictHostKeyChecking=no' -o reconnect -o allow_other -o idmap=none -p %d", + ip, + h.config.Option(config.Source), + h.config.MountPoint(), + keyFile, + SftpPort) + + if glog.V(2) { + fmt.Println(cmd) + } + + if _, err := drivers.RunSSHCommandFromDriver(driver, cmd); err != nil { + return fmt.Errorf("error occured while mounting host folder: %s", err) + } + + return nil +} + +func (h *SSHFSHostFolder) Umount(driver drivers.Driver) error { + cmd := fmt.Sprintf("sudo umount %s", h.config.MountPoint()) + + if _, err := drivers.RunSSHCommandFromDriver(driver, cmd); err != nil { + return fmt.Errorf("error during umounting of host folder: %s", err) + } + + return nil +} + +func (h *SSHFSHostFolder) hostIP(driver drivers.Driver) (string, error) { + cmd := fmt.Sprint("sudo netstat -tapen | grep 'sshd: docker' | head -n1 | awk '{split($5, a, \":\"); print a[1]}'") + + out, err := drivers.RunSSHCommandFromDriver(driver, cmd) + if err != nil { + return "", err + } + + return strings.TrimSpace(out), nil +} + +func (h *SSHFSHostFolder) ensureSFTPDDaemonRunning() error { + running, err := h.isRunning() + if err != nil { + return err + } + + if running { + return nil + } + + sftpCmd, err := createSftpCommand() + if err != nil { + return err + } + + err = sftpCmd.Start() + if err != nil { + return err + } + + h.globalConfig.SftpdPID = sftpCmd.Process.Pid + h.globalConfig.Write() + return nil +} + +func (h *SSHFSHostFolder) ensureRSAKeyExists(driver drivers.Driver) error { + cmd := fmt.Sprintf("if [ ! -f %s ]; then ssh-keygen -t rsa -N \"\" -f %s; fi", keyFile, keyFile) + + _, err := drivers.RunSSHCommandFromDriver(driver, cmd) + if err != nil { + return err + } + + return nil +} + +func (h *SSHFSHostFolder) isRunning() (bool, error) { + if h.globalConfig.SftpdPID <= 0 { + return false, nil + } + + // TODO Issue #317 Check whether this works on Windows or whether OS specific code is needed here + process, err := goos.FindProcess(h.globalConfig.SftpdPID) + if err != nil { + return false, err + } + + err = process.Signal(syscall.Signal(0)) + if err == nil { + return true, nil + } else { + return false, nil + } +} + +func createSftpCommand() (*exec.Cmd, error) { + cmd, err := os.CurrentExecutable() + if err != nil { + return nil, err + } + + args := []string{ + "hostfolder", + "sftpd"} + exportCmd := exec.Command(cmd, args...) + // don't inherit any file handles + exportCmd.Stderr = nil + exportCmd.Stdin = nil + exportCmd.Stdout = nil + exportCmd.SysProcAttr = process.SysProcForBackgroundProcess() + exportCmd.Env = process.EnvForBackgroundProcess() + + return exportCmd, nil +} diff --git a/pkg/util/strings/strings.go b/pkg/util/strings/strings.go index 950acf3397..76bbda9b90 100644 --- a/pkg/util/strings/strings.go +++ b/pkg/util/strings/strings.go @@ -115,3 +115,7 @@ func SplitAndTrim(s string, separator string) ([]string, error) { return cleanSplit, nil } + +func ConvertSlashes(input string) string { + return strings.Replace(input, "\\", "/", -1) +}