-
Notifications
You must be signed in to change notification settings - Fork 228
Add new command: ignite exec
#232
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
} |
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") | ||
} |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There could be a blank line between the closing brace and There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here a blank line as well There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a scenario where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think so, but for the moment this matches There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Defaulting to |
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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 | ||
} |
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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
vm -> VM
There was a problem hiding this comment.
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?