Skip to content
This repository has been archived by the owner on Dec 7, 2023. It is now read-only.

Commit

Permalink
implement ignite exec
Browse files Browse the repository at this point in the history
  • Loading branch information
BenTheElder committed Jul 24, 2019
1 parent 72875d0 commit a403351
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 2 deletions.
13 changes: 13 additions & 0 deletions cmd/ignite/cmd/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cmd

import (
"io"

"github.com/spf13/cobra"
"github.com/weaveworks/ignite/cmd/ignite/cmd/vmcmd"
)

// NewCmdExec is an alias for vmcmd.NewCmdExec
func NewCmdExec(out io.Writer, err io.Writer, in io.Reader) *cobra.Command {
return vmcmd.NewCmdExec(out, err, in)
}
1 change: 1 addition & 0 deletions cmd/ignite/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func NewIgniteCommand(in io.Reader, out, err io.Writer) *cobra.Command {
root.AddCommand(NewCmdRmk(os.Stdout))
root.AddCommand(NewCmdRun(os.Stdout))
root.AddCommand(NewCmdSSH(os.Stdout))
root.AddCommand(NewCmdExec(os.Stdout, os.Stderr, os.Stdin))
root.AddCommand(NewCmdStart(os.Stdout))
root.AddCommand(NewCmdStop(os.Stdout))
root.AddCommand(NewCmdVersion(os.Stdout))
Expand Down
46 changes: 46 additions & 0 deletions cmd/ignite/cmd/vmcmd/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package vmcmd

import (
"io"

"github.com/lithammer/dedent"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/weaveworks/ignite/cmd/ignite/run"
"github.com/weaveworks/ignite/pkg/errutils"
)

// NewCmdExec exec's into a running VM
func NewCmdExec(out io.Writer, err io.Writer, in io.Reader) *cobra.Command {
ef := &run.ExecFlags{}

cmd := &cobra.Command{
Use: "exec <vm> <command...>",
Short: "execute a command in a running VM",
Long: dedent.Dedent(`
Execute a command in a running VM using SSH and the private key created for it during generation.
If no private key was created or wanting to use a different identity file,
use the identity file flag (-i, --identity) to override the used identity file.
The given VM is matched by prefix based on its ID and name.
`),
Args: cobra.MinimumNArgs(2),
Run: func(cmd *cobra.Command, args []string) {
errutils.Check(func() error {
eo, err := ef.NewExecOptions(args[0], args[1:]...)
if err != nil {
return err
}

return run.Exec(eo)
}())
},
}

addExecFlags(cmd.Flags(), ef)
return cmd
}

func addExecFlags(fs *pflag.FlagSet, ef *run.ExecFlags) {
fs.StringVarP(&ef.IdentityFile, "identity", "i", "", "Override the vm's default identity file")
fs.Uint32VarP(&ef.Timeout, "timeout", "t", 10, "Timeout waiting for connection in seconds")
}
155 changes: 155 additions & 0 deletions cmd/ignite/run/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package run

import (
"fmt"
"io"
"io/ioutil"
"net"
"os"
"path"
"time"

api "github.com/weaveworks/ignite/pkg/apis/ignite"
"github.com/weaveworks/ignite/pkg/constants"
"github.com/weaveworks/ignite/pkg/util"
"golang.org/x/crypto/ssh"
shellescape "gopkg.in/alessio/shellescape.v1"
)

type ExecFlags struct {
Timeout uint32
IdentityFile string
}

type execOptions struct {
*ExecFlags
vm *api.VM
command []string
}

func (ef *ExecFlags) NewExecOptions(vmMatch string, command ...string) (eo *execOptions, err error) {
eo = &execOptions{
ExecFlags: ef,
command: command,
}

eo.vm, err = getVMForMatch(vmMatch)
return
}

func Exec(eo *execOptions) error {
// Check if the VM is running
if !eo.vm.Running() {
return fmt.Errorf("VM %q is not running", eo.vm.GetUID())
}

// Get the IP address
ipAddrs := eo.vm.Status.IPAddresses
if len(ipAddrs) == 0 {
return fmt.Errorf("VM %q has no usable IP addresses", eo.vm.GetUID())
}

// If an external identity file is specified, use it instead of the internal one
privKeyFile := eo.IdentityFile
if len(privKeyFile) == 0 {
privKeyFile = path.Join(eo.vm.ObjectPath(), fmt.Sprintf(constants.VM_SSH_KEY_TEMPLATE, eo.vm.GetUID()))
if !util.FileExists(privKeyFile) {
return fmt.Errorf("no private key found for VM %q", eo.vm.GetUID())
}
}

signer, err := newSignerForKey(privKeyFile)
if err != nil {
return fmt.Errorf("unable to create signer for private key: %v", err)
}

// Create an SSH client, and connect, we will use this to exec
config := newSSHConfig(signer, eo.Timeout)
client, err := ssh.Dial("tcp", net.JoinHostPort(ipAddrs[0].String(), "22"), config)
if err != nil {
return fmt.Errorf("failed to dial: %v", err)
}

// Run the command, DO NOT wrap this error as the caller can check for the command exit
// code in the ssh.ExitError type
return runSSHCommand(client, eo.command)
}

func newSignerForKey(keyPath string) (ssh.Signer, error) {
key, err := ioutil.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("unable to read private key: %v", err)
}

// Create the Signer for this private key.
return ssh.ParsePrivateKey(key)
}

func newSSHConfig(publicKey ssh.Signer, timeout uint32) *ssh.ClientConfig {
return &ssh.ClientConfig{
User: "root",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(publicKey),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO: use ssh.FixedPublicKey instead
Timeout: time.Second * time.Duration(timeout),
}
}

func runSSHCommand(client *ssh.Client, command []string) error {
// create a session for the command
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("failed to create session: %v", err)
}
defer session.Close()

// get a pty
// TODO: should these be based on the host terminal?
// TODO: should we request something other than xterm?
// TODO: we should probably configure the terminal modes
modes := ssh.TerminalModes{}
if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
return fmt.Errorf("request for pseudo terminal failed: %v", err)
}

// connect input / output
// TODO: these should come from the cobra command instead of hardcoding os.Stderr etc.
stderr, err := session.StderrPipe()
if err != nil {
return fmt.Errorf("failed to connect stderr: %v", err)
}
go io.Copy(os.Stderr, stderr)
stdout, err := session.StdoutPipe()
if err != nil {
return fmt.Errorf("failed to connect stdout: %v", err)
}
go io.Copy(os.Stdout, stdout)
stdin, err := session.StdinPipe()
if err != nil {
return fmt.Errorf("failed to connect stdin: %v", err)
}
go io.Copy(stdin, os.Stdin)

/*
Do not wrap this error so the caller can check for the exit code
If the remote server does not send an exit status, an error of type *ExitMissingError is returned.
If the command completes unsuccessfully or is interrupted by a signal, the error is of type *ExitError.
Other error types may be returned for I/O problems.
*/
return session.Run(joinShellCommand(command))
}

// joinShellCommand joins command parts into a single string safe for passing to sh -c (or SSH)
func joinShellCommand(command []string) string {
joined := command[0]
if len(command) == 1 {
return joined
}
for _, arg := range command[1:] {
// NOTE: we need to escape / quote to ensure that
// each component of command... is read as a single shell word
joined += " " + shellescape.Quote(arg)
}
return joined
}
2 changes: 0 additions & 2 deletions cmd/ignite/run/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import (
api "github.com/weaveworks/ignite/pkg/apis/ignite"
"github.com/weaveworks/ignite/pkg/constants"
"github.com/weaveworks/ignite/pkg/util"

_ "golang.org/x/crypto/ssh"
)

type SSHFlags struct {
Expand Down
1 change: 1 addition & 0 deletions docs/cli/ignite.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Example usage:
* [ignite attach](ignite_attach.md) - Attach to a running VM
* [ignite completion](ignite_completion.md) - Output bash completion for ignite to stdout
* [ignite create](ignite_create.md) - Create a new VM without starting it
* [ignite exec](ignite_exec.md) - execute a command in a running VM
* [ignite gitops](ignite_gitops.md) - Run the GitOps feature of Ignite
* [ignite image](ignite_image.md) - Manage base images for VMs
* [ignite inspect](ignite_inspect.md) - Inspect an Ignite Object
Expand Down
36 changes: 36 additions & 0 deletions docs/cli/ignite_exec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## ignite exec

execute a command in a running VM

### Synopsis


Execute a command in a running VM using SSH and the private key created for it during generation.
If no private key was created or wanting to use a different identity file,
use the identity file flag (-i, --identity) to override the used identity file.
The given VM is matched by prefix based on its ID and name.


```
ignite exec <vm> <command...> [flags]
```

### Options

```
-h, --help help for exec
-i, --identity string Override the vm's default identity file
-t, --timeout uint32 Timeout waiting for connection in seconds (default 10)
```

### Options inherited from parent commands

```
--log-level loglevel Specify the loglevel for the program (default info)
-q, --quiet The quiet mode allows for machine-parsable output, by printing only IDs
```

### SEE ALSO

* [ignite](ignite.md) - ignite: easily run Firecracker VMs

5 changes: 5 additions & 0 deletions pkg/errutils/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"os"
"strings"

"golang.org/x/crypto/ssh"
)

const (
Expand Down Expand Up @@ -34,6 +36,9 @@ func Check(err error) {
switch err.(type) {
case nil:
return
case *ssh.ExitError:
exitError := err.(*ssh.ExitError)
fatal(err.Error(), exitError.ExitStatus())
default:
fatal(err.Error(), DefaultErrorExitCode)
}
Expand Down

0 comments on commit a403351

Please sign in to comment.