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

Add new command: ignite exec #232

Merged
merged 3 commits into from
Jul 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vm -> VM

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this matches the ssh command's flags exactly at the moment, should we update it there too?

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There could be a blank line between the closing brace and eo.vm to help readability

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here a blank line as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a scenario where root may not be the user wanted for ssh - VMs with "disable root login"?

Copy link
Contributor Author

@BenTheElder BenTheElder Jul 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, but for the moment this matches ignite ssh which also assumes the user is root.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The disable root login option default in most distributions prevents password-based authentication, but SSH keys should work just fine. We should also open an issue about allowing user selection in ssh and exec.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaulting to xterm is fine for now, we could later also allow changing this via a flag. Anyways this is non-blocking for this initial implementation.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rgee0 @matipan is this method/package any use to us with faas-cli or ofc-bootstrap?

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
}
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

Loading