Skip to content

Commit

Permalink
Merge pull request #1140 from srl-labs/steiler-execNodeInterface
Browse files Browse the repository at this point in the history
Exec package and interface to enable commands execution on nodes
  • Loading branch information
hellt authored Dec 23, 2022
2 parents f961b64 + 587adaf commit a470278
Show file tree
Hide file tree
Showing 38 changed files with 848 additions and 283 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mocks-gen: mocks-rm ## Generate mocks for all the defined interfaces.
mockgen -package=mocks -source=clab/dependency_manager.go -destination=$(MOCKDIR)/dependency_manager.go
mockgen -package=mocks -source=runtime/runtime.go -destination=$(MOCKDIR)/runtime.go
mockgen -package=mocks -source=nodes/default_node.go -destination=$(MOCKDIR)/default_node.go
mockgen -package=mocks -source=clab/exec/exec.go -destination=$(MOCKDIR)/exec.go

.PHONY: mocks-rm
mocks-rm: ## remove generated mocks
Expand Down
10 changes: 5 additions & 5 deletions clab/config/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import (
"gopkg.in/yaml.v2"
)

// templates to execute.
// TemplateNames is templates to execute.
var TemplateNames []string

// path to additional templates.
// TemplatePaths is path to additional templates.
var TemplatePaths []string

// debug count.
// DebugCount is a debug verbosity counter.
var DebugCount int

type NodeConfig struct {
Expand All @@ -34,7 +34,7 @@ type NodeConfig struct {
Info []string
}

// Load templates from all paths for the specific role/kind.
// LoadTemplates loads templates from all paths for the specific role/kind.
func LoadTemplates(tmpl *template.Template, role string) error {
for _, p := range TemplatePaths {
fn := filepath.Join(p, fmt.Sprintf("*__%s.tmpl", role))
Expand Down Expand Up @@ -107,7 +107,7 @@ func RenderAll(allnodes map[string]*NodeConfig) error {
return nil
}

// Implement stringer for NodeConfig.
// String implements stringer interface for NodeConfig.
func (c *NodeConfig) String() string {
s := fmt.Sprintf("%s: %v", c.TargetNode.ShortName, c.Info)
return s
Expand Down
12 changes: 6 additions & 6 deletions clab/config/transport/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type SSHSession struct {

type SSHTransportOption func(*SSHTransport) error

// The SSH reply, executed command and the prompt.
// SSHReply is SSH reply, executed command and the prompt.
type SSHReply struct{ result, prompt, command string }

// SSHTransport setting needs to be set before calling Connect()
Expand Down Expand Up @@ -52,7 +52,7 @@ type SSHTransport struct {
K SSHKind
}

// Add username & password authentication.
// WithUserNamePassword adds username & password authentication.
func WithUserNamePassword(username, password string) SSHTransportOption {
return func(tx *SSHTransport) error {
tx.SSHConfig.User = username
Expand All @@ -64,7 +64,7 @@ func WithUserNamePassword(username, password string) SSHTransportOption {
}
}

// Add a basic username & password to a config
// HostKeyCallback adds a basic username & password to a config.
// Will initialize the config if required.
func HostKeyCallback(callback ...ssh.HostKeyCallback) SSHTransportOption {
return func(tx *SSHTransport) error {
Expand Down Expand Up @@ -109,7 +109,7 @@ func NewSSHTransport(node *types.NodeConfig, options ...SSHTransportOption) (*SS
return nil, fmt.Errorf("no transport implemented for kind: %s", node.Kind)
}

// Creates the channel reading the SSH connection
// InChannel creates the channel reading the SSH connection.
//
// The first prompt is saved in LoginMessages
//
Expand Down Expand Up @@ -311,7 +311,7 @@ func (t *SSHTransport) Close() {
t.ses.Close()
}

// Create a new SSH session (Dial, open in/out pipes and start the shell)
// NewSSHSession creates a new SSH session (Dial, open in/out pipes and start the shell)
// pass the authentication details in sshConfig.
func NewSSHSession(host string, sshConfig *ssh.ClientConfig) (*SSHSession, error) {
if !strings.Contains(host, ":") {
Expand Down Expand Up @@ -369,7 +369,7 @@ func (ses *SSHSession) Close() {
ses.Session.Close()
}

// The LogString will include the entire SSHReply
// LogString will include the entire SSHReply
// Each field will be prefixed by a character.
// # - command sent
// | - result received
Expand Down
6 changes: 3 additions & 3 deletions clab/config/transport/sshkind.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
log "github.com/sirupsen/logrus"
)

// An interface to implement kind specific methods for transactions and prompt checking.
// SSHKind is an interface to implement kind specific methods for transactions and prompt checking.
type SSHKind interface {
// Start a config transaction
ConfigStart(s *SSHTransport, transaction bool) error
Expand All @@ -23,7 +23,7 @@ type SSHKind interface {
PromptParse(s *SSHTransport, in *string) *SSHReply
}

// implements SShKind.
// VrSrosSSHKind implements SShKind.
type VrSrosSSHKind struct{}

func (*VrSrosSSHKind) ConfigStart(s *SSHTransport, transaction bool) error { // skipcq: RVV-A0005
Expand Down Expand Up @@ -61,7 +61,7 @@ func (*VrSrosSSHKind) PromptParse(s *SSHTransport, in *string) *SSHReply {
return nil
}

// implements SShKind.
// SrlSSHKind implements SShKind.
type SrlSSHKind struct{}

func (*SrlSSHKind) ConfigStart(s *SSHTransport, transaction bool) error { // skipcq: RVV-A0005
Expand Down
2 changes: 1 addition & 1 deletion clab/config/transport/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"
)

// Debug count.
// DebugCount is a debug verbosity counter.
var DebugCount int

type TransportOption func(*Transport)
Expand Down
2 changes: 1 addition & 1 deletion clab/config/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const (

type Dict map[string]interface{}

// Prepare variables for all nodes. This will also prepare all variables for the links.
// PrepareVars variables for all nodes. This will also prepare all variables for the links.
func PrepareVars(nodes map[string]nodes.Node, links map[int]*types.Link) map[string]*NodeConfig {
res := make(map[string]*NodeConfig)

Expand Down
235 changes: 235 additions & 0 deletions clab/exec/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package exec

import (
"encoding/json"
"errors"
"fmt"
"strings"

log "github.com/sirupsen/logrus"

"github.com/google/shlex"
)

const (
ExecFormatJSON string = "json"
ExecFormatPlain string = "plain"
)

var ErrRunExecNotSupported = errors.New("exec not supported for this kind")

// ParseExecOutputFormat parses the exec output format user input.
func ParseExecOutputFormat(s string) (string, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case ExecFormatJSON:
return ExecFormatJSON, nil
case ExecFormatPlain, "table":
return ExecFormatPlain, nil
}
return "", fmt.Errorf("cannot parse %q as execution output format, supported output formats %q",
s, []string{ExecFormatJSON, ExecFormatPlain})
}

// ExecCmd represents an exec command.
type ExecCmd struct {
Cmd []string `json:"cmd"` // Cmd is a slice-based representation of a string command.
}

// NewExecCmdFromString creates ExecCmd for a string-based command.
func NewExecCmdFromString(cmd string) (*ExecCmd, error) {
result := &ExecCmd{}
if err := result.SetCmd(cmd); err != nil {
return nil, err
}
return result, nil
}

// NewExecCmdFromSlice creates ExecCmd for a command represented as a slice of strings.
func NewExecCmdFromSlice(cmd []string) *ExecCmd {
return &ExecCmd{
Cmd: cmd,
}
}

type ExecResultHolder interface {
GetStdOutString() string
GetStdErrString() string
GetStdOutByteSlice() []byte
GetStdErrByteSlice() []byte
GetReturnCode() int
GetCmdString() string
Dump(format string) (string, error)
String() string
}

// ExecResult represents a result of a command execution.
type ExecResult struct {
Cmd []string `json:"cmd"`
ReturnCode int `json:"return-code"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
}

func NewExecResult(op *ExecCmd) *ExecResult {
er := &ExecResult{Cmd: op.GetCmd()}
return er
}

// SetCmd sets the command that is to be executed.
func (e *ExecCmd) SetCmd(cmd string) error {
c, err := shlex.Split(cmd)
if err != nil {
return err
}
e.Cmd = c
return nil
}

// GetCmd sets the command that is to be executed.
func (e *ExecCmd) GetCmd() []string {
return e.Cmd
}

// GetCmdString sets the command that is to be executed.
func (e *ExecCmd) GetCmdString() string {
return strings.Join(e.Cmd, " ")
}

func (e *ExecResult) String() string {
return fmt.Sprintf("Cmd: %s\nReturnCode: %d\nStdOut:\n%s\nStdErr:\n%s\n", e.GetCmdString(), e.ReturnCode, e.Stdout, e.Stderr)
}

// Dump dumps execution result as a string in one of the provided formats.
func (e *ExecResult) Dump(format string) (string, error) {
var result string
switch format {
case ExecFormatJSON:
byteData, err := json.MarshalIndent(e, "", " ")
if err != nil {
return "", err
}
result = string(byteData)
case ExecFormatPlain:
result = e.String()
}
return result, nil
}

// GetCmdString returns the initially parsed cmd as a string for e.g. log output purpose.
func (e *ExecResult) GetCmdString() string {
return strings.Join(e.Cmd, " ")
}

func (e *ExecResult) GetReturnCode() int {
return e.ReturnCode
}

func (e *ExecResult) SetReturnCode(rc int) {
e.ReturnCode = rc
}

func (e *ExecResult) GetStdOutString() string {
return string(e.Stdout)
}

func (e *ExecResult) GetStdErrString() string {
return string(e.Stderr)
}

func (e *ExecResult) GetStdOutByteSlice() []byte {
return []byte(e.Stdout)
}

func (e *ExecResult) GetStdErrByteSlice() []byte {
return []byte(e.Stderr)
}

func (e *ExecResult) GetCmd() []string {
return e.Cmd
}

func (e *ExecResult) SetStdOut(data []byte) {
e.Stdout = string(data)
}

func (e *ExecResult) SetStdErr(data []byte) {
e.Stderr = string(data)
}

// execEntries is a map indexed by container IDs storing lists of ExecResultHolder.
// ExecResultHolder is an interface that is backed by the type storing data for the executed command.
type execEntries map[string][]ExecResultHolder

// ExecCollection represents a datastore for exec commands execution results.
type ExecCollection struct {
execEntries
}

// NewExecCollection initializes the collection of exec command results.
func NewExecCollection() *ExecCollection {
return &ExecCollection{
execEntries{},
}
}

func (ec *ExecCollection) Add(cId string, e ExecResultHolder) {
ec.execEntries[cId] = append(ec.execEntries[cId], e)
}

func (ec *ExecCollection) AddAll(cId string, e []ExecResultHolder) {
ec.execEntries[cId] = append(ec.execEntries[cId], e...)
}

// Dump dumps the contents of ExecCollection as a string in one of the provided formats.
func (ec *ExecCollection) Dump(format string) (string, error) {
result := strings.Builder{}
switch format {
case ExecFormatJSON:
byteData, err := json.MarshalIndent(ec.execEntries, "", " ")
if err != nil {
return "", err
}
result.Write(byteData)
case ExecFormatPlain:
printSep := false
for k, execResults := range ec.execEntries {
if len(execResults) == 0 {
// skip if there is no result
continue
}
// write seperator
if printSep {
result.WriteString("\n+++++++++++++++++++++++++++++\n\n")
}
// write header for entry
result.WriteString("Node: ")
result.WriteString(k)
result.WriteString("\n")
for _, er := range execResults {
// write entry
result.WriteString(er.String())
}
// starting second run, print sep
printSep = true
}
}
return result.String(), nil
}

// Log writes to the log execution results stored in ExecCollection.
// If execution result contains error, the error log facility is used,
// otherwise it is logged as INFO.
func (ec *ExecCollection) Log() {
for k, execResults := range ec.execEntries {
for _, er := range execResults {
switch {
case er.GetReturnCode() != 0 || er.GetStdErrString() != "":
log.Errorf("Failed to execute command '%s' on node %s. rc=%d,\nstdout:\n%s\nstderr:\n%s",
er.GetCmdString(), k, er.GetReturnCode(), er.GetStdOutString(), er.GetStdErrString())
default:
log.Infof("Executed command '%s' on node %s. stdout:\n%s",
er.GetCmdString(), k, er.GetStdOutString())
}
}
}
}
Loading

0 comments on commit a470278

Please sign in to comment.