From fb236fb922ed5fe4ca78cfc18d7f908d3a3aa214 Mon Sep 17 00:00:00 2001 From: Pavel Sviderski Date: Wed, 11 Sep 2024 20:46:43 +1000 Subject: [PATCH] establish SSH connection when adding a new machine --- internal/cli/cli.go | 48 ++++++++++++++++++++++++++-- internal/cli/client/cluster.go | 9 ------ internal/cli/client/connector/ssh.go | 18 ++++++++--- internal/cli/config/connection.go | 8 +++++ 4 files changed, 68 insertions(+), 15 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 942e0a9b..415b40bb 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -10,6 +10,7 @@ import ( "uncloud/internal/cli/config" "uncloud/internal/machine/api/pb" "uncloud/internal/secret" + "uncloud/internal/sshexec" ) const defaultClusterName = "default" @@ -196,9 +197,52 @@ func (cli *CLI) AddMachine(ctx context.Context, remoteMachine RemoteMachine, clu if err != nil { return fmt.Errorf("connect to cluster: %w", err) } + defer func() { + _ = c.Close() + }() + + sshClient, err := sshexec.Connect(remoteMachine.User, remoteMachine.Host, remoteMachine.Port, remoteMachine.KeyPath) + if err != nil { + return fmt.Errorf( + "SSH login to remote machine %s: %w", + config.NewSSHDestination(remoteMachine.User, remoteMachine.Host, remoteMachine.Port), err, + ) + } + machineExec := sshexec.NewRemote(sshClient) + + // TODO: Check if the machine is already provisioned using machineClient and ask the user to reset it first. + //conn := connector.NewSSHConnectorFromClient(sshClient) + //machineClient, err := client.New(ctx, conn) + //if err != nil { + // return fmt.Errorf("connect to remote machine API: %w", err) + //} + + // TODO: Download and install the latest uncloudd binary by running the install shell script from GitHub. + // For now upload the binary using scp manually. + if _, err = machineExec.Run(ctx, "which uncloudd"); err != nil { + return fmt.Errorf("uncloudd binary not found on the remote machine: %w", err) + } + + //req := &pb.AddMachineRequest{ + // Name: machineName, + // Network: &pb.NetworkConfig{ + // + // } + //} + //resp, err := c.AddMachine(ctx, req) + //if err != nil { + // return fmt.Errorf("add machine to cluster: %w", err) + //} + + // TODO: + // --1. Establish a client connection to the remote machine. + // --2. Check if the machine is already provisioned and ask the user to reset it first. + // --3. Download and install the latest uncloudd binary by running the install shell script from GitHub. + // 4. Request token from the remote machine. + // 5. Add the machine to the cluster using its token and receive a configuration token. + // 6. Request the machine to join the cluster using the configuration token. + // 7. Save the machine's SSH connection details in the cluster config. - fmt.Println("Adding machine to cluster...", c) - // TODO //name, connCfg, err := c.AddMachine(ctx, machineName, user, host, port, sshKeyPath) //if err != nil { // return fmt.Errorf("add machine to cluster %q: %w", cluster.Name(), err) diff --git a/internal/cli/client/cluster.go b/internal/cli/client/cluster.go index f91eeaa0..e18bbeb9 100644 --- a/internal/cli/client/cluster.go +++ b/internal/cli/client/cluster.go @@ -2,7 +2,6 @@ package client import ( "context" - "crypto/ed25519" "errors" "fmt" "google.golang.org/grpc" @@ -297,11 +296,3 @@ func (c *ClusterClient) registerNewMachine( } return c.client.AddMachine(ctx, req) } - -func privateKeyFromSecret(s secret.Secret) (ed25519.PrivateKey, error) { - // Cluster secret in the config is a hex-encoded private key seed. - if len(s) != ed25519.SeedSize { - return nil, fmt.Errorf("invalid cluster secret length") - } - return ed25519.NewKeyFromSeed(s), nil -} diff --git a/internal/cli/client/connector/ssh.go b/internal/cli/client/connector/ssh.go index 6a20db95..55116ee4 100644 --- a/internal/cli/client/connector/ssh.go +++ b/internal/cli/client/connector/ssh.go @@ -41,12 +41,22 @@ func NewSSHConnector(cfg *SSHConnectorConfig) *SSHConnector { return c } +func NewSSHConnectorFromClient(client *ssh.Client) *SSHConnector { + return &SSHConnector{client: client} +} + // TODO: handle context cancelation. func (c *SSHConnector) Connect(ctx context.Context) (*grpc.ClientConn, error) { - var err error - c.client, err = sshexec.Connect(c.config.User, c.config.Host, c.config.Port, c.config.KeyPath) - if err != nil { - return nil, fmt.Errorf("SSH login to %s@%s:%d: %w", c.config.User, c.config.Host, c.config.Port, err) + if c.client == nil { + // Establish an SSH connection if the SSH client is not provided. + if c.config == (SSHConnectorConfig{}) { + return nil, fmt.Errorf("SSH connector not configured") + } + var err error + c.client, err = sshexec.Connect(c.config.User, c.config.Host, c.config.Port, c.config.KeyPath) + if err != nil { + return nil, fmt.Errorf("SSH login to %s@%s:%d: %w", c.config.User, c.config.Host, c.config.Port, err) + } } conn, err := grpc.NewClient( diff --git a/internal/cli/config/connection.go b/internal/cli/config/connection.go index d054c0ee..f85e5c0d 100644 --- a/internal/cli/config/connection.go +++ b/internal/cli/config/connection.go @@ -18,6 +18,8 @@ type MachineConnection struct { PublicKey secret.Secret `toml:"public_key,omitempty"` } +// SSHDestination represents an SSH destination string in the canonical form of "user@host:port". +// The default user "root" and port 22 can be omitted. type SSHDestination string func NewSSHDestination(user, host string, port int) SSHDestination { @@ -37,10 +39,16 @@ func (d SSHDestination) Parse() (user string, host string, port int, err error) if strings.Contains(host, "@") { user, host, _ = strings.Cut(host, "@") } + if user == "" { + user = DefaultSSHUser + } h, p, sErr := net.SplitHostPort(host) if sErr == nil { host = h port, err = strconv.Atoi(p) } + if port == 0 { + port = DefaultSSHPort + } return }