Skip to content

Commit df95489

Browse files
authored
Merge pull request #17 from xibz/logging_file
cmd/firectl: adding support for file based logging
2 parents d6ebdb6 + 24f72e1 commit df95489

File tree

10 files changed

+661
-134
lines changed

10 files changed

+661
-134
lines changed

cmd/firectl/main.go

Lines changed: 125 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,28 @@ import (
1919
"errors"
2020
"fmt"
2121
"os"
22+
"os/exec"
2223
"strconv"
2324
"strings"
2425

2526
"github.com/firecracker-microvm/firecracker-go-sdk"
27+
"github.com/hashicorp/go-multierror"
2628
"github.com/jessevdk/go-flags"
2729
log "github.com/sirupsen/logrus"
2830
)
2931

32+
const (
33+
terminalProgram = "xterm"
34+
// consoleXterm indicates that the machine's console should be presented in an xterm
35+
consoleXterm = "xterm"
36+
// consoleStdio indicates that the machine's console should re-use the parent's stdio streams
37+
consoleStdio = "stdio"
38+
// consoleFile inddicates that the machine's console should be presented in files rather than stdout/stderr
39+
consoleFile = "file"
40+
// consoleNone indicates that the machine's console IO should be discarded
41+
consoleNone = "none"
42+
)
43+
3044
func parseBlockDevices(entries []string) ([]firecracker.BlockDevice, error) {
3145
var devices []firecracker.BlockDevice
3246
for _, entry := range entries {
@@ -89,29 +103,33 @@ func parseVsocks(devices []string) ([]firecracker.VsockDevice, error) {
89103
return result, nil
90104
}
91105

106+
type options struct {
107+
FcBinary string `long:"firecracker-binary" description:"Path to firecracker binary"`
108+
FcConsole string `long:"firecracker-console" description:"Console type (stdio|file|xterm|none)" default:"stdio"`
109+
FcKernelImage string `long:"kernel" description:"Path to the kernel image" default:"./vmlinux"`
110+
FcKernelCmdLine string `long:"kernel-opts" description:"Kernel commandline" default:"ro console=ttyS0 noapic reboot=k panic=1 pci=off nomodules"`
111+
FcRootDrivePath string `long:"root-drive" description:"Path to root disk image"`
112+
FcRootPartUUID string `long:"root-partition" description:"Root partition UUID"`
113+
FcAdditionalDrives []string `long:"add-drive" description:"Path to additional drive, suffixed with :ro or :rw, can be specified multiple times"`
114+
FcNicConfig string `long:"tap-device" description:"NIC info, specified as DEVICE/MAC"`
115+
FcVsockDevices []string `long:"vsock-device" description:"Vsock interface, specified as PATH:CID. Multiple OK"`
116+
FcLogFifo string `long:"vmm-log-fifo" description:"FIFO for firecracker logs"`
117+
FcLogLevel string `long:"log-level" description:"vmm log level" default:"Debug"`
118+
FcLogStdoutFilePath string `long:"log-stdout-file" description:"Specifies where stdout logs should be placed"`
119+
FcLogStderrFilePath string `long:"log-stderr-file" description:"Specifies where stderr logs should be placed"`
120+
FcMetricsFifo string `long:"metrics-fifo" description:"FIFO for firecracker metrics"`
121+
FcDisableHt bool `long:"disable-hyperthreading" short:"t" description:"Disable CPU Hyperthreading"`
122+
FcCPUCount int64 `long:"ncpus" short:"c" description:"Number of CPUs" default:"1"`
123+
FcCPUTemplate string `long:"cpu-template" description:"Firecracker CPU Template (C3 or T2)"`
124+
FcMemSz int64 `long:"memory" short:"m" description:"VM memory, in MiB" default:"512"`
125+
FcMetadata string `long:"metadata" description:"Firecracker Meatadata for MMDS (json)"`
126+
Debug bool `long:"debug" short:"d" description:"Enable debug output"`
127+
Help bool `long:"help" short:"h" description:"Show usage"`
128+
}
129+
92130
func main() {
93131
var err error
94-
var opts struct {
95-
FcBinary string `long:"firecracker-binary" description:"Path to firecracker binary"`
96-
FcConsole string `long:"firecracker-console" description:"Console type (stdio|xterm|none)" default:"stdio"`
97-
FcKernelImage string `long:"kernel" description:"Path to the kernel image" default:"./vmlinux"`
98-
FcKernelCmdLine string `long:"kernel-opts" description:"Kernel commandline" default:"ro console=ttyS0 noapic reboot=k panic=1 pci=off nomodules"`
99-
FcRootDrivePath string `long:"root-drive" description:"Path to root disk image"`
100-
FcRootPartUUID string `long:"root-partition" description:"Root partition UUID"`
101-
FcAdditionalDrives []string `long:"add-drive" description:"Path to additional drive, suffixed with :ro or :rw, can be specified multiple times"`
102-
FcNicConfig string `long:"tap-device" description:"NIC info, specified as DEVICE/MAC"`
103-
FcVsockDevices []string `long:"vsock-device" description:"Vsock interface, specified as PATH:CID. Multiple OK"`
104-
FcLogFifo string `long:"vmm-log-fifo" description:"FIFO for firecracker logs"`
105-
FcLogLevel string `long:"log-level" description:"vmm log level" default:"Debug"`
106-
FcMetricsFifo string `long:"metrics-fifo" description:"FIFO for firecracker metrics"`
107-
FcDisableHt bool `long:"disable-hyperthreading" short:"t" description:"Disable CPU Hyperthreading"`
108-
FcCPUCount int64 `long:"ncpus" short:"c" description:"Number of CPUs" default:"1"`
109-
FcCPUTemplate string `long:"cpu-template" description:"Firecracker CPU Template (C3 or T2)"`
110-
FcMemSz int64 `long:"memory" short:"m" description:"VM memory, in MiB" default:"512"`
111-
FcMetadata string `long:"metadata" description:"Firecracker Meatadata for MMDS (json)"`
112-
Debug bool `long:"debug" short:"d" description:"Enable debug output"`
113-
Help bool `long:"help" short:"h" description:"Show usage"`
114-
}
132+
opts := options{}
115133

116134
p := flags.NewParser(&opts, 0)
117135
_, err = p.Parse()
@@ -172,7 +190,6 @@ func main() {
172190
}
173191

174192
fcCfg := firecracker.Config{
175-
BinPath: opts.FcBinary,
176193
SocketPath: "./firecracker.sock",
177194
LogFifo: opts.FcLogFifo,
178195
LogLevel: opts.FcLogLevel,
@@ -192,15 +209,30 @@ func main() {
192209
Debug: opts.Debug,
193210
}
194211

195-
m, err := firecracker.NewMachine(fcCfg, firecracker.WithLogger(log.NewEntry(logger)))
196-
if err != nil {
197-
log.Fatalf("Failed creating machine: %s", err)
212+
if len(os.Args) == 1 {
213+
p.WriteHelp(os.Stderr)
214+
os.Exit(0)
198215
}
199216

200217
ctx := context.Background()
201218
vmmCtx, vmmCancel := context.WithCancel(ctx)
202219
defer vmmCancel()
203-
errchan, err := m.Init(ctx)
220+
221+
cmd, cleanup, err := buildCommand(vmmCtx, fcCfg.SocketPath, opts)
222+
if err != nil {
223+
log.Fatalf("Failed to build command: %v", err)
224+
}
225+
defer cleanup()
226+
227+
m, err := firecracker.NewMachine(fcCfg,
228+
firecracker.WithLogger(log.NewEntry(logger)),
229+
firecracker.WithProcessRunner(cmd),
230+
)
231+
if err != nil {
232+
log.Fatalf("Failed creating machine: %s", err)
233+
}
234+
235+
errCh, err := m.Init(vmmCtx)
204236
if err != nil {
205237
log.Fatalf("Firecracker Init returned error %s", err)
206238
}
@@ -211,16 +243,76 @@ func main() {
211243
log.Fatalf("Firecracker SetMetadata returned error %s", err)
212244
}
213245
}
214-
err = m.StartInstance(vmmCtx)
215-
if err != nil {
216-
log.Fatalf("Firecracker StartInstance returned error %s", err)
246+
247+
if err := m.StartInstance(vmmCtx); err != nil {
248+
log.Fatalf("Failed to start instance: %v", err)
217249
}
218250

219251
// wait for the VMM to exit
220-
err = <-errchan
221-
if err != nil {
252+
if err := <-errCh; err != nil {
222253
log.Fatalf("startVMM returned error %s", err)
223-
} else {
224-
log.Printf("startVMM was happy")
225254
}
255+
log.Printf("startVMM was happy")
256+
}
257+
258+
func buildCommand(ctx context.Context, socketPath string, opts options) (*exec.Cmd, func() error, error) {
259+
var cmd *exec.Cmd
260+
b := firecracker.VMCommandBuilder{}
261+
262+
fn := func() error {
263+
return nil
264+
}
265+
266+
switch opts.FcConsole {
267+
case consoleXterm:
268+
cmd = b.WithBin(terminalProgram).
269+
AddArgs("-e", opts.FcBinary, "--api-sock", socketPath).
270+
Build(ctx)
271+
case consoleStdio:
272+
cmd = b.WithBin(opts.FcBinary).
273+
WithSocketPath(socketPath).
274+
WithStdin(os.Stdin).
275+
WithStdout(os.Stdout).
276+
WithStderr(os.Stderr).
277+
Build(ctx)
278+
case consoleFile:
279+
stdout, err := generateLogFile(opts.FcLogStdoutFilePath)
280+
if err != nil {
281+
return nil, nil, err
282+
}
283+
284+
stderr, err := generateLogFile(opts.FcLogStderrFilePath)
285+
if err != nil {
286+
return nil, nil, err
287+
}
288+
289+
fn = func() error {
290+
var merr *multierror.Error
291+
if err := stdout.Close(); err != nil {
292+
merr = multierror.Append(merr, err)
293+
}
294+
295+
if err := stderr.Close(); err != nil {
296+
merr = multierror.Append(merr, err)
297+
}
298+
299+
return merr.ErrorOrNil()
300+
}
301+
302+
cmd = b.WithBin(opts.FcBinary).
303+
WithSocketPath(socketPath).
304+
WithStdout(stdout).
305+
WithStderr(stderr).
306+
Build(ctx)
307+
default:
308+
cmd = b.WithBin(opts.FcBinary).
309+
WithSocketPath(socketPath).
310+
Build(ctx)
311+
}
312+
313+
return cmd, fn, nil
314+
}
315+
316+
func generateLogFile(filename string) (*os.File, error) {
317+
return os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
226318
}

cmd/firectl/main_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"net"
6+
"os"
7+
"os/exec"
8+
"testing"
9+
10+
"github.com/firecracker-microvm/firecracker-go-sdk"
11+
models "github.com/firecracker-microvm/firecracker-go-sdk/client/models"
12+
ops "github.com/firecracker-microvm/firecracker-go-sdk/client/operations"
13+
"github.com/firecracker-microvm/firecracker-go-sdk/fctesting"
14+
"github.com/golang/mock/gomock"
15+
)
16+
17+
func TestGenerateLogFile(t *testing.T) {
18+
cases := []struct {
19+
name string
20+
path string
21+
expectedFilePath string
22+
expectedErr bool
23+
}{
24+
{
25+
name: "simple case",
26+
path: "../..//testdata/foo",
27+
expectedFilePath: "../../testdata/foo",
28+
},
29+
{
30+
name: "directory case",
31+
path: "../../testdata",
32+
expectedErr: true,
33+
},
34+
{
35+
name: "invalid directory case",
36+
path: "../../testdata/foo/bar/baz",
37+
expectedErr: true,
38+
},
39+
}
40+
41+
for _, c := range cases {
42+
t.Run(c.name, func(t *testing.T) {
43+
f, err := generateLogFile(c.path)
44+
if err != nil && !c.expectedErr {
45+
t.Errorf("expected no error, but received %v", err)
46+
}
47+
f.Close()
48+
49+
if err == nil && c.expectedErr {
50+
t.Errorf("expected an error, but received none")
51+
}
52+
53+
if c.expectedErr {
54+
return
55+
}
56+
57+
if _, err := os.Stat(c.expectedFilePath); os.IsNotExist(err) {
58+
t.Errorf("expected file to exist")
59+
}
60+
61+
os.Remove(c.expectedFilePath)
62+
})
63+
}
64+
}
65+
66+
func TestLogFiles(t *testing.T) {
67+
ctrl := gomock.NewController(t)
68+
defer ctrl.Finish()
69+
cfg := firecracker.Config{
70+
Debug: true,
71+
Console: consoleFile,
72+
KernelImagePath: "../../testdata/vmlinux",
73+
SocketPath: "../../testdata/socket-path",
74+
RootDrive: firecracker.BlockDevice{
75+
HostPath: "../../testdata/root-drive.img",
76+
Mode: "rw",
77+
},
78+
DisableValidation: true,
79+
}
80+
81+
client := fctesting.NewMockFirecracker(ctrl)
82+
ctx := context.Background()
83+
client.
84+
EXPECT().
85+
PutMachineConfiguration(
86+
ctx,
87+
gomock.Any(),
88+
).
89+
AnyTimes().
90+
Return(&ops.PutMachineConfigurationNoContent{}, nil)
91+
92+
client.
93+
EXPECT().
94+
PutGuestBootSource(
95+
ctx,
96+
gomock.Any(),
97+
).
98+
AnyTimes().
99+
Return(&ops.PutGuestBootSourceNoContent{}, nil)
100+
101+
client.
102+
EXPECT().
103+
PutGuestDriveByID(
104+
ctx,
105+
gomock.Any(),
106+
gomock.Any(),
107+
).
108+
AnyTimes().
109+
Return(&ops.PutGuestDriveByIDNoContent{}, nil)
110+
111+
client.EXPECT().GetMachineConfig().AnyTimes().Return(&ops.GetMachineConfigOK{
112+
Payload: &models.MachineConfiguration{},
113+
}, nil)
114+
115+
stdoutPath := "../../testdata/stdout.log"
116+
stderrPath := "../../testdata/stderr.log"
117+
opts := options{
118+
FcConsole: consoleFile,
119+
FcLogStdoutFilePath: stdoutPath,
120+
FcLogStderrFilePath: stderrPath,
121+
}
122+
123+
fd, err := net.Listen("unix", cfg.SocketPath)
124+
if err != nil {
125+
t.Fatalf("unexpected error during creation of unix socket: %v", err)
126+
}
127+
128+
defer func() {
129+
fd.Close()
130+
}()
131+
132+
_, closeFn, err := buildCommand(ctx, cfg.SocketPath, opts)
133+
if err != nil {
134+
t.Fatalf("unexpected error: %v", err)
135+
}
136+
defer closeFn()
137+
defer func() {
138+
os.Remove(stdoutPath)
139+
os.Remove(stderrPath)
140+
}()
141+
142+
cmd := exec.Command("ls")
143+
m, err := firecracker.NewMachine(cfg,
144+
firecracker.WithClient(client),
145+
firecracker.WithProcessRunner(cmd),
146+
)
147+
if err != nil {
148+
t.Fatalf("unexpected error: %v", err)
149+
}
150+
_, err = m.Init(ctx)
151+
if err != nil {
152+
t.Fatalf("unexpected error: %v", err)
153+
}
154+
155+
if _, err := os.Stat(stdoutPath); os.IsNotExist(err) {
156+
t.Errorf("expected log file to be present")
157+
158+
}
159+
160+
if _, err := os.Stat(stderrPath); os.IsNotExist(err) {
161+
t.Errorf("expected log file to be present")
162+
}
163+
}

0 commit comments

Comments
 (0)