github.com/caos/orbos@v1.5.14-0.20221103111702-e6cd0cea7ad4/internal/operator/orbiter/kinds/providers/ssh/machine.go (about)

     1  package ssh
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  
    11  	sshlib "golang.org/x/crypto/ssh"
    12  
    13  	"github.com/caos/orbos/internal/ssh"
    14  	"github.com/caos/orbos/mntr"
    15  )
    16  
    17  type Machine struct {
    18  	monitor    mntr.Monitor
    19  	remoteUser string
    20  	ip         string
    21  	zone       string
    22  	sshCfg     *sshlib.ClientConfig
    23  }
    24  
    25  func NewMachine(monitor mntr.Monitor, remoteUser, ip string) *Machine {
    26  	return &Machine{
    27  		remoteUser: remoteUser,
    28  		monitor: monitor.WithFields(map[string]interface{}{
    29  			"host": ip,
    30  			"user": remoteUser,
    31  		}),
    32  		ip: ip,
    33  	}
    34  }
    35  
    36  func (c *Machine) Zone() string {
    37  	return c.zone
    38  }
    39  
    40  func (c *Machine) Execute(stdin io.Reader, cmd string) (stdout []byte, err error) {
    41  
    42  	monitor := c.monitor.WithFields(map[string]interface{}{
    43  		"command": cmd,
    44  	})
    45  	defer func() {
    46  		if err != nil {
    47  			err = fmt.Errorf("executing %s failed: %w", cmd, err)
    48  		} else {
    49  			monitor.WithField("stdout", string(stdout)).Debug("Done executing command with ssh")
    50  		}
    51  	}()
    52  
    53  	monitor.Debug("Trying to execute with ssh")
    54  
    55  	var output []byte
    56  	sess, close, err := c.open()
    57  	defer close()
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  
    62  	buf := new(bytes.Buffer)
    63  	defer buf.Reset()
    64  	sess.Stdin = stdin
    65  	sess.Stderr = buf
    66  
    67  	output, err = sess.Output(cmd)
    68  	if err != nil {
    69  		return output, fmt.Errorf("stderr: %s", buf.String())
    70  	}
    71  	return output, nil
    72  }
    73  
    74  func (c *Machine) Shell() (err error) {
    75  	defer func() {
    76  		if err != nil {
    77  			err = fmt.Errorf("executing shell failed: %w", err)
    78  		} else {
    79  			c.monitor.Debug("Done executing shell with ssh")
    80  		}
    81  	}()
    82  
    83  	sess, close, err := c.open()
    84  	defer close()
    85  	if err != nil {
    86  		return err
    87  	}
    88  	sess.Stdin = os.Stdin
    89  	sess.Stderr = os.Stderr
    90  	sess.Stdout = os.Stdout
    91  	modes := sshlib.TerminalModes{
    92  		sshlib.ECHO:          0,     // disable echoing
    93  		sshlib.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
    94  		sshlib.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
    95  	}
    96  
    97  	if err := sess.RequestPty("xterm", 40, 80, modes); err != nil {
    98  		return fmt.Errorf("request for pseudo terminal failed: %w", err)
    99  	}
   100  
   101  	if err := sess.Shell(); err != nil {
   102  		return fmt.Errorf("failed to start shell: %w", err)
   103  	}
   104  	return sess.Wait()
   105  }
   106  
   107  func WriteFileCommands(user, path string, permissions uint16) (string, string) {
   108  	return fmt.Sprintf("sudo mkdir -p %s && sudo chown -R %s %s", filepath.Dir(path), user, filepath.Dir(path)),
   109  		fmt.Sprintf("sudo sh -c 'cat > %s && chmod %d %s && chown %s %s'", path, permissions, path, user, path)
   110  }
   111  
   112  func (c *Machine) WriteFile(path string, data io.Reader, permissions uint16) (err error) {
   113  
   114  	monitor := c.monitor.WithFields(map[string]interface{}{
   115  		"path":        path,
   116  		"permissions": permissions,
   117  	})
   118  	defer func() {
   119  		if err != nil {
   120  			err = fmt.Errorf("writing file %s failed: %w", path, err)
   121  		} else {
   122  			monitor.Debug("Done writing file with ssh")
   123  		}
   124  	}()
   125  
   126  	monitor.Debug("Trying to write file with ssh")
   127  
   128  	ensurePath, writeFile := WriteFileCommands(c.remoteUser, path, permissions)
   129  
   130  	if _, err := c.Execute(nil, ensurePath); err != nil {
   131  		return err
   132  	}
   133  
   134  	_, err = c.Execute(data, writeFile)
   135  	return err
   136  }
   137  
   138  func (c *Machine) ReadFile(path string, data io.Writer) (err error) {
   139  
   140  	monitor := c.monitor.WithFields(map[string]interface{}{
   141  		"path": path,
   142  	})
   143  	defer func() {
   144  		if err != nil {
   145  			err = fmt.Errorf("reading file %s failed: %w", path, err)
   146  		} else {
   147  			monitor.Debug("Done reading file with ssh")
   148  		}
   149  	}()
   150  
   151  	monitor.Debug("Trying to read file with ssh")
   152  
   153  	cmd := fmt.Sprintf("sudo cat %s", path)
   154  	sess, close, err := c.open()
   155  	defer close()
   156  	if err != nil {
   157  		return err
   158  	}
   159  	stderr := new(bytes.Buffer)
   160  	defer stderr.Reset()
   161  	sess.Stdout = data
   162  	sess.Stderr = stderr
   163  
   164  	if err := sess.Run(cmd); err != nil {
   165  		return fmt.Errorf("executing %s failed with stderr %s: %w", cmd, stderr.String(), err)
   166  	}
   167  	return nil
   168  }
   169  
   170  func (c *Machine) open() (sess *sshlib.Session, close func() error, err error) {
   171  
   172  	c.monitor.Debug("Trying to open an ssh connection")
   173  	close = func() error { return nil }
   174  
   175  	if c.sshCfg == nil {
   176  		return nil, close, errors.New("no ssh key passed via infra.Machine.UseKey")
   177  	}
   178  
   179  	address := fmt.Sprintf("%s:%d", c.ip, 22)
   180  	conn, err := sshlib.Dial("tcp", address, c.sshCfg)
   181  	if err != nil {
   182  		return nil, close, fmt.Errorf("dialling tcp %s with user %s failed: %w", address, c.remoteUser, err)
   183  	}
   184  
   185  	sess, err = conn.NewSession()
   186  	if err != nil {
   187  		conn.Close()
   188  		return sess, close, err
   189  	}
   190  	return sess, func() error {
   191  		err := sess.Close()
   192  		err = conn.Close()
   193  		return err
   194  	}, nil
   195  }
   196  
   197  func (c *Machine) UseKey(keys ...[]byte) error {
   198  
   199  	publicKeys, err := ssh.AuthMethodFromKeys(keys...)
   200  	if err != nil {
   201  		return err
   202  	}
   203  
   204  	c.sshCfg = &sshlib.ClientConfig{
   205  		User:            c.remoteUser,
   206  		Auth:            []sshlib.AuthMethod{publicKeys},
   207  		HostKeyCallback: sshlib.InsecureIgnoreHostKey(),
   208  	}
   209  	return nil
   210  }