github.phpd.cn/cilium/cilium@v1.6.12/test/helpers/node.go (about) 1 // Copyright 2017 Authors of Cilium 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package helpers 16 17 import ( 18 "context" 19 "fmt" 20 "io" 21 "os" 22 "path/filepath" 23 "strings" 24 "sync" 25 "time" 26 27 "github.com/cilium/cilium/test/config" 28 ginkgoext "github.com/cilium/cilium/test/ginkgo-ext" 29 30 "github.com/sirupsen/logrus" 31 "golang.org/x/crypto/ssh" 32 ) 33 34 var ( 35 //SSHMetaLogs is a buffer where all commands sent over ssh are saved. 36 SSHMetaLogs = ginkgoext.NewWriter(new(Buffer)) 37 ) 38 39 // SSHMeta contains metadata to SSH into a remote location to run tests 40 type SSHMeta struct { 41 sshClient *SSHClient 42 env []string 43 rawConfig []byte 44 nodeName string 45 logger *logrus.Entry 46 } 47 48 // CreateSSHMeta returns an SSHMeta with the specified host, port, and user, as 49 // well as an according SSHClient. 50 func CreateSSHMeta(host string, port int, user string) *SSHMeta { 51 return &SSHMeta{ 52 sshClient: GetSSHClient(host, port, user), 53 } 54 } 55 56 // IsLocal returns true if commands are executed on the Ginkgo host 57 func (s *SSHMeta) IsLocal() bool { 58 return false 59 } 60 61 func (s *SSHMeta) String() string { 62 return fmt.Sprintf("environment: %s, SSHClient: %s", s.env, s.sshClient.String()) 63 64 } 65 66 // CloseSSHClient closes all of the connections made by the SSH Client for this 67 // SSHMeta. 68 func (s *SSHMeta) CloseSSHClient() { 69 if s.sshClient == nil || s.sshClient.client == nil { 70 log.Error("SSH client is nil; cannot close") 71 } 72 if err := s.sshClient.client.Close(); err != nil { 73 log.WithError(err).Error("error closing SSH client") 74 } 75 } 76 77 // GetVagrantSSHMeta returns a SSHMeta initialized based on the provided 78 // SSH-config target. 79 func GetVagrantSSHMeta(vmName string) *SSHMeta { 80 config, err := GetVagrantSSHMetadata(vmName) 81 if err != nil { 82 return nil 83 } 84 85 log.Debugf("generated SSHConfig for node %s", vmName) 86 nodes, err := ImportSSHconfig(config) 87 if err != nil { 88 log.WithError(err).Error("Error importing ssh config") 89 return nil 90 } 91 var node *SSHConfig 92 log.Debugf("done importing ssh config") 93 for name := range nodes { 94 if strings.HasPrefix(name, vmName) { 95 node = nodes[name] 96 break 97 } 98 } 99 if node == nil { 100 log.Errorf("Node %s not found in ssh config", vmName) 101 return nil 102 } 103 sshMeta := &SSHMeta{ 104 sshClient: node.GetSSHClient(), 105 rawConfig: config, 106 nodeName: vmName, 107 } 108 109 sshMeta.setBasePath() 110 return sshMeta 111 } 112 113 // setBasePath if the SSHConfig is defined we set the BasePath to the GOPATH, 114 // from golang 1.8 GOPATH is by default $HOME/go so we also check that. 115 func (s *SSHMeta) setBasePath() { 116 if config.CiliumTestConfig.SSHConfig == "" { 117 return 118 } 119 120 gopath := s.Exec("echo $GOPATH").SingleOut() 121 if gopath != "" { 122 BasePath = filepath.Join(gopath, CiliumPath) 123 return 124 } 125 126 home := s.Exec("echo $HOME").SingleOut() 127 if home == "" { 128 return 129 } 130 131 BasePath = filepath.Join(home, "go", CiliumPath) 132 return 133 } 134 135 // ExecuteContext executes the given `cmd` and writes the cmd's stdout and 136 // stderr into the given io.Writers. 137 // Returns an error if context Deadline() is reached or if there was an error 138 // executing the command. 139 func (s *SSHMeta) ExecuteContext(ctx context.Context, cmd string, stdout io.Writer, stderr io.Writer) error { 140 if stdout == nil { 141 stdout = os.Stdout 142 } 143 144 if stderr == nil { 145 stderr = os.Stderr 146 } 147 fmt.Fprintln(SSHMetaLogs, cmd) 148 command := &SSHCommand{ 149 Path: cmd, 150 Stdin: os.Stdin, 151 Stdout: stdout, 152 Stderr: stderr, 153 } 154 return s.sshClient.RunCommandContext(ctx, command) 155 } 156 157 // ExecWithSudo returns the result of executing the provided cmd via SSH using 158 // sudo. 159 func (s *SSHMeta) ExecWithSudo(cmd string, options ...ExecOptions) *CmdRes { 160 command := fmt.Sprintf("sudo %s", cmd) 161 return s.Exec(command, options...) 162 } 163 164 // ExecOptions options to execute Exec and ExecWithContext 165 type ExecOptions struct { 166 SkipLog bool 167 } 168 169 // Exec returns the results of executing the provided cmd via SSH. 170 func (s *SSHMeta) Exec(cmd string, options ...ExecOptions) *CmdRes { 171 // Bound all command executions to be at most the timeout used by the CI 172 // so that commands do not block forever. 173 ctx, cancel := context.WithTimeout(context.Background(), HelperTimeout) 174 defer cancel() 175 return s.ExecContext(ctx, cmd, options...) 176 } 177 178 // ExecShort runs command with the provided options. It will take up to 179 // ShortCommandTimeout seconds to run the command before it times out. 180 func (s *SSHMeta) ExecShort(cmd string, options ...ExecOptions) *CmdRes { 181 ctx, cancel := context.WithTimeout(context.Background(), ShortCommandTimeout) 182 defer cancel() 183 return s.ExecContext(ctx, cmd, options...) 184 } 185 186 // ExecMiddle runs command with the provided options. It will take up to 187 // MidCommandTimeout seconds to run the command before it times out. 188 func (s *SSHMeta) ExecMiddle(cmd string, options ...ExecOptions) *CmdRes { 189 ctx, cancel := context.WithTimeout(context.Background(), MidCommandTimeout) 190 defer cancel() 191 return s.ExecContext(ctx, cmd, options...) 192 } 193 194 // ExecContextShort is a wrapper around ExecContext which creates a child 195 // context with a timeout of ShortCommandTimeout. 196 func (s *SSHMeta) ExecContextShort(ctx context.Context, cmd string, options ...ExecOptions) *CmdRes { 197 shortCtx, cancel := context.WithTimeout(ctx, ShortCommandTimeout) 198 defer cancel() 199 return s.ExecContext(shortCtx, cmd, options...) 200 } 201 202 // ExecContext returns the results of executing the provided cmd via SSH. 203 func (s *SSHMeta) ExecContext(ctx context.Context, cmd string, options ...ExecOptions) *CmdRes { 204 var ops ExecOptions 205 if len(options) > 0 { 206 ops = options[0] 207 } 208 209 log.Debugf("running command: %s", cmd) 210 stdout := new(Buffer) 211 stderr := new(Buffer) 212 start := time.Now() 213 err := s.ExecuteContext(ctx, cmd, stdout, stderr) 214 215 res := CmdRes{ 216 cmd: cmd, 217 stdout: stdout, 218 stderr: stderr, 219 success: true, // this may be toggled when err != nil below 220 duration: time.Since(start), 221 } 222 223 if err != nil { 224 res.success = false 225 // Set error code to 1 in case that it's another error to see that the 226 // command failed. If the default value (0) indicates that command 227 // works but it was not executed at all. 228 res.exitcode = 1 229 exiterr, isExitError := err.(*ssh.ExitError) 230 if isExitError { 231 // Set res's exitcode if the error is an ExitError 232 res.exitcode = exiterr.Waitmsg.ExitStatus() 233 } else { 234 // Log other error types. They are likely from SSH or the network 235 log.WithError(err).Errorf("Error executing command '%s'", cmd) 236 res.err = err 237 } 238 } 239 240 res.SendToLog(ops.SkipLog) 241 return &res 242 } 243 244 // GetCopy returns a copy of SSHMeta, useful for parallel requests 245 func (s *SSHMeta) GetCopy() *SSHMeta { 246 nodes, err := ImportSSHconfig(s.rawConfig) 247 if err != nil { 248 log.WithError(err).Error("while importing ssh config for meta copy") 249 return nil 250 } 251 252 config := nodes[s.nodeName] 253 if config == nil { 254 log.Errorf("no node %s in imported config", s.nodeName) 255 return nil 256 } 257 258 copy := &SSHMeta{ 259 sshClient: config.GetSSHClient(), 260 rawConfig: s.rawConfig, 261 nodeName: s.nodeName, 262 } 263 264 return copy 265 } 266 267 // ExecInBackground returns the results of running cmd via SSH in the specified 268 // context. The command will be executed in the background until context.Context 269 // is canceled or the command has finish its execution. 270 func (s *SSHMeta) ExecInBackground(ctx context.Context, cmd string, options ...ExecOptions) *CmdRes { 271 if ctx == nil { 272 panic("no context provided") 273 } 274 275 var ops ExecOptions 276 if len(options) > 0 { 277 ops = options[0] 278 } 279 280 fmt.Fprintln(SSHMetaLogs, cmd) 281 stdout := new(Buffer) 282 stderr := new(Buffer) 283 284 command := &SSHCommand{ 285 Path: cmd, 286 Stdin: os.Stdin, 287 Stdout: stdout, 288 Stderr: stderr, 289 } 290 var wg sync.WaitGroup 291 res := &CmdRes{ 292 cmd: cmd, 293 stdout: stdout, 294 stderr: stderr, 295 success: false, 296 wg: &wg, 297 } 298 299 res.wg.Add(1) 300 go func(res *CmdRes) { 301 defer res.wg.Done() 302 start := time.Now() 303 err := s.sshClient.RunCommandInBackground(ctx, command) 304 if err != nil { 305 exiterr, isExitError := err.(*ssh.ExitError) 306 if isExitError { 307 res.exitcode = exiterr.Waitmsg.ExitStatus() 308 // Set success as true if SIGINT signal was sent to command 309 if res.exitcode == 130 { 310 res.success = true 311 } 312 } 313 } else { 314 res.success = true 315 res.exitcode = 0 316 } 317 res.duration = time.Since(start) 318 res.SendToLog(ops.SkipLog) 319 }(res) 320 321 return res 322 }