Skip to content

Instantly share code, notes, and snippets.

@jvns
Last active July 12, 2024 05:16
Show Gist options
  • Save jvns/9b274f24cfa1db7abecd0d32483666a3 to your computer and use it in GitHub Desktop.
Save jvns/9b274f24cfa1db7abecd0d32483666a3 to your computer and use it in GitHub Desktop.
package main
import (
"context"
"crypto/rand"
"encoding/json"
"fmt"
firecracker "github.com/firecracker-microvm/firecracker-go-sdk"
models "github.com/firecracker-microvm/firecracker-go-sdk/client/models"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"syscall"
)
type CreateRequest struct {
RootDrivePath string `json:"root_image_path"`
KernelPath string `json:"kernel_path"`
}
type CreateResponse struct {
IpAddress string `json:"ip_address"`
ID string `json:"id"`
}
type DeleteRequest struct {
ID string `json:"id"`
}
var runningVMs map[string]RunningFirecracker = make(map[string]RunningFirecracker)
var ipByte byte = 3
func main() {
http.HandleFunc("/create", createRequestHandler)
http.HandleFunc("/delete", deleteRequestHandler)
defer cleanup()
log.Fatal(http.ListenAndServe(":8080", nil))
}
func cleanup() {
for _, running := range runningVMs {
shutDown(running)
}
}
func shutDown(running RunningFirecracker) {
running.machine.StopVMM()
os.Remove(running.image)
}
func makeIso(cloudInitPath string) (string, error) {
image := "/tmp/cloud-init.iso"
metaDataPath := "/tmp/my-meta-data.yml"
err := ioutil.WriteFile(metaDataPath, []byte("instance-id: i-litchi12345"), 0644)
if err != nil {
return "", fmt.Errorf("Failed to create metadata file: %s", err)
}
if err := exec.Command("cloud-localds", image, cloudInitPath, metaDataPath).Run(); err != nil {
return "", fmt.Errorf("cloud-localds failed: %s", err)
}
return image, nil
}
func deleteRequestHandler(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Fatalf("failed to read body, %s", err)
}
var req DeleteRequest
json.Unmarshal([]byte(body), &req)
if err != nil {
log.Fatalf(err.Error())
}
running := runningVMs[req.ID]
shutDown(running)
delete(runningVMs, req.ID)
}
func createRequestHandler(w http.ResponseWriter, r *http.Request) {
ipByte += 1
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Fatalf("failed to read body, %s", err)
}
var req CreateRequest
json.Unmarshal([]byte(body), &req)
opts := getOptions(ipByte, req)
running, err := opts.createVMM(context.Background())
if err != nil {
log.Fatalf(err.Error())
}
id := pseudo_uuid()
resp := CreateResponse{
IpAddress: opts.FcIP,
ID: id,
}
response, err := json.Marshal(&resp)
if err != nil {
log.Fatalf("failed to marshal json, %s", err)
}
w.Header().Add("Content-Type", "application/json")
w.Write(response)
runningVMs[id] = *running
go func() {
defer running.cancelCtx()
// there's an error here but we ignore it for now because we terminate
// the VM on /delete and it returns an error when it's terminated
running.machine.Wait(running.ctx)
}()
}
func pseudo_uuid() string {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
log.Fatalf("failed to generate uuid, %s", err)
}
return fmt.Sprintf("%X-%X-%X-%X-%X", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}
func getOptions(id byte, req CreateRequest) options {
fc_ip := net.IPv4(172, 102, 0, id).String()
gateway_ip := "172.102.0.1"
docker_mask_long := "255.255.255.0"
bootArgs := "ro console=ttyS0 noapic reboot=k panic=1 pci=off nomodules random.trust_cpu=on "
bootArgs = bootArgs + fmt.Sprintf("ip=%s::%s:%s::eth0:off", fc_ip, gateway_ip, docker_mask_long)
return options{
FcBinary: "/usr/bin/firecracker",
Request: req,
FcKernelCmdLine: bootArgs,
FcSocketPath: fmt.Sprintf("/tmp/firecracker-%d.sock", id),
TapMacAddr: fmt.Sprintf("02:FC:00:00:00:%02x", id),
TapDev: fmt.Sprintf("fc-tap-%d", id),
FcIP: fc_ip,
FcCPUCount: 1,
FcMemSz: 512,
}
}
type RunningFirecracker struct {
ctx context.Context
cancelCtx context.CancelFunc
image string
machine *firecracker.Machine
}
func (opts *options) createVMM(ctx context.Context) (*RunningFirecracker, error) {
vmmCtx, vmmCancel := context.WithCancel(ctx)
rootImagePath, err := copyImage(opts.Request.RootDrivePath)
opts.Request.RootDrivePath = rootImagePath
if err != nil {
return nil, fmt.Errorf("Failed copying root path: %s", err)
}
fcCfg, err := opts.getConfig()
if err != nil {
return nil, err
}
cmd := firecracker.VMCommandBuilder{}.
WithBin(opts.FcBinary).
WithSocketPath(fcCfg.SocketPath).
WithStdin(os.Stdin).
WithStdout(os.Stdout).
WithStderr(os.Stderr).
Build(ctx)
machineOpts := []firecracker.Opt{
firecracker.WithProcessRunner(cmd),
}
exec.Command("ip", "link", "del", opts.TapDev).Run()
if err := exec.Command("ip", "tuntap", "add", "dev", opts.TapDev, "mode", "tap").Run(); err != nil {
return nil, fmt.Errorf("Failed creating ip link: %s", err)
}
if err := exec.Command("rm", "-f", opts.FcSocketPath).Run(); err != nil {
return nil, fmt.Errorf("Failed to delete old socket path: %s", err)
}
if err := exec.Command("ip", "link", "set", opts.TapDev, "master", "firecracker0").Run(); err != nil {
return nil, fmt.Errorf("Failed adding tap device to bridge: %s", err)
}
if err := exec.Command("ip", "link", "set", opts.TapDev, "up").Run(); err != nil {
return nil, fmt.Errorf("Failed creating ip link: %s", err)
}
if err := exec.Command("sysctl", "-w", fmt.Sprintf("net.ipv4.conf.%s.proxy_arp=1", opts.TapDev)).Run(); err != nil {
return nil, fmt.Errorf("Failed doing first sysctl: %s", err)
}
if err := exec.Command("sysctl", "-w", fmt.Sprintf("net.ipv6.conf.%s.disable_ipv6=1", opts.TapDev)).Run(); err != nil {
return nil, fmt.Errorf("Failed doing second sysctl: %s", err)
}
m, err := firecracker.NewMachine(vmmCtx, *fcCfg, machineOpts...)
if err != nil {
return nil, fmt.Errorf("Failed creating machine: %s", err)
}
if err := m.Start(vmmCtx); err != nil {
return nil, fmt.Errorf("Failed to start machine: %v", err)
}
installSignalHandlers(vmmCtx, m)
return &RunningFirecracker{
ctx: vmmCtx,
image: rootImagePath,
cancelCtx: vmmCancel,
machine: m,
}, nil
}
type options struct {
Id string `long:"id" description:"Jailer VMM id"`
// maybe make this an int instead
IpId byte `byte:"id" description:"an ip we use to generate an ip address"`
FcBinary string `long:"firecracker-binary" description:"Path to firecracker binary"`
FcKernelCmdLine string `long:"kernel-opts" description:"Kernel commandline"`
Request CreateRequest
FcSocketPath string `long:"socket-path" short:"s" description:"path to use for firecracker socket"`
TapMacAddr string `long:"tap-mac-addr" description:"tap macaddress"`
TapDev string `long:"tap-dev" description:"tap device"`
FcCPUCount int64 `long:"ncpus" short:"c" description:"Number of CPUs"`
FcMemSz int64 `long:"memory" short:"m" description:"VM memory, in MiB"`
FcIP string `long:"fc-ip" description:"IP address of the VM"`
}
func (opts *options) getConfig() (*firecracker.Config, error) {
drives := []models.Drive{
models.Drive{
DriveID: firecracker.String("1"),
PathOnHost: &opts.Request.RootDrivePath,
IsRootDevice: firecracker.Bool(true),
IsReadOnly: firecracker.Bool(false),
},
}
if opts.Request.CloudInitPath != "" {
isoPath, err := makeIso(opts.Request.CloudInitPath)
if err != nil {
return nil, fmt.Errorf("Failed to create iso: %s", err)
}
drives = append(drives, models.Drive{
DriveID: firecracker.String("2"),
PathOnHost: &isoPath,
IsRootDevice: firecracker.Bool(false),
IsReadOnly: firecracker.Bool(true),
})
}
return &firecracker.Config{
VMID: opts.Id,
SocketPath: opts.FcSocketPath,
KernelImagePath: opts.Request.KernelPath,
KernelArgs: opts.FcKernelCmdLine,
Drives: drives,
NetworkInterfaces: []firecracker.NetworkInterface{
firecracker.NetworkInterface{
StaticConfiguration: &firecracker.StaticNetworkConfiguration{
MacAddress: opts.TapMacAddr,
HostDevName: opts.TapDev,
},
//AllowMMDS: allowMMDS,
},
},
MachineCfg: models.MachineConfiguration{
VcpuCount: firecracker.Int64(opts.FcCPUCount),
MemSizeMib: firecracker.Int64(opts.FcMemSz),
//CPUTemplate: models.CPUTemplate(opts.FcCPUTemplate),
HtEnabled: firecracker.Bool(false),
},
//JailerCfg: jail,
//VsockDevices: vsocks,
//LogFifo: opts.FcLogFifo,
//LogLevel: opts.FcLogLevel,
//MetricsFifo: opts.FcMetricsFifo,
//FifoLogWriter: fifo,
}, nil
}
func copyImage(src string) (string, error) {
sourceFileStat, err := os.Stat(src)
if err != nil {
return "", err
}
if !sourceFileStat.Mode().IsRegular() {
return "", fmt.Errorf("%s is not a regular file", src)
}
source, err := os.Open(src)
if err != nil {
return "", err
}
defer source.Close()
destination, err := ioutil.TempFile("/images", "image")
if err != nil {
return "", err
}
defer destination.Close()
_, err = io.Copy(destination, source)
return destination.Name(), err
}
func installSignalHandlers(ctx context.Context, m *firecracker.Machine) {
// not sure if this is actually really helping with anything
go func() {
// Clear some default handlers installed by the firecracker SDK:
signal.Reset(os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)
for {
switch s := <-c; {
case s == syscall.SIGTERM || s == os.Interrupt:
log.Printf("Caught SIGINT, requesting clean shutdown")
m.Shutdown(ctx)
case s == syscall.SIGQUIT:
log.Printf("Caught SIGTERM, forcing shutdown")
m.StopVMM()
}
}
}()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment