summaryrefslogtreecommitdiff
path: root/libpod/network/netavark/exec.go
blob: 01dea8489cbb4909e2b165d5d27057a1d6de76e4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
package netavark

import (
	"encoding/json"
	"errors"
	"io"
	"os"
	"os/exec"
	"strconv"

	"github.com/sirupsen/logrus"
)

type netavarkError struct {
	exitCode int
	// Set the json key to "error" so we can directly unmarshal into this struct
	Msg string `json:"error"`
	err error
}

func (e *netavarkError) Error() string {
	ec := ""
	// only add the exit code the the error message if we have at least info log level
	// the normal user does not need to care about the number
	if e.exitCode > 0 && logrus.IsLevelEnabled(logrus.InfoLevel) {
		ec = " (exit code " + strconv.Itoa(e.exitCode) + ")"
	}
	msg := "netavark" + ec
	if len(msg) > 0 {
		msg += ": " + e.Msg
	}
	if e.err != nil {
		msg += ": " + e.err.Error()
	}
	return msg
}

func (e *netavarkError) Unwrap() error {
	return e.err
}

func newNetavarkError(msg string, err error) error {
	return &netavarkError{
		Msg: msg,
		err: err,
	}
}

// Type to implement io.Writer interface
// This will write the logrus at info level
type logrusNetavarkWriter struct{}

func (l *logrusNetavarkWriter) Write(b []byte) (int, error) {
	logrus.Info("netavark: ", string(b))
	return len(b), nil
}

// getRustLogEnv returns the RUST_LOG env var based on the current logrus level
func getRustLogEnv() string {
	level := logrus.GetLevel().String()
	// rust env_log uses warn instead of warning
	if level == "warning" {
		level = "warn"
	}
	// the rust netlink library is very verbose
	// make sure to only log netavark logs
	return "RUST_LOG=netavark=" + level
}

// execNetavark will execute netavark with the following arguments
// It takes the path to the binary, the list of args and an interface which is
// marshaled to json and send via stdin to netavark. The result interface is
// used to marshal the netavark output into it. This can be nil.
// All errors return by this function should be of the type netavarkError
// to provide a helpful error message.
func (n *netavarkNetwork) execNetavark(args []string, stdin, result interface{}) error {
	stdinR, stdinW, err := os.Pipe()
	if err != nil {
		return newNetavarkError("failed to create stdin pipe", err)
	}
	stdinWClosed := false
	defer func() {
		stdinR.Close()
		if !stdinWClosed {
			stdinW.Close()
		}
	}()

	stdoutR, stdoutW, err := os.Pipe()
	if err != nil {
		return newNetavarkError("failed to create stdout pipe", err)
	}
	stdoutWClosed := false
	defer func() {
		stdoutR.Close()
		if !stdoutWClosed {
			stdoutW.Close()
		}
	}()

	// connect stderr to the podman stderr for logging
	var logWriter io.Writer = os.Stderr
	if n.syslog {
		// connect logrus to stderr as well so that the logs will be written to the syslog as well
		logWriter = io.MultiWriter(logWriter, &logrusNetavarkWriter{})
	}

	cmd := exec.Command(n.netavarkBinary, args...)
	// connect the pipes to stdin and stdout
	cmd.Stdin = stdinR
	cmd.Stdout = stdoutW
	cmd.Stderr = logWriter
	// set the netavark log level to the same as the podman
	cmd.Env = append(os.Environ(), getRustLogEnv())
	// if we run with debug log level lets also set RUST_BACKTRACE=1 so we can get the full stack trace in case of panics
	if logrus.IsLevelEnabled(logrus.DebugLevel) {
		cmd.Env = append(cmd.Env, "RUST_BACKTRACE=1")
	}

	err = cmd.Start()
	if err != nil {
		return newNetavarkError("failed to start process", err)
	}
	err = json.NewEncoder(stdinW).Encode(stdin)
	// we have to close stdinW so netavark gets the EOF and does not hang forever
	stdinW.Close()
	stdinWClosed = true
	if err != nil {
		return newNetavarkError("failed to encode stdin data", err)
	}

	dec := json.NewDecoder(stdoutR)

	err = cmd.Wait()
	// we have to close stdoutW so we can decode the json without hanging forever
	stdoutW.Close()
	stdoutWClosed = true
	if err != nil {
		exitError := &exec.ExitError{}
		if errors.As(err, &exitError) {
			ne := &netavarkError{}
			// lets disallow unknown fields to make sure we do not get some unexpected stuff
			dec.DisallowUnknownFields()
			// this will unmarshal the error message into the error struct
			ne.err = dec.Decode(ne)
			ne.exitCode = exitError.ExitCode()
			return ne
		}
		return newNetavarkError("unexpected failure during execution", err)
	}

	if result != nil {
		err = dec.Decode(result)
		if err != nil {
			return newNetavarkError("failed to decode result", err)
		}
	}
	return nil
}