From 2dd8dc8e7f551c0e40ff1dae749fcd78ce4738df Mon Sep 17 00:00:00 2001 From: Pedro Castillo Date: Tue, 26 Feb 2019 12:19:52 -0600 Subject: [PATCH] add ssh subcommand Signed-off-by: Pedro Castillo --- engines/docker/ssh.go | 108 +++++++++++++++++++++++++++++++++++ engines/engine.go | 12 ++-- pkg/cli/cli.go | 1 + pkg/cli/connect.go | 107 ----------------------------------- pkg/cli/ssh.go | 127 +++++++++++------------------------------- pkg/types/network.go | 8 +++ pkg/types/size.go | 12 ++++ pkg/types/vm.go | 28 ++++++++++ 8 files changed, 198 insertions(+), 205 deletions(-) create mode 100644 engines/docker/ssh.go delete mode 100644 pkg/cli/connect.go create mode 100644 pkg/types/network.go create mode 100644 pkg/types/size.go create mode 100644 pkg/types/vm.go diff --git a/engines/docker/ssh.go b/engines/docker/ssh.go new file mode 100644 index 0000000..e21575f --- /dev/null +++ b/engines/docker/ssh.go @@ -0,0 +1,108 @@ +package docker + +import ( + "io/ioutil" + "os" + "os/signal" + "syscall" + + "golang.org/x/crypto/ssh" + + "github.com/govm-project/govm/internal" + "github.com/govm-project/govm/pkg/homedir" + "github.com/govm-project/govm/pkg/termutil" +) + +func (e *Engine) SSHVM(namespace, id, user, key string, term *termutil.Terminal) error { + container, err := e.docker.Inspect(id) + if err != nil { + fullName := internal.GenerateContainerName(namespace, id) + container, err = e.docker.Inspect(fullName) + if err != nil { + return err + } + } + + ip := container.NetworkSettings.IPAddress + keyPath := homedir.ExpandPath(key) + + privateKey, err := ioutil.ReadFile(keyPath) + if err != nil { + return err + } + + signer, err := ssh.ParsePrivateKey(privateKey) + if err != nil { + return err + } + + config := ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ + ssh.PublicKeys(signer), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + config.SetDefaults() + + conn, err := ssh.Dial("tcp", ip+":22", &config) + if err != nil { + return err + } + defer conn.Close() + + sess, err := conn.NewSession() + if err != nil { + return err + } + defer sess.Close() + + sess.Stdin = term.In() + sess.Stdout = term.Out() + sess.Stderr = term.Err() + + sz, err := term.GetWinsize() + if err != nil { + return err + } + + err = term.MakeRaw() + if err != nil { + return err + } + defer term.Restore() + + err = sess.RequestPty(os.Getenv("TERM"), int(sz.Height), int(sz.Width), nil) + if err != nil { + return err + } + + err = sess.Shell() + if err != nil { + return err + } + + // If our terminal window changes, signal the ssh connection + stopch := make(chan struct{}) + defer close(stopch) + go func() { + sigch := make(chan os.Signal) + signal.Notify(sigch, syscall.SIGWINCH) + defer signal.Stop(sigch) + defer close(sigch) + outer: + for { + select { + case <-sigch: + sz, err := term.GetWinsize() + if err == nil { + sess.WindowChange(int(sz.Height), int(sz.Width)) + } + case <-stopch: + break outer + } + } + }() + + return sess.Wait() +} diff --git a/engines/engine.go b/engines/engine.go index b48aa45..bdd4034 100644 --- a/engines/engine.go +++ b/engines/engine.go @@ -1,13 +1,15 @@ -package main +package engines import ( + "github.com/govm-project/govm/pkg/termutil" "github.com/govm-project/govm/vm" ) // VMEngine stands as an abstraction for VMs management engines type VMEngine interface { - Create(spec vm.Instance) (string, error) - Start(namespace, id string) error - Delete(namespace, id string) error - List(namespace string, all bool) ([]vm.Instance, error) + CreateVM(spec vm.Instance) (string, error) + StartVM(namespace, id string) error + DeleteVM(namespace, id string) error + SSHVM(namespace, id, user, key string, term *termutil.Terminal) error + ListVM(namespace string, all bool) ([]vm.Instance, error) } diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 9e4c915..a141106 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -35,6 +35,7 @@ func New() (*cli.App, error) { &removeCommand, &startCommand, &composeCommand, + &sshCommand, }, }, nil } diff --git a/pkg/cli/connect.go b/pkg/cli/connect.go deleted file mode 100644 index c618faf..0000000 --- a/pkg/cli/connect.go +++ /dev/null @@ -1,107 +0,0 @@ -package cli - -import ( - "context" - "fmt" - "os" - "os/user" - "path/filepath" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/client" - "github.com/govm-project/govm/engines/docker" - log "github.com/sirupsen/logrus" - cli "gopkg.in/urfave/cli.v2" -) - -// TODO: Reduce cyclomatic complexity -var connectCommand = cli.Command{ - Name: "connect", - Aliases: []string{"conn"}, - Usage: "Get a shell from a vm", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "user", - Value: "", - Usage: "ssh login user", - }, - &cli.StringFlag{ - Name: "key", - Value: "", - Usage: "private key path (default: ~/.ssh/id_rsa)", - }, - }, - Action: func(c *cli.Context) error { - var name, loginUser, key string - var vmID int - nameFound := false - nargs := c.NArg() - switch { - case nargs == 1: - // Parse flags - if c.String("user") != "" { - loginUser = c.String("user") - } else { - usr, _ := user.Current() - loginUser = usr.Name - } - - if c.String("key") != "" { - key, _ = filepath.Abs(c.String("key")) - } else { - usr, err := user.Current() - if err != nil { - log.Fatal(err) - } - - key = usr.HomeDir + "/.ssh/id_rsa" - } - name = c.Args().First() - // TODO: Replace it when docker engine is properly addressed - docker.SetAPIVersion() - cli, err := client.NewEnvClient() - if err != nil { - panic(err) - } - listArgs := filters.NewArgs() - listArgs.Add("ancestor", VMLauncherContainerImage) - containers, err := cli.ContainerList(context.Background(), - types.ContainerListOptions{ - Quiet: false, - Size: false, - All: true, - Latest: false, - Since: "", - Before: "", - Limit: 0, - Filters: listArgs, - }) - if err != nil { - panic(err) - } - for id, container := range containers { - if container.Names[0][1:] == name { - nameFound = true - vmID = id - } - } - if !nameFound { - fmt.Printf("Unable to find a running vm with name: %s", name) - os.Exit(1) - } else { - vmIP := containers[vmID].NetworkSettings.Networks["bridge"].IPAddress - getNewSSHConn(loginUser, vmIP, key) - } - - case nargs == 0: - fmt.Println("No name provided as argument.") - os.Exit(1) - - case nargs > 1: - fmt.Println("Only one argument is allowed") - os.Exit(1) - } - return nil - }, -} diff --git a/pkg/cli/ssh.go b/pkg/cli/ssh.go index cfc1b6f..e29d0f0 100644 --- a/pkg/cli/ssh.go +++ b/pkg/cli/ssh.go @@ -1,104 +1,45 @@ package cli import ( - "bufio" "fmt" - "io/ioutil" - "log" - "os" - "golang.org/x/crypto/ssh" + "github.com/govm-project/govm/engines/docker" + "github.com/govm-project/govm/pkg/termutil" + cli "gopkg.in/urfave/cli.v2" ) -type password string - -func (p password) Password(user string) (password string, err error) { - return string(p), nil -} - -// TODO: Reduce cyclomatic complexity -func getNewSSHConn(username, hostname, key string) { // nolint: gocyclo - //var hostKey ssh.PublicKey - - privateKeyBytes, err := ioutil.ReadFile(key) - if err != nil { - log.Fatalf("Error on reading private key file: %v", err) - } - - // Create the Signer for this private key. - signer, err := ssh.ParsePrivateKey(privateKeyBytes) - if err != nil { - log.Fatalf("unable to parse private key: %v", err) - } - - // Create client config - config := &ssh.ClientConfig{ - User: username, - Auth: []ssh.AuthMethod{ - //ssh.Password("password"), - ssh.PublicKeys(signer), +var sshCommand = cli.Command{ + Name: "ssh", + Usage: "ssh into a running VM", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "user", + Aliases: []string{"u"}, + Usage: "login as this username", }, - //HostKeyCallback: ssh.FixedHostKey(hostKey), - HostKeyCallback: ssh.InsecureIgnoreHostKey(), - } - config.SetDefaults() - // Connect to ssh server - conn, err := ssh.Dial("tcp", hostname+":22", config) - if err != nil { - log.Fatal("unable to connect: ", err) - } - defer func() { - err := conn.Close() - // TODO: Change to warning when log package is changed - log.Println(err) - }() - - // Create a session - session, err := conn.NewSession() - if err != nil { - log.Fatal("unable to create session: ", err) - } - defer func() { - err := session.Close() - // TODO: Change to warning when log package is changed - log.Println(err) - }() - - // Set IO - session.Stdout = os.Stdout - session.Stderr = os.Stderr - in, err := session.StdinPipe() - if err != nil { - log.Fatal(err) - } - - // Set up terminal modes - modes := ssh.TerminalModes{ - ssh.ECHO: 0, // disable echoing - ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud - ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud - } - - // Request pseudo terminal - if err := session.RequestPty("xterm", 80, 40, modes); err != nil { - log.Fatalf("request for pseudo terminal failed: %s", err) - } - - // Start remote shell - if err := session.Shell(); err != nil { - log.Fatalf("failed to start shell: %s", err) - } - - // Accepting commands - for { - reader := bufio.NewReader(os.Stdin) - str, err := reader.ReadString('\n') - if err != nil { - log.Println("Error reading command") + &cli.StringFlag{ + Name: "key", + Aliases: []string{"k"}, + Usage: "ssh private key file", + Value: "~/.ssh/id_rsa", + }, + }, + Action: func(c *cli.Context) error { + if c.Args().Len() != 1 { + return fmt.Errorf("VM name required") } - _, err = fmt.Fprint(in, str) - if err != nil { - log.Println("Error reading command") + name := c.Args().First() + namespace := c.String("namespace") + user := c.String("user") + if user == "" { + return fmt.Errorf("--user argument required") } - } + key := c.String("key") + term := termutil.StdTerminal() + + engine := docker.Engine{} + engine.Init() + + return engine.SSHVM(namespace, name, user, key, term) + }, } diff --git a/pkg/types/network.go b/pkg/types/network.go new file mode 100644 index 0000000..4d41381 --- /dev/null +++ b/pkg/types/network.go @@ -0,0 +1,8 @@ +package types + +// Network represents a network for VMs +type Network struct { + ID string `yaml:"id" json:"id"` + Subnet string `yaml:"subnet" json:"subnet"` + DNS []string `yaml:"dns" json:"dns"` +} diff --git a/pkg/types/size.go b/pkg/types/size.go new file mode 100644 index 0000000..0bd12fd --- /dev/null +++ b/pkg/types/size.go @@ -0,0 +1,12 @@ +package types + +// Size represents all hardware spec definitions for the VM +type Size struct { + Name string `yaml:"name" json:"name"` + CPUModel string `yaml:"cpu-model" json:"cpu_model"` + CPUs uint `yaml:"cpus" json:"cpus"` + Cores uint `yaml:"cores" json:"cores"` + Threads uint `yaml:"threads" json:"threads"` + Sockets uint `yaml:"sockets" json:"sockets"` + Memory uint `yaml:"memory" json:"memory"` +} diff --git a/pkg/types/vm.go b/pkg/types/vm.go new file mode 100644 index 0000000..facaf6e --- /dev/null +++ b/pkg/types/vm.go @@ -0,0 +1,28 @@ +package types + +import ( + "net" +) + +// VM represents a Virtual Machine guest +type VM struct { + Name string `yaml:"name" json:"name"` + Namespace string `yaml:"namespace" json:"namespace"` + ParentImage string `yaml:"image" json:"image"` + Size Size `yaml:"size" json:"size"` + SSHKey string `yaml:"sshkey" json:"sshkey"` + UserData string `yaml:"user-data" json:"user_data"` + Cloud bool `yaml:"cloud" json:"cloud"` + Efi bool `yaml:"efi" json:"efi"` + AutoRemove bool `yaml:"auto-remove" json:"auto_remove"` + Network VMNetOpts `yaml:"network" json:"network"` + Shares []string `yaml:"shares" json:"shares"` + EmulatorEnv []string `yaml:"emulator-env" json:"emulator_env"` +} + +// VMNetOpts represents a VM's options for connecting to a network +type VMNetOpts struct { + NetID string + IP net.IP + MAC net.HardwareAddr +}