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 }