204 lines
4.4 KiB
Go
204 lines
4.4 KiB
Go
package main
|
|
|
|
// https://stanislas.blog/2021/08/firecracker/
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/firecracker-microvm/firecracker-go-sdk"
|
|
"github.com/firecracker-microvm/firecracker-go-sdk/client/models"
|
|
"github.com/jaevor/go-nanoid"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
)
|
|
|
|
func main() {
|
|
e := echo.New()
|
|
e.Use(middleware.Logger())
|
|
e.Use(middleware.Recover())
|
|
e.POST("/run", run)
|
|
|
|
e.Logger.Fatal(e.Start(":8080"))
|
|
}
|
|
|
|
type runResp struct {
|
|
VmId string
|
|
}
|
|
|
|
func run(c echo.Context) error {
|
|
script, err := ioutil.ReadAll(c.Request().Body)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
vmid, agent, m := startVM()
|
|
err = agent.run(script)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
go func() {
|
|
ctx := context.Background()
|
|
if err := m.Wait(ctx); err != nil {
|
|
panic(err)
|
|
}
|
|
}()
|
|
defer agent.off()
|
|
|
|
return c.JSON(http.StatusOK, runResp{
|
|
VmId: vmid,
|
|
})
|
|
}
|
|
|
|
func startVM() (string, agentConfig, *firecracker.Machine) {
|
|
nanid, err := nanoid.Standard(21)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
vmid := nanid()
|
|
secret := nanid()
|
|
socketPath := "/tmp/firecracker-" + vmid + ".sock"
|
|
|
|
cfg := firecracker.Config{
|
|
SocketPath: socketPath,
|
|
KernelImagePath: "vmlinux.bin",
|
|
Drives: firecracker.NewDrivesBuilder("./rootfs.ext4").Build(),
|
|
NetworkInterfaces: []firecracker.NetworkInterface{{
|
|
CNIConfiguration: &firecracker.CNIConfiguration{
|
|
NetworkName: "fcnet",
|
|
IfName: "veth0-fire",
|
|
BinPath: []string{"/opt/cni/bin", "/usr/libexec/cni"},
|
|
},
|
|
}},
|
|
MachineCfg: models.MachineConfiguration{
|
|
VcpuCount: firecracker.Int64(1),
|
|
MemSizeMib: firecracker.Int64(1024),
|
|
},
|
|
// TODO: setup jailer
|
|
KernelArgs: "console=ttyS0 reboot=k panic=1 pci=off fireactions.secret=" + secret,
|
|
}
|
|
|
|
// TODO: change stdout/stderr files
|
|
stdoutPath := "/tmp/stdout.log"
|
|
stdout, err := os.OpenFile(stdoutPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to create stdout file: %v", err))
|
|
}
|
|
stderrPath := "/tmp/stderr.log"
|
|
stderr, err := os.OpenFile(stderrPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to create stderr file: %v", err))
|
|
}
|
|
|
|
ctx := context.Background()
|
|
// build our custom command that contains our two files to
|
|
// write to during process execution
|
|
cmd := firecracker.VMCommandBuilder{}.
|
|
WithBin("firecracker").
|
|
WithSocketPath(socketPath).
|
|
WithStdout(stdout).
|
|
WithStderr(stderr).
|
|
Build(ctx)
|
|
|
|
m, err := firecracker.NewMachine(ctx, cfg, firecracker.WithProcessRunner(cmd))
|
|
if err != nil {
|
|
panic(fmt.Errorf("failed to create new machine: %v", err))
|
|
}
|
|
|
|
if err := m.Start(ctx); err != nil {
|
|
panic(fmt.Errorf("failed to initialize machine: %v", err))
|
|
}
|
|
|
|
ip := m.Cfg.NetworkInterfaces[0].StaticConfiguration.IPConfiguration.IPAddr.IP
|
|
log.Printf("IP: %s", ip.String())
|
|
|
|
// defer m.StopVMM()
|
|
|
|
agent := agentConfig{ip: ip.String(), secret: secret}
|
|
|
|
if err := agent.waitForAgent(); err != nil {
|
|
log.Panic(err)
|
|
}
|
|
|
|
go func() {
|
|
ctx := context.Background()
|
|
if err := m.Wait(ctx); err != nil {
|
|
panic(err)
|
|
}
|
|
os.Remove(cfg.SocketPath)
|
|
}()
|
|
// if err := m.Wait(ctx); err != nil {
|
|
// panic(err)
|
|
// }
|
|
|
|
return vmid, agent, m
|
|
}
|
|
|
|
type agentConfig struct {
|
|
ip string
|
|
secret string
|
|
}
|
|
|
|
func (a agentConfig) request() *http.Request {
|
|
req, err := http.NewRequest("GET", "http://"+a.ip+":8080/hello", nil)
|
|
req.Header.Set("Authorization", "Bearer "+a.secret)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return req
|
|
}
|
|
|
|
func (a agentConfig) run(script []byte) error {
|
|
req, err := http.NewRequest("POST", "http://"+a.ip+":8080/run", bytes.NewBuffer(script))
|
|
req.Header.Set("Authorization", "Bearer "+a.secret)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
res, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
byt, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log.Println("[QIOdLqxLoKIMQ0uGoxNuu]", string(byt))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a agentConfig) off() error {
|
|
return a.run([]byte("#!/bin/sh\nreboot"))
|
|
}
|
|
|
|
func (a agentConfig) waitForAgent() error {
|
|
client := http.Client{
|
|
Timeout: time.Millisecond * 50,
|
|
}
|
|
for {
|
|
log.Println("waiting for agent to come up...")
|
|
req := a.request()
|
|
req.Method = "GET"
|
|
req.URL.Path = "/hello"
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
time.Sleep(time.Millisecond * 200)
|
|
continue
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
log.Println("Agent is failing", resp)
|
|
return errors.New("Agent is failing")
|
|
}
|
|
break
|
|
}
|
|
return nil
|
|
}
|