cloud/shepherd: move ssh client to own package
Change-Id: I56ad16f8f2f355243c5c0414656bbfbbff1faef5
Reviewed-on: https://review.monogon.dev/c/monogon/+/2791
Reviewed-by: Serge Bazanski <serge@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/shepherd/manager/BUILD.bazel b/cloud/shepherd/manager/BUILD.bazel
index 4119ff7..aa61caa 100644
--- a/cloud/shepherd/manager/BUILD.bazel
+++ b/cloud/shepherd/manager/BUILD.bazel
@@ -9,7 +9,6 @@
"manager.go",
"provisioner.go",
"recoverer.go",
- "ssh_client.go",
"ssh_key_signer.go",
],
importpath = "source.monogon.dev/cloud/shepherd/manager",
@@ -21,8 +20,8 @@
"//cloud/bmaas/bmdb/model",
"//cloud/shepherd",
"//go/mflags",
+ "//go/net/ssh",
"@com_github_google_uuid//:uuid",
- "@com_github_pkg_sftp//:sftp",
"@io_k8s_klog_v2//:klog",
"@org_golang_google_protobuf//proto",
"@org_golang_x_crypto//ssh",
@@ -47,6 +46,7 @@
"//cloud/bmaas/bmdb/model",
"//cloud/lib/component",
"//cloud/shepherd",
+ "//go/net/ssh",
"@com_github_google_uuid//:uuid",
"@io_k8s_klog_v2//:klog",
"@org_golang_x_time//rate",
diff --git a/cloud/shepherd/manager/fake_ssh_client.go b/cloud/shepherd/manager/fake_ssh_client.go
index 1d9d371..db13b5b 100644
--- a/cloud/shepherd/manager/fake_ssh_client.go
+++ b/cloud/shepherd/manager/fake_ssh_client.go
@@ -5,20 +5,23 @@
"crypto/ed25519"
"crypto/rand"
"fmt"
+ "io"
"time"
"google.golang.org/protobuf/proto"
apb "source.monogon.dev/cloud/agent/api"
+
+ "source.monogon.dev/go/net/ssh"
)
-// FakeSSHClient is an SSHClient that pretends to start an agent, but in reality
+// FakeSSHClient is an Client that pretends to start an agent, but in reality
// just responds with what an agent would respond on every execution attempt.
type FakeSSHClient struct{}
type fakeSSHConnection struct{}
-func (f *FakeSSHClient) Dial(ctx context.Context, address string, timeout time.Duration) (SSHConnection, error) {
+func (f *FakeSSHClient) Dial(ctx context.Context, address string, timeout time.Duration) (ssh.Connection, error) {
return &fakeSSHConnection{}, nil
}
@@ -46,7 +49,7 @@
return arspb, nil, nil
}
-func (f *fakeSSHConnection) Upload(ctx context.Context, targetPath string, data []byte) error {
+func (f *fakeSSHConnection) Upload(ctx context.Context, targetPath string, _ io.Reader) error {
if targetPath != "/fake/path" {
return fmt.Errorf("unexpected target path in test")
}
diff --git a/cloud/shepherd/manager/initializer.go b/cloud/shepherd/manager/initializer.go
index 5abbc68..2c04c43 100644
--- a/cloud/shepherd/manager/initializer.go
+++ b/cloud/shepherd/manager/initializer.go
@@ -1,6 +1,7 @@
package manager
import (
+ "bytes"
"context"
"crypto/ed25519"
"crypto/x509"
@@ -23,6 +24,7 @@
"source.monogon.dev/cloud/bmaas/bmdb/metrics"
"source.monogon.dev/cloud/bmaas/bmdb/model"
"source.monogon.dev/cloud/shepherd"
+ "source.monogon.dev/go/net/ssh"
)
// InitializerConfig configures how the Initializer will deploy Agents on
@@ -126,13 +128,13 @@
type Initializer struct {
InitializerConfig
- sshClient SSHClient
+ sshClient ssh.Client
p shepherd.Provider
}
// NewInitializer creates an Initializer instance, checking the
// InitializerConfig, SharedConfig and AgentConfig for errors.
-func NewInitializer(p shepherd.Provider, sshClient SSHClient, ic InitializerConfig) (*Initializer, error) {
+func NewInitializer(p shepherd.Provider, sshClient ssh.Client, ic InitializerConfig) (*Initializer, error) {
if err := ic.Check(); err != nil {
return nil, err
}
@@ -219,7 +221,7 @@
// Upload the agent executable.
klog.Infof("Uploading the agent executable (machine ID: %s, addr: %s).", mid, addr)
- if err := conn.Upload(sctx, i.TargetPath, i.Executable); err != nil {
+ if err := conn.Upload(sctx, i.TargetPath, bytes.NewReader(i.Executable)); err != nil {
return nil, fmt.Errorf("while uploading agent executable: %w", err)
}
klog.V(1).Infof("Upload successful (machine ID: %s, addr: %s).", mid, addr)
diff --git a/cloud/shepherd/manager/provider_test.go b/cloud/shepherd/manager/provider_test.go
index b18dc45..4cdfb18 100644
--- a/cloud/shepherd/manager/provider_test.go
+++ b/cloud/shepherd/manager/provider_test.go
@@ -12,6 +12,7 @@
"source.monogon.dev/cloud/bmaas/bmdb"
"source.monogon.dev/cloud/bmaas/bmdb/model"
"source.monogon.dev/cloud/shepherd"
+ "source.monogon.dev/go/net/ssh"
)
type dummyMachine struct {
@@ -38,17 +39,17 @@
}
type dummySSHClient struct {
- SSHClient
+ ssh.Client
dp *dummyProvider
}
type dummySSHConnection struct {
- SSHConnection
+ ssh.Connection
m *dummyMachine
}
func (dsc *dummySSHConnection) Execute(ctx context.Context, command string, stdin []byte) ([]byte, []byte, error) {
- stdout, stderr, err := dsc.SSHConnection.Execute(ctx, command, stdin)
+ stdout, stderr, err := dsc.Connection.Execute(ctx, command, stdin)
if err != nil {
return nil, nil, err
}
@@ -57,8 +58,8 @@
return stdout, stderr, nil
}
-func (dsc *dummySSHClient) Dial(ctx context.Context, address string, timeout time.Duration) (SSHConnection, error) {
- conn, err := dsc.SSHClient.Dial(ctx, address, timeout)
+func (dsc *dummySSHClient) Dial(ctx context.Context, address string, timeout time.Duration) (ssh.Connection, error) {
+ conn, err := dsc.Client.Dial(ctx, address, timeout)
if err != nil {
return nil, err
}
@@ -77,10 +78,10 @@
return &dummySSHConnection{conn, m}, nil
}
-func (dp *dummyProvider) sshClient() SSHClient {
+func (dp *dummyProvider) sshClient() ssh.Client {
return &dummySSHClient{
- SSHClient: &FakeSSHClient{},
- dp: dp,
+ Client: &FakeSSHClient{},
+ dp: dp,
}
}
diff --git a/cloud/shepherd/manager/ssh_client.go b/cloud/shepherd/manager/ssh_client.go
deleted file mode 100644
index a1a305a..0000000
--- a/cloud/shepherd/manager/ssh_client.go
+++ /dev/null
@@ -1,143 +0,0 @@
-package manager
-
-import (
- "bytes"
- "context"
- "fmt"
- "io"
- "net"
- "time"
-
- "github.com/pkg/sftp"
- "golang.org/x/crypto/ssh"
-)
-
-// SSHClient defines a simple interface to an abstract SSH client. Usually this
-// would be PlainSSHClient, but tests can use this interface to dependency-inject
-// fake SSH connections.
-type SSHClient interface {
- // Dial returns an SSHConnection to a given address (host:port pair) with
- // a timeout for connection.
- Dial(ctx context.Context, address string, connectTimeout time.Duration) (SSHConnection, error)
-}
-
-type SSHConnection interface {
- // Execute a given command on a remote host synchronously, passing in stdin as
- // input, and returning a captured stdout/stderr. The returned data might be
- // valid even when err != nil, which might happen if the remote side returned a
- // non-zero exit code.
- Execute(ctx context.Context, command string, stdin []byte) (stdout []byte, stderr []byte, err error)
- // Upload a given blob to a targetPath on the system and make executable.
- Upload(ctx context.Context, targetPath string, data []byte) error
- // Close this connection.
- Close() error
-}
-
-// PlainSSHClient implements SSHClient (and SSHConnection) using
-// golang.org/x/crypto/ssh.
-type PlainSSHClient struct {
- AuthMethod ssh.AuthMethod
- Username string
-}
-
-type plainSSHConn struct {
- cl *ssh.Client
-}
-
-func (p *PlainSSHClient) Dial(ctx context.Context, address string, connectTimeout time.Duration) (SSHConnection, error) {
- d := net.Dialer{
- Timeout: connectTimeout,
- }
- conn, err := d.DialContext(ctx, "tcp", address)
- if err != nil {
- return nil, err
- }
- conf := &ssh.ClientConfig{
- User: p.Username,
- Auth: []ssh.AuthMethod{
- p.AuthMethod,
- },
- // Ignore the host key, since it's likely the first time anything logs into
- // this device, and also because there's no way of knowing its fingerprint.
- HostKeyCallback: ssh.InsecureIgnoreHostKey(),
- // Timeout sets a bound on the time it takes to set up the connection, but
- // not on total session time.
- Timeout: connectTimeout,
- }
- conn2, chanC, reqC, err := ssh.NewClientConn(conn, address, conf)
- if err != nil {
- return nil, err
- }
- cl := ssh.NewClient(conn2, chanC, reqC)
- return &plainSSHConn{
- cl: cl,
- }, nil
-}
-
-func (p *plainSSHConn) Execute(ctx context.Context, command string, stdin []byte) (stdout []byte, stderr []byte, err error) {
- sess, err := p.cl.NewSession()
- if err != nil {
- return nil, nil, fmt.Errorf("while creating SSH session: %w", err)
- }
- stdoutBuf := bytes.NewBuffer(nil)
- stderrBuf := bytes.NewBuffer(nil)
- sess.Stdin = bytes.NewBuffer(stdin)
- sess.Stdout = stdoutBuf
- sess.Stderr = stderrBuf
- defer sess.Close()
-
- if err := sess.Start(command); err != nil {
- return nil, nil, err
- }
- doneC := make(chan error, 1)
- go func() {
- doneC <- sess.Wait()
- }()
- select {
- case <-ctx.Done():
- return nil, nil, ctx.Err()
- case err := <-doneC:
- return stdoutBuf.Bytes(), stderrBuf.Bytes(), err
- }
-}
-
-func (p *plainSSHConn) Upload(ctx context.Context, targetPath string, data []byte) error {
- sc, err := sftp.NewClient(p.cl)
- if err != nil {
- return fmt.Errorf("while building sftp client: %w", err)
- }
- defer sc.Close()
-
- acrdr := bytes.NewReader(data)
- df, err := sc.Create(targetPath)
- if err != nil {
- return fmt.Errorf("while creating file on the host: %w", err)
- }
-
- doneC := make(chan error, 1)
-
- go func() {
- _, err := io.Copy(df, acrdr)
- df.Close()
- doneC <- err
- }()
-
- select {
- case err := <-doneC:
- if err != nil {
- return fmt.Errorf("while copying file: %w", err)
- }
- case <-ctx.Done():
- df.Close()
- return ctx.Err()
- }
-
- if err := sc.Chmod(targetPath, 0755); err != nil {
- return fmt.Errorf("while setting file permissions: %w", err)
- }
- return nil
-}
-
-func (p *plainSSHConn) Close() error {
- return p.cl.Close()
-}
diff --git a/cloud/shepherd/mini/BUILD.bazel b/cloud/shepherd/mini/BUILD.bazel
index 63ad885..5fad3c2 100644
--- a/cloud/shepherd/mini/BUILD.bazel
+++ b/cloud/shepherd/mini/BUILD.bazel
@@ -16,6 +16,7 @@
"//cloud/lib/component",
"//cloud/shepherd",
"//cloud/shepherd/manager",
+ "//go/net/ssh",
"//metropolis/cli/pkg/context",
"@io_k8s_klog_v2//:klog",
"@org_golang_x_crypto//ssh",
diff --git a/cloud/shepherd/mini/ssh.go b/cloud/shepherd/mini/ssh.go
index 99f3e90..1a2d23c 100644
--- a/cloud/shepherd/mini/ssh.go
+++ b/cloud/shepherd/mini/ssh.go
@@ -4,10 +4,11 @@
"flag"
"fmt"
- "golang.org/x/crypto/ssh"
+ xssh "golang.org/x/crypto/ssh"
"k8s.io/klog/v2"
"source.monogon.dev/cloud/shepherd/manager"
+ "source.monogon.dev/go/net/ssh"
)
type sshConfig struct {
@@ -36,18 +37,18 @@
sc.SSHKey.RegisterFlags()
}
-func (sc *sshConfig) NewClient() (*manager.PlainSSHClient, error) {
+func (sc *sshConfig) NewClient() (*ssh.DirectClient, error) {
if err := sc.check(); err != nil {
return nil, err
}
- c := manager.PlainSSHClient{
+ c := ssh.DirectClient{
Username: sc.User,
}
switch {
case sc.Pass != "":
- c.AuthMethod = ssh.Password(sc.Pass)
+ c.AuthMethod = xssh.Password(sc.Pass)
case sc.SSHKey.KeyPersistPath != "":
signer, err := sc.SSHKey.Signer()
if err != nil {
@@ -61,7 +62,7 @@
klog.Infof("Using ssh key auth with public key: %s", pubKey)
- c.AuthMethod = ssh.PublicKeys(signer)
+ c.AuthMethod = xssh.PublicKeys(signer)
}
return &c, nil
}
diff --git a/cloud/shepherd/provider/equinix/BUILD.bazel b/cloud/shepherd/provider/equinix/BUILD.bazel
index 727fbcf..4214f16 100644
--- a/cloud/shepherd/provider/equinix/BUILD.bazel
+++ b/cloud/shepherd/provider/equinix/BUILD.bazel
@@ -20,6 +20,7 @@
"//cloud/lib/sinbin",
"//cloud/shepherd",
"//cloud/shepherd/manager",
+ "//go/net/ssh",
"//metropolis/cli/pkg/context",
"@com_github_packethost_packngo//:packngo",
"@io_k8s_klog_v2//:klog",
diff --git a/cloud/shepherd/provider/equinix/main.go b/cloud/shepherd/provider/equinix/main.go
index 3a402e8..0060502 100644
--- a/cloud/shepherd/provider/equinix/main.go
+++ b/cloud/shepherd/provider/equinix/main.go
@@ -14,6 +14,7 @@
"source.monogon.dev/cloud/equinix/wrapngo"
"source.monogon.dev/cloud/lib/component"
"source.monogon.dev/cloud/shepherd/manager"
+ ssh2 "source.monogon.dev/go/net/ssh"
clicontext "source.monogon.dev/metropolis/cli/pkg/context"
)
@@ -89,7 +90,7 @@
klog.Exitf("%v", err)
}
- sshClient := &manager.PlainSSHClient{
+ sshClient := &ssh2.DirectClient{
AuthMethod: ssh.PublicKeys(sshSigner),
// Equinix OS installations always use root.
Username: "root",