mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-27 00:46:25 -06:00
c9e9e374bb
This is needed as preperation for adding WinRM support. There is still one error in the tests which needs another look, but other than that it seems like were now ready to start working on the WinRM part…
164 lines
4.5 KiB
Go
164 lines
4.5 KiB
Go
package ssh
|
|
|
|
import (
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/hashicorp/terraform/terraform"
|
|
"github.com/mitchellh/go-homedir"
|
|
"github.com/mitchellh/mapstructure"
|
|
"golang.org/x/crypto/ssh"
|
|
"golang.org/x/crypto/ssh/agent"
|
|
)
|
|
|
|
const (
|
|
// DefaultUser is used if there is no default user given
|
|
DefaultUser = "root"
|
|
|
|
// DefaultPort is used if there is no port given
|
|
DefaultPort = 22
|
|
|
|
// DefaultScriptPath is used as the path to copy the file to
|
|
// for remote execution if not provided otherwise.
|
|
DefaultScriptPath = "/tmp/script_%RAND%.sh"
|
|
|
|
// DefaultTimeout is used if there is no timeout given
|
|
DefaultTimeout = 5 * time.Minute
|
|
)
|
|
|
|
// ConnectionInfo is decoded from the ConnInfo of the resource. These are the
|
|
// only keys we look at. If a KeyFile is given, that is used instead
|
|
// of a password.
|
|
type ConnectionInfo struct {
|
|
User string
|
|
Password string
|
|
KeyFile string `mapstructure:"key_file"`
|
|
Host string
|
|
Port int
|
|
Agent bool
|
|
Timeout string
|
|
ScriptPath string `mapstructure:"script_path"`
|
|
TimeoutVal time.Duration `mapstructure:"-"`
|
|
}
|
|
|
|
// ParseConnectionInfo is used to convert the ConnInfo of the InstanceState into
|
|
// a ConnectionInfo struct
|
|
func ParseConnectionInfo(s *terraform.InstanceState) (*ConnectionInfo, error) {
|
|
connInfo := &ConnectionInfo{}
|
|
decConf := &mapstructure.DecoderConfig{
|
|
WeaklyTypedInput: true,
|
|
Result: connInfo,
|
|
}
|
|
dec, err := mapstructure.NewDecoder(decConf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := dec.Decode(s.Ephemeral.ConnInfo); err != nil {
|
|
return nil, err
|
|
}
|
|
if connInfo.User == "" {
|
|
connInfo.User = DefaultUser
|
|
}
|
|
if connInfo.Port == 0 {
|
|
connInfo.Port = DefaultPort
|
|
}
|
|
if connInfo.ScriptPath == "" {
|
|
connInfo.ScriptPath = DefaultScriptPath
|
|
}
|
|
if connInfo.Timeout != "" {
|
|
connInfo.TimeoutVal = safeDuration(connInfo.Timeout, DefaultTimeout)
|
|
} else {
|
|
connInfo.TimeoutVal = DefaultTimeout
|
|
}
|
|
return connInfo, nil
|
|
}
|
|
|
|
// safeDuration returns either the parsed duration or a default value
|
|
func safeDuration(dur string, defaultDur time.Duration) time.Duration {
|
|
d, err := time.ParseDuration(dur)
|
|
if err != nil {
|
|
log.Printf("Invalid duration '%s', using default of %s", dur, defaultDur)
|
|
return defaultDur
|
|
}
|
|
return d
|
|
}
|
|
|
|
// PrepareSSHConfig is used to turn the *ConnectionInfo provided into a
|
|
// usable *SSHConfig for client initialization.
|
|
func PrepareSSHConfig(connInfo *ConnectionInfo) (*SSHConfig, error) {
|
|
var conn net.Conn
|
|
var err error
|
|
|
|
sshConf := &ssh.ClientConfig{
|
|
User: connInfo.User,
|
|
}
|
|
if connInfo.Agent {
|
|
sshAuthSock := os.Getenv("SSH_AUTH_SOCK")
|
|
|
|
if sshAuthSock == "" {
|
|
return nil, fmt.Errorf("SSH Requested but SSH_AUTH_SOCK not-specified")
|
|
}
|
|
|
|
conn, err = net.Dial("unix", sshAuthSock)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error connecting to SSH_AUTH_SOCK: %v", err)
|
|
}
|
|
// I need to close this but, later after all connections have been made
|
|
// defer conn.Close()
|
|
signers, err := agent.NewClient(conn).Signers()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error getting keys from ssh agent: %v", err)
|
|
}
|
|
|
|
sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signers...))
|
|
}
|
|
if connInfo.KeyFile != "" {
|
|
fullPath, err := homedir.Expand(connInfo.KeyFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to expand home directory: %v", err)
|
|
}
|
|
key, err := ioutil.ReadFile(fullPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to read key file '%s': %v", connInfo.KeyFile, err)
|
|
}
|
|
|
|
// We parse the private key on our own first so that we can
|
|
// show a nicer error if the private key has a password.
|
|
block, _ := pem.Decode(key)
|
|
if block == nil {
|
|
return nil, fmt.Errorf(
|
|
"Failed to read key '%s': no key found", connInfo.KeyFile)
|
|
}
|
|
if block.Headers["Proc-Type"] == "4,ENCRYPTED" {
|
|
return nil, fmt.Errorf(
|
|
"Failed to read key '%s': password protected keys are\n"+
|
|
"not supported. Please decrypt the key prior to use.", connInfo.KeyFile)
|
|
}
|
|
|
|
signer, err := ssh.ParsePrivateKey(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to parse key file '%s': %v", connInfo.KeyFile, err)
|
|
}
|
|
|
|
sshConf.Auth = append(sshConf.Auth, ssh.PublicKeys(signer))
|
|
}
|
|
if connInfo.Password != "" {
|
|
sshConf.Auth = append(sshConf.Auth,
|
|
ssh.Password(connInfo.Password))
|
|
sshConf.Auth = append(sshConf.Auth,
|
|
ssh.KeyboardInteractive(PasswordKeyboardInteractive(connInfo.Password)))
|
|
}
|
|
host := fmt.Sprintf("%s:%d", connInfo.Host, connInfo.Port)
|
|
config := &SSHConfig{
|
|
Config: sshConf,
|
|
Connection: ConnectFunc("tcp", host),
|
|
SSHAgentConn: conn,
|
|
}
|
|
return config, nil
|
|
}
|