Skip to content

Commit

Permalink
Implementation of the capture() starlark function.
Browse files Browse the repository at this point in the history
This patch implements the Go code for starlark builtin function capture(). This
function allows Crashd script to execute commands on specified compute resources
and capture the result in a local file.

This patch does the followings:
- Updates pacakge ssh with support for capture
- Adds Go function to support starlark builtin func for capture
- Adds and updates tests for capture

Signed-off-by: Vladimir Vivien <vivienv@vmware.com>
  • Loading branch information
vladimirvivien committed Jun 26, 2020
1 parent ac97913 commit 6257c24
Show file tree
Hide file tree
Showing 13 changed files with 746 additions and 52 deletions.
43 changes: 32 additions & 11 deletions ssh/ssh_run.go
Expand Up @@ -4,7 +4,10 @@
package ssh

import (
"bytes"
"fmt"
"io"
"strings"
"time"

"github.com/sirupsen/logrus"
Expand All @@ -26,16 +29,34 @@ type SSHArgs struct {
JumpProxy *JumpProxyArg
}

// Run runs a command over SSH and returns the result as a string
func Run(args SSHArgs, cmd string) (string, error) {
reader, err := sshRunProc(args, cmd)
if err != nil {
return "", err
}
var result bytes.Buffer
if _, err := result.ReadFrom(reader); err != nil {
return "", err
}
return strings.TrimSpace(result.String()), nil
}

// RunRead runs a command over SSH and returns an io.Reader for stdout/stderr
func RunRead(args SSHArgs, cmd string) (io.Reader, error) {
return sshRunProc(args, cmd)
}

func sshRunProc(args SSHArgs, cmd string) (io.Reader, error) {
e := echo.New()
sshCmd, err := makeSSHCmdStr(args)
if err != nil {
return "", err
return nil, err
}
effectiveCmd := fmt.Sprintf(`%s "%s"`, sshCmd, cmd)
logrus.Debug("ssh.Run: ", effectiveCmd)
logrus.Debug("ssh.run: ", effectiveCmd)

var result string
var proc *echo.Proc
maxRetries := args.MaxRetries
if maxRetries == 0 {
maxRetries = 10
Expand All @@ -44,21 +65,21 @@ func Run(args SSHArgs, cmd string) (string, error) {
if err := wait.ExponentialBackoff(retries, func() (bool, error) {
p := e.RunProc(effectiveCmd)
if p.Err() != nil {
logrus.Warn(fmt.Sprintf("unable to connect: %s", p.Err()))
logrus.Warn(fmt.Sprintf("ssh: failed to connect to %s: error '%s': retrying connection", args.Host, p.Err()))
return false, nil
}
result = p.Result()
proc = p
return true, nil // worked
}); err != nil {
logrus.Debugf("ssh.Run failed after %d tries", maxRetries)
return "", err
logrus.Debugf("ssh.run failed after %d tries", maxRetries)
return nil, err
}

return result, nil
}
if proc == nil {
return nil, fmt.Errorf("ssh.run: did get process result")
}

func SSHCapture(args SSHArgs, cmd string, path string) error {
return nil
return proc.Out(), nil
}

func makeSSHCmdStr(args SSHArgs) (string, error) {
Expand Down
49 changes: 47 additions & 2 deletions ssh/ssh_run_test.go
Expand Up @@ -4,14 +4,15 @@
package ssh

import (
"bytes"
"os"
"os/user"
"path/filepath"
"strings"
"testing"
)

func TestSSHRun(t *testing.T) {
func TestRun(t *testing.T) {
homeDir, err := os.UserHomeDir()
if err != nil {
t.Fatal(err)
Expand All @@ -31,7 +32,7 @@ func TestSSHRun(t *testing.T) {
}{
{
name: "simple cmd",
args: SSHArgs{User: usr.Username, PrivateKeyPath: pkPath, Host: "127.0.0.1", Port: testSSHPort, MaxRetries: 100},
args: SSHArgs{User: usr.Username, PrivateKeyPath: pkPath, Host: "127.0.0.1", Port: testSSHPort, MaxRetries: 10},
cmd: "echo 'Hello World!'",
result: "Hello World!",
},
Expand All @@ -50,6 +51,50 @@ func TestSSHRun(t *testing.T) {
}
}

func TestRunRead(t *testing.T) {
homeDir, err := os.UserHomeDir()
if err != nil {
t.Fatal(err)
}

usr, err := user.Current()
if err != nil {
t.Fatal(err)
}
pkPath := filepath.Join(homeDir, ".ssh/id_rsa")

tests := []struct {
name string
args SSHArgs
cmd string
result string
}{
{
name: "simple cmd",
args: SSHArgs{User: usr.Username, PrivateKeyPath: pkPath, Host: "127.0.0.1", Port: testSSHPort, MaxRetries: 10},
cmd: "echo 'Hello World!'",
result: "Hello World!",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
reader, err := RunRead(test.args, test.cmd)
if err != nil {
t.Fatal(err)
}
buf := new(bytes.Buffer)
if _, err := buf.ReadFrom(reader); err != nil {
t.Fatal(err)
}
expected := strings.TrimSpace(buf.String())
if test.result != expected {
t.Fatalf("unexpected result %s", expected)
}
})
}
}

func TestSSHRunMakeCmdStr(t *testing.T) {
tests := []struct {
name string
Expand Down
211 changes: 211 additions & 0 deletions starlark/capture.go
@@ -0,0 +1,211 @@
// Copyright (c) 2020 VMware, Inc. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package starlark

import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/sirupsen/logrus"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"

"github.com/vmware-tanzu/crash-diagnostics/ssh"
)

// captureFunc is a built-in starlark function that runs a provided command and
// captures the result of the command in a specified file stored in workdir.
// If resources and workdir are not provided, captureFunc uses defaults from starlark thread generated
// by previous calls to resources() and crashd_config().
// Starlark format: capture(cmd="command" [,resources=resources][,workdir=path][,file_name=name][,desc=description])
func captureFunc(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var cmdStr string
if args != nil && args.Len() == 1 {
cmd, ok := args.Index(0).(starlark.String)
if !ok {
return starlark.None, fmt.Errorf("%s: default argument must be a string", identifiers.capture)
}
cmdStr = string(cmd)
}

// grab named arguments
var dictionary starlark.StringDict
if kwargs != nil {
dict, err := kwargsToStringDict(kwargs)
if err != nil {
return starlark.None, err
}
dictionary = dict
}

if dictionary["cmd"] != nil {
if cmd, ok := dictionary["cmd"].(starlark.String); ok {
cmdStr = string(cmd)
}
}

if len(cmdStr) == 0 {
return starlark.None, fmt.Errorf("%s: missing command string", identifiers.capture)
}

var fileName string
if dictionary["file_name"] != nil {
if cmd, ok := dictionary["file_name"].(starlark.String); ok {
fileName = string(cmd)
}
}

var desc string
if dictionary["desc"] != nil {
if cmd, ok := dictionary["desc"].(starlark.String); ok {
desc = string(cmd)
}
}

// extract workdir
var workdir string
if dictionary["workdir"] != nil {
if dir, ok := dictionary["workdir"].(starlark.String); ok {
workdir = string(dir)
}
}
if len(workdir) == 0 {
if dir, err := getWorkdirFromThread(thread); err == nil {
workdir = dir
}
}
if len(workdir) == 0 {
workdir = defaults.workdir
}

// extract resources
var resources *starlark.List
if dictionary[identifiers.resources] != nil {
res, ok := dictionary[identifiers.resources].(*starlark.List)
if !ok {
return starlark.None, fmt.Errorf("%s: unexpected resources type", identifiers.capture)
}
resources = res
}
if resources == nil {
res := thread.Local(identifiers.resources)
if res == nil {
return starlark.None, fmt.Errorf("%s: default resources not found", identifiers.capture)
}
resList, ok := res.(*starlark.List)
if !ok {
return starlark.None, fmt.Errorf("%s: unexpected resources type", identifiers.capture)
}
resources = resList
}

results, err := execCapture(cmdStr, workdir, fileName, desc, resources)
if err != nil {
return starlark.None, err
}

// build list of struct as result
var resultList []starlark.Value
for _, result := range results {
if len(results) == 1 {
return result.toStarlarkStruct(), nil
}
resultList = append(resultList, result.toStarlarkStruct())
}

return starlark.NewList(resultList), nil
}

func execCapture(cmdStr, rootPath, fileName, desc string, resources *starlark.List) ([]runResult, error) {
if resources == nil {
return nil, fmt.Errorf("%s: missing resources", identifiers.capture)
}

logrus.Debugf("%s: executing command on %d resources", identifiers.capture, resources.Len())
var results []runResult
for i := 0; i < resources.Len(); i++ {
val := resources.Index(i)
res, ok := val.(*starlarkstruct.Struct)
if !ok {
return nil, fmt.Errorf("%s: unexpected resource type", identifiers.run)
}

val, err := res.Attr("kind")
if err != nil {
return nil, fmt.Errorf("%s: resource.kind: %s", identifiers.capture, err)
}
kind := val.(starlark.String)

val, err = res.Attr("transport")
if err != nil {
return nil, fmt.Errorf("%s: resource.transport: %s", identifiers.capture, err)
}
transport := val.(starlark.String)

val, err = res.Attr("host")
if err != nil {
return nil, fmt.Errorf("%s: resource.host: %s", identifiers.capture, err)
}
host := string(val.(starlark.String))
rootDir := filepath.Join(rootPath, sanitizeStr(host))

switch {
case string(kind) == identifiers.hostResource && string(transport) == "ssh":
result, err := execCaptureSSH(host, cmdStr, rootDir, fileName, desc, res)
if err != nil {
logrus.Error(err)
continue
}
results = append(results, result)
default:
logrus.Errorf("%s: unsupported or invalid resource kind: %s", identifiers.capture, kind)
continue
}
}

return results, nil
}

func execCaptureSSH(host, cmdStr, rootDir, fileName, desc string, res *starlarkstruct.Struct) (runResult, error) {
sshCfg := starlarkstruct.FromKeywords(starlarkstruct.Default, makeDefaultSSHConfig())
if val, err := res.Attr(identifiers.sshCfg); err == nil {
if cfg, ok := val.(*starlarkstruct.Struct); ok {
sshCfg = cfg
}
}

args, err := getSSHArgsFromCfg(sshCfg)
if err != nil {
return runResult{}, err
}
args.Host = host

// create dir for the host
if err := os.MkdirAll(rootDir, 0744); err != nil && !os.IsExist(err) {
return runResult{}, err
}

if len(fileName) == 0 {
fileName = fmt.Sprintf("%s.txt", sanitizeStr(cmdStr))
}
filePath := filepath.Join(rootDir, fileName)

logrus.Debugf("%s: capturing command on %s using ssh: [%s]", identifiers.capture, args.Host, cmdStr)

reader, err := ssh.RunRead(args, cmdStr)
if err != nil {
if err := captureOutput(strings.NewReader(err.Error()), filePath, fmt.Sprintf("%s: failed", cmdStr)); err != nil {
return runResult{}, err
}
}

if err := captureOutput(reader, filePath, desc); err != nil {
return runResult{}, err
}

return runResult{resource: args.Host, result: filePath, err: err}, nil

}

0 comments on commit 6257c24

Please sign in to comment.