-
-
Save quin2/f9e1f6e1392c1b8abd1284731c34afab to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | |
CloudInitPath string `json:"cloud_init_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) | |
//TODO: spin down virtual networks... | |
} | |
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: "../release-v1.1.4-x86_64/firecracker-v1.1.4-x86_64", | |
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, | |
TapIp: gateway_ip, | |
MaskShort: "/30", | |
} | |
} | |
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), | |
} | |
//set up network bridge | |
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("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) | |
} | |
/* | |
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", "addr", "add", fmt.Sprintf("%s%s", opts.TapIp, opts.MaskShort), "dev", opts.TapDev).Run(); err != nil { | |
return nil, fmt.Errorf("Failed creating ip link: %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) | |
} | |
//set up access to outside world | |
if err := exec.Command("sh", "-c", "echo 1 > /proc/sys/net/ipv4/ip_forward").Run(); err != nil { | |
return nil, fmt.Errorf("Failed setting up ip forwarding: %s", err) | |
} | |
if err := exec.Command("iptables", "-t", "nat", "-A", "POSTROUTING", "-o", "eth0", "-j", "MASQUERADE").Run(); err != nil { | |
return nil, fmt.Errorf("Failed setting up postrouting: %s", err) | |
} | |
if err := exec.Command("iptables", "-A", "FORWARD", "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT").Run(); err != nil { | |
return nil, fmt.Errorf("Failed setting up iptables forwarding: %s", err) | |
} | |
if err := exec.Command("iptables", "-A", "FORWARD", "-i", opts.TapDev, "-o", "eth0", "-j", "ACCEPT").Run(); err != nil { | |
return nil, fmt.Errorf("Failed final iptables: %s", err) | |
} | |
//create noo machine | |
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) //issue is now here! | |
} | |
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"` | |
TapIp string | |
MaskShort string | |
} | |
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), | |
}, | |
//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