func sshClient(ctx context.Context, node infra.Node, log logrus.FieldLogger) (*ssh.Client, error) {
	ctx, cancel := context.WithTimeout(ctx, deadlineSSH)
	defer cancel()
	var client *ssh.Client
	b := backoff.NewConstantBackOff(retrySSH)
	err := wait.RetryWithInterval(ctx, b, func() (err error) {
		client, err = node.Client()
		if err == nil {
			log.Debug("Connected via SSH.")
			return nil
		}
		log.WithError(err).Debug("Waiting for SSH.")
		return trace.Wrap(err)
	}, log)
	if err != nil {
		return nil, trace.Wrap(err)
	}
	return client, nil
}
waits for SSH to be up on node and returns client
package gravity
import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"path/filepath"
	"regexp"
	"strings"
	"text/template"
	"time"
	"github.com/gravitational/robotest/infra"
	"github.com/gravitational/robotest/lib/constants"
	"github.com/gravitational/robotest/lib/defaults"
	sshutils "github.com/gravitational/robotest/lib/ssh"
	"github.com/gravitational/robotest/lib/wait"
	"github.com/cenkalti/backoff"
	"github.com/gravitational/trace"
	"github.com/sirupsen/logrus"
	"golang.org/x/crypto/ssh"
)
type Gravity interface {
	json.Marshaler
	fmt.Stringer
	SetInstaller(ctx context.Context, installerUrl, subdir string) error
	TransferFile(ctx context.Context, url, subdir string) error
	ExecScript(ctx context.Context, scriptUrl string, args []string) error
	Install(ctx context.Context, param InstallParam) error
	Status(ctx context.Context) (*GravityStatus, error)
	OfflineUpdate(ctx context.Context, installerUrl string) error
	Join(ctx context.Context, param JoinCmd) error
	Leave(ctx context.Context, graceful Graceful) error
	Remove(ctx context.Context, node string, graceful Graceful) error
	Uninstall(ctx context.Context) error
	UninstallApp(ctx context.Context) error
	PowerOff(ctx context.Context, graceful Graceful) error
	Reboot(ctx context.Context, graceful Graceful) error
	CollectLogs(ctx context.Context, prefix string, args ...string) (localPath string, err error)
	Upload(ctx context.Context) error
	Upgrade(ctx context.Context) error
	RunInPlanet(ctx context.Context, cmd string, args ...string) (string, error)
	Node() infra.Node
	Offline() bool
	Client() *ssh.Client
	Logger() logrus.FieldLogger
}
type Graceful bool
type InstallParam struct {
	Token string `json:"-"`
	Role string `json:"role" validate:"required"`
	Cluster string `json:"cluster"`
	Flavor string `json:"flavor" validate:"required"`
	EnableRemoteSupport bool `json:"remote_support"`
	CloudProvider string `json:"cloud_provider,omitempty"`
	GCENodeTag string `json:"gce_node_tag,omitempty"`
	StateDir string `json:"state_dir" validate:"required"`
	OSFlavor OS `json:"os" validate:"required"`
	DockerStorageDriver StorageDriver `json:"storage_driver"`
	InstallerURL string `json:"installer_url,omitempty"`
	OpsAdvertiseAddr string `json:"ops_advertise_addr,omitempty"`
	ServiceUID *uint `json:"service_uid,omitempty"`
	ServiceGID *uint `json:"service_gid,omitempty"`
}
type JoinCmd struct {
	InstallDir string
	PeerAddr string
	Token string
	Role string
	StateDir string
}
type gravity struct {
	node       infra.Node
	ssh        *ssh.Client
	installDir string
	param      cloudDynamicParams
	ts         time.Time
	log        logrus.FieldLogger
}
func (g *gravity) MarshalJSON() ([]byte, error) {
	return json.Marshal(map[string]string{
		"public_ip": g.node.Addr(),
		"ip":        g.node.PrivateAddr(),
	})
}