Repository: firecracker-microvm/firecracker-containerd
Pull request #105 (2019-02-11 21:18:53)

Pass Firecracker VM opts
*Issue #, if available:* #76

*Description of changes:* Use CreateTaskRequest's Options field to pass firecracker config args

*Testing done:* Tested with the `taskworkflow.go` code attached in the PR. Output from execution:

```bash
2019/02/11 21:02:48.813898 Creating contained client
2019/02/11 21:02:48.814324 Created contained client
2019/02/11 21:02:48.981848 Successfully pulled docker.io/library/nginx:latest image
2019/02/11 21:02:51.202941 Successfully created task: demo for the container
2019/02/11 21:02:51.202962 Completed waiting for the container task
2019/02/11 21:02:51.231901 Successfully started the container task
2019/02/11 21:02:54.232051 Executing http GET on 172.16.0.2
2019/02/11 21:02:54.233193 Response from [172.16.0.2]:
[<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
]
172.16.0.1 - - [11/Feb/2019:21:02:54 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
```

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/Makefile b/Makefile
index 62c4af0..51be554 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,7 @@
 # express or implied. See the License for the specific language governing
 # permissions and limitations under the License.
 
-SUBDIRS:=agent runtime snapshotter
+SUBDIRS:=agent runtime snapshotter examples
 export INSTALLROOT?=/usr/local
 export STATIC_AGENT
 
diff --git a/examples/.gitignore b/examples/.gitignore
new file mode 100644
index 0000000..8a806b3
--- /dev/null
+++ b/examples/.gitignore
@@ -0,0 +1,1 @@
+taskworkflow
diff --git a/examples/Makefile b/examples/Makefile
new file mode 100644
index 0000000..86ede77
--- /dev/null
+++ b/examples/Makefile
@@ -0,0 +1,24 @@
+# Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You may
+# not use this file except in compliance with the License. A copy of the
+# License is located at
+#
+# 	http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is distributed
+# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+# express or implied. See the License for the specific language governing
+# permissions and limitations under the License.
+
+all: examples
+
+examples: taskworkflow
+
+taskworkflow: taskworkflow.go
+	go build -o taskworkflow taskworkflow.go
+
+clean:
+	- rm -f taskworkflow
+
+.PHONY: all examples clean
diff --git a/examples/taskworkflow.go b/examples/taskworkflow.go
new file mode 100644
index 0000000..42a53ec
--- /dev/null
+++ b/examples/taskworkflow.go
@@ -0,0 +1,157 @@
+// Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License"). You may
+// not use this file except in compliance with the License. A copy of the
+// License is located at
+//
+//	http://aws.amazon.com/apache2.0/
+//
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+
+package main
+
+import (
+	"context"
+	"flag"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"syscall"
+	"time"
+
+	"github.com/containerd/containerd"
+	"github.com/containerd/containerd/cio"
+	"github.com/containerd/containerd/namespaces"
+	"github.com/containerd/containerd/oci"
+	"github.com/firecracker-microvm/firecracker-containerd/proto"
+	specs "github.com/opencontainers/runtime-spec/specs-go"
+	"github.com/pkg/errors"
+)
+
+func main() {
+	var ip = flag.String("ip", "", "ip address assigned to the container")
+	log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
+	flag.Parse()
+	if err := taskWorkflow(*ip); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func taskWorkflow(containerIP string) error {
+	log.Println("Creating containerd client")
+	client, err := containerd.New("/run/containerd/containerd.sock")
+	if err != nil {
+		return errors.Wrapf(err, "creating client")
+
+	}
+	defer client.Close()
+	log.Println("Created containerd client")
+
+	ctx := namespaces.WithNamespace(context.Background(), "firecracker-containerd-example")
+	image, err := client.Pull(ctx, "docker.io/library/nginx:latest",
+		containerd.WithPullUnpack,
+		containerd.WithPullSnapshotter("firecracker-naive"),
+	)
+	if err != nil {
+		return errors.Wrapf(err, "creating container")
+
+	}
+	log.Printf("Successfully pulled %s image\n", image.Name())
+	container, err := client.NewContainer(
+		ctx,
+		"demo",
+		containerd.WithSnapshotter("firecracker-naive"),
+		containerd.WithNewSnapshot("demo-snapshot", image),
+		containerd.WithNewSpec(
+			oci.WithImageConfig(image),
+			oci.WithHostNamespace(specs.NetworkNamespace),
+			oci.WithHostHostsFile,
+			oci.WithHostResolvconf,
+		),
+		containerd.WithRuntime("aws.firecracker", nil),
+	)
+	if err != nil {
+		return err
+	}
+	defer container.Delete(ctx, containerd.WithSnapshotCleanup)
+
+	task, err := container.NewTask(ctx,
+		cio.NewCreator(cio.WithStdio),
+		withFirecrackerConfig(containerIP, "AA:FC:00:00:00:01", "tap0"),
+	)
+	if err != nil {
+		return errors.Wrapf(err, "creating task")
+
+	}
+	defer task.Delete(ctx)
+
+	log.Printf("Successfully created task: %s for the container\n", task.ID())
+	exitStatusC, err := task.Wait(ctx)
+	if err != nil {
+		return errors.Wrapf(err, "waiting for task")
+
+	}
+
+	log.Println("Completed waiting for the container task")
+	if err := task.Start(ctx); err != nil {
+		return errors.Wrapf(err, "starting task")
+
+	}
+
+	log.Println("Successfully started the container task")
+	time.Sleep(3 * time.Second)
+
+	if containerIP != "" {
+		log.Println("Executing http GET on " + containerIP)
+		getResponse(containerIP)
+	}
+
+	if err := task.Kill(ctx, syscall.SIGTERM); err != nil {
+		return errors.Wrapf(err, "killing task")
+
+	}
+	status := <-exitStatusC
+	code, _, err := status.Result()
+	if err != nil {
+		return errors.Wrapf(err, "getting task's exit code")
+
+	}
+	log.Printf("task exited with status: %d\n", code)
+	return nil
+}
+
+func withFirecrackerConfig(containerIP string, mac string, hostDevName string) containerd.NewTaskOpts {
+	return func(ctx context.Context, _ *containerd.Client, ti *containerd.TaskInfo) error {
+		if containerIP != "" {
+			firecrackerConfig := &proto.FirecrackerConfig{
+				NetworkInterfaces: []*proto.FirecrackerNetworkInterface{
+					{
+						MacAddress:  mac,
+						HostDevName: hostDevName,
+					},
+				},
+			}
+			ti.Options = firecrackerConfig
+		}
+		return nil
+	}
+}
+
+func getResponse(containerIP string) {
+	response, err := http.Get("http://" + containerIP)
+	if err != nil {
+		log.Println("Unable to get response from " + containerIP)
+		return
+	}
+	defer response.Body.Close()
+	contents, err := ioutil.ReadAll(response.Body)
+	if err != nil {
+		log.Printf("Unable to read response body: %v\n", err)
+		return
+	}
+
+	log.Printf("Response from [%s]: \n[%s]\n", containerIP, contents)
+}
diff --git a/examples/taskworkflow.md b/examples/taskworkflow.md
new file mode 100644
index 0000000..9db7144
--- /dev/null
+++ b/examples/taskworkflow.md
@@ -0,0 +1,84 @@
+### About
+The `taskworkfow.go` file contains code to pull and unpack a container image
+using the `naive` snaphotter, create a container and execute a task using the
+`firecracker-containerd` runtime.
+
+### Building
+After checking out the `firecracker-containerd` repo, you can build this
+example using the `make examples` command.
+
+### Running
+For a basic workflow, without any networking setup, just run the executable
+built with the command in the previous section as super user:
+```bash
+$ sudo /path/to/firecracker-containerd/examples/taskworkflow
+```
+
+For a workflow with networking setup for the container, create a tap device
+for the VM by following [these instructions](https://github.com/firecracker-microvm/firecracker/blob/master/docs/network-setup.md).
+
+This will create a tap device named `tap0`. Now, edit the kernel args in the
+firecracker-containerd config to set a static ip address from the
+`172.16.0.1/24` subnet. The following example sets it to `172.16.0.2`.
+** NOTE: This example will not work if you're running more than 1 container
+on a host at the same time **
+```bash
+$ cat /etc/containerd/firecracker-runtime.json
+{
+    "firecracker_binary_path": "/usr/local/bin/firecracker",
+    "socket_path": "./firecracker.sock",
+    "kernel_image_path": "/var/lib/firecracker-containerd/runtime/vmlinux-2018-07-03",
+    "kernel_args": "console=ttyS0 noapic reboot=k panic=1 pci=off nomodules rw ip=172.16.0.2::172.16.0.1:255.255.255.0:::off::::",
+    "root_drive": "/var/lib/firecracker-containerd/runtime/amzn2-firecracker-fc-ctrd-2.0.20190130.runc-x86_64.ext4",
+    "cpu_count": 1,
+    "cpu_template": "T2",
+    "console": "stdio",
+    "log_fifo": "/tmp/fc-logs.fifo",
+    "log_level": "Debug",
+    "metrics_fifo": "/tmp/fc-metrics.fifo"
+}
+```
+
+Now, run the example by passing the `-ip` argument:
+```bash
+$ sudo /path/to/firecracker-containerd/examples/taskworkflow -ip 172.16.0.2
+```
+
+You should see output similar to this:
+```
+2019/02/11 21:02:48.813898 Creating contained client
+2019/02/11 21:02:48.814324 Created contained client
+2019/02/11 21:02:48.981848 Successfully pulled docker.io/library/nginx:latest image
+2019/02/11 21:02:51.202941 Successfully created task: demo for the container
+2019/02/11 21:02:51.202962 Completed waiting for the container task
+2019/02/11 21:02:51.231901 Successfully started the container task
+2019/02/11 21:02:54.232051 Executing http GET on 172.16.0.2
+2019/02/11 21:02:54.233193 Response from [172.16.0.2]:
+[<!DOCTYPE html>
+<html>
+<head>
+<title>Welcome to nginx!</title>
+<style>
+    body {
+        width: 35em;
+        margin: 0 auto;
+        font-family: Tahoma, Verdana, Arial, sans-serif;
+    }
+</style>
+</head>
+<body>
+<h1>Welcome to nginx!</h1>
+<p>If you see this page, the nginx web server is successfully installed and
+working. Further configuration is required.</p>
+
+<p>For online documentation and support please refer to
+<a href="http://nginx.org/">nginx.org</a>.<br/>
+Commercial support is available at
+<a href="http://nginx.com/">nginx.com</a>.</p>
+
+<p><em>Thank you for using nginx.</em></p>
+</body>
+</html>
+]
+172.16.0.1 - - [11/Feb/2019:21:02:54 +0000] "GET / HTTP/1.1" 200 612 "-" "Go-http-client/1.1" "-"
+```
diff --git a/go.mod b/go.mod
index 4492d40..12b6f3a 100644
--- a/go.mod
+++ b/go.mod
@@ -8,15 +8,19 @@ require (
 	github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50 // indirect
 	github.com/containerd/containerd v1.2.0
 	github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac
+	github.com/containerd/cri v1.11.1 // indirect
 	github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260
 	github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3 // indirect
 	github.com/containerd/ttrpc v0.0.0-20181001154009-f51df4475b76
-	github.com/containerd/typeurl v0.0.0-20181015155603-461401dc8f19 // indirect
+	github.com/containerd/typeurl v0.0.0-20181015155603-461401dc8f19
 	github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 // indirect
+	github.com/docker/distribution v2.7.1+incompatible // indirect
+	github.com/docker/docker v1.13.1 // indirect
 	github.com/docker/go-events v0.0.0-20170721190031-9461782956ad // indirect
 	github.com/docker/go-units v0.3.3
 	github.com/firecracker-microvm/firecracker-go-sdk v0.0.0-20190111234345-e4d80c16bbb3
 	github.com/godbus/dbus v0.0.0-20181025153459-66d97aec3384 // indirect
+	github.com/gogo/googleapis v1.1.0 // indirect
 	github.com/gogo/protobuf v1.1.1
 	github.com/google/go-cmp v0.2.0 // indirect
 	github.com/hashicorp/go-multierror v1.0.0
@@ -24,10 +28,11 @@ require (
 	github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
 	github.com/opencontainers/image-spec v1.0.1 // indirect
 	github.com/opencontainers/runc v0.1.1 // indirect
-	github.com/opencontainers/runtime-spec v0.1.2-0.20181106065543-31e0d16c1cb7 // indirect
+	github.com/opencontainers/runtime-spec v0.1.2-0.20181106065543-31e0d16c1cb7
 	github.com/pkg/errors v0.8.0
 	github.com/sirupsen/logrus v1.2.0
 	github.com/stretchr/testify v1.2.2
+	github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 // indirect
 	go.etcd.io/bbolt v1.3.0
 	golang.org/x/sync v0.0.0-20181108010431-42b317875d0f
 	golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8
diff --git a/go.sum b/go.sum
index 21679e9..c4ee08e 100644
--- a/go.sum
+++ b/go.sum
@@ -20,6 +20,8 @@ github.com/containerd/containerd v1.2.0 h1:0NP+uCCcSf7IHOEw/WE1vgcKh6DKIlTqqfh+d
 github.com/containerd/containerd v1.2.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac h1:PThQaO4yCvJzJBUW1XoFQxLotWRhvX2fgljJX8yrhFI=
 github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
+github.com/containerd/cri v1.11.1 h1:mR8+eNW4zEcbWGTGEpmDd7GzMmK7IMxMSVAZ2aIDKA4=
+github.com/containerd/cri v1.11.1/go.mod h1:DavH5Qa8+6jOmeOMO3dhWoqksucZDe06LfuhBz/xPZs=
 github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260 h1:XGyg7oTtD0DoRFhbpV6x1WfV0flKC4UxXU7ab1zC08U=
 github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
 github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3 h1:esQOJREg8nw8aXj6uCN5dfW5cKUBiEJ/+nni1Q/D/sw=
@@ -32,6 +34,10 @@ github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142 h1:3jFq2xL4ZajGK
 github.com/coreos/go-systemd v0.0.0-20181031085051-9002847aa142/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
+github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
+github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/go-events v0.0.0-20170721190031-9461782956ad h1:VXIse57M5C6ezDuCPyq6QmMvEJ2xclYKZ35SfkXdm3E=
 github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
 github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
@@ -68,6 +74,8 @@ github.com/go-openapi/validate v0.17.1 h1:RfQTLHm/gEu0oSUmbTOy0PMufjkE5/pPfnqYpo
 github.com/go-openapi/validate v0.17.1/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
 github.com/godbus/dbus v0.0.0-20181025153459-66d97aec3384 h1:xNwo3yd3PZYRDAr/Dz0sBfDWY6El2xPCKJrwJVfMFjY=
 github.com/godbus/dbus v0.0.0-20181025153459-66d97aec3384/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
+github.com/gogo/googleapis v1.1.0 h1:kFkMAZBNAn4j7K0GiZr8cRYzejq68VbheufiV3YuyFI=
+github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
 github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
@@ -115,6 +123,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 h1:b6uOv7YOFK0TYG7HtkIgExQo+2RdLuwRft63jn2HWj8=
+github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
 go.etcd.io/bbolt v1.3.0 h1:oY10fI923Q5pVCVt1GBTZMn8LHo5M+RCInFpeMnV4QI=
 go.etcd.io/bbolt v1.3.0/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
diff --git a/proto/types.pb.go b/proto/types.pb.go
index f8868c3..8c800de 100644
--- a/proto/types.pb.go
+++ b/proto/types.pb.go
@@ -22,7 +22,7 @@ const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
 // Message to store bundle/config.json bytes
 type ExtraData struct {
 	JsonSpec             []byte     `protobuf:"bytes,1,opt,name=JsonSpec,proto3" json:"JsonSpec,omitempty"`
-	RuncOptions          *types.Any `protobuf:"bytes,2,opt,name=RuncOptions" json:"RuncOptions,omitempty"`
+	RuncOptions          *types.Any `protobuf:"bytes,2,opt,name=RuncOptions,proto3" json:"RuncOptions,omitempty"`
 	XXX_NoUnkeyedLiteral struct{}   `json:"-"`
 	XXX_unrecognized     []byte     `json:"-"`
 	XXX_sizecache        int32      `json:"-"`
@@ -32,7 +32,7 @@ func (m *ExtraData) Reset()         { *m = ExtraData{} }
 func (m *ExtraData) String() string { return proto.CompactTextString(m) }
 func (*ExtraData) ProtoMessage()    {}
 func (*ExtraData) Descriptor() ([]byte, []int) {
-	return fileDescriptor_types_fd1a0f9dde9f57da, []int{0}
+	return fileDescriptor_types_cfec9a3218eb42c5, []int{0}
 }
 func (m *ExtraData) XXX_Unmarshal(b []byte) error {
 	return xxx_messageInfo_ExtraData.Unmarshal(m, b)
@@ -66,24 +66,126 @@ func (m *ExtraData) GetRuncOptions() *types.Any {
 	return nil
 }
 
+// Message to specify network config for a Firecracker VM
+type FirecrackerNetworkInterface struct {
+	MacAddress           string   `protobuf:"bytes,1,opt,name=MacAddress,proto3" json:"MacAddress,omitempty"`
+	HostDevName          string   `protobuf:"bytes,2,opt,name=HostDevName,proto3" json:"HostDevName,omitempty"`
+	XXX_NoUnkeyedLiteral struct{} `json:"-"`
+	XXX_unrecognized     []byte   `json:"-"`
+	XXX_sizecache        int32    `json:"-"`
+}
+
+func (m *FirecrackerNetworkInterface) Reset()         { *m = FirecrackerNetworkInterface{} }
+func (m *FirecrackerNetworkInterface) String() string { return proto.CompactTextString(m) }
+func (*FirecrackerNetworkInterface) ProtoMessage()    {}
+func (*FirecrackerNetworkInterface) Descriptor() ([]byte, []int) {
+	return fileDescriptor_types_cfec9a3218eb42c5, []int{1}
+}
+func (m *FirecrackerNetworkInterface) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_FirecrackerNetworkInterface.Unmarshal(m, b)
+}
+func (m *FirecrackerNetworkInterface) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_FirecrackerNetworkInterface.Marshal(b, m, deterministic)
+}
+func (dst *FirecrackerNetworkInterface) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_FirecrackerNetworkInterface.Merge(dst, src)
+}
+func (m *FirecrackerNetworkInterface) XXX_Size() int {
+	return xxx_messageInfo_FirecrackerNetworkInterface.Size(m)
+}
+func (m *FirecrackerNetworkInterface) XXX_DiscardUnknown() {
+	xxx_messageInfo_FirecrackerNetworkInterface.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_FirecrackerNetworkInterface proto.InternalMessageInfo
+
+func (m *FirecrackerNetworkInterface) GetMacAddress() string {
+	if m != nil {
+		return m.MacAddress
+	}
+	return ""
+}
+
+func (m *FirecrackerNetworkInterface) GetHostDevName() string {
+	if m != nil {
+		return m.HostDevName
+	}
+	return ""
+}
+
+// Message to specify the various Firecracker VM configurations for a task
+type FirecrackerConfig struct {
+	RuncOptions          *types.Any                     `protobuf:"bytes,1,opt,name=RuncOptions,proto3" json:"RuncOptions,omitempty"`
+	NetworkInterfaces    []*FirecrackerNetworkInterface `protobuf:"bytes,2,rep,name=NetworkInterfaces,proto3" json:"NetworkInterfaces,omitempty"`
+	XXX_NoUnkeyedLiteral struct{}                       `json:"-"`
+	XXX_unrecognized     []byte                         `json:"-"`
+	XXX_sizecache        int32                          `json:"-"`
+}
+
+func (m *FirecrackerConfig) Reset()         { *m = FirecrackerConfig{} }
+func (m *FirecrackerConfig) String() string { return proto.CompactTextString(m) }
+func (*FirecrackerConfig) ProtoMessage()    {}
+func (*FirecrackerConfig) Descriptor() ([]byte, []int) {
+	return fileDescriptor_types_cfec9a3218eb42c5, []int{2}
+}
+func (m *FirecrackerConfig) XXX_Unmarshal(b []byte) error {
+	return xxx_messageInfo_FirecrackerConfig.Unmarshal(m, b)
+}
+func (m *FirecrackerConfig) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+	return xxx_messageInfo_FirecrackerConfig.Marshal(b, m, deterministic)
+}
+func (dst *FirecrackerConfig) XXX_Merge(src proto.Message) {
+	xxx_messageInfo_FirecrackerConfig.Merge(dst, src)
+}
+func (m *FirecrackerConfig) XXX_Size() int {
+	return xxx_messageInfo_FirecrackerConfig.Size(m)
+}
+func (m *FirecrackerConfig) XXX_DiscardUnknown() {
+	xxx_messageInfo_FirecrackerConfig.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_FirecrackerConfig proto.InternalMessageInfo
+
+func (m *FirecrackerConfig) GetRuncOptions() *types.Any {
+	if m != nil {
+		return m.RuncOptions
+	}
+	return nil
+}
+
+func (m *FirecrackerConfig) GetNetworkInterfaces() []*FirecrackerNetworkInterface {
+	if m != nil {
+		return m.NetworkInterfaces
+	}
+	return nil
+}
+
 func init() {
 	proto.RegisterType((*ExtraData)(nil), "firecracker.containerd.ExtraData")
+	proto.RegisterType((*FirecrackerNetworkInterface)(nil), "firecracker.containerd.FirecrackerNetworkInterface")
+	proto.RegisterType((*FirecrackerConfig)(nil), "firecracker.containerd.FirecrackerConfig")
 }
 
-func init() { proto.RegisterFile("proto/types.proto", fileDescriptor_types_fd1a0f9dde9f57da) }
-
-var fileDescriptor_types_fd1a0f9dde9f57da = []byte{
-	// 188 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x2c, 0x28, 0xca, 0x2f,
-	0xc9, 0xd7, 0x2f, 0xa9, 0x2c, 0x48, 0x2d, 0xd6, 0x03, 0xb3, 0x85, 0xc4, 0xd2, 0x32, 0x8b, 0x52,
-	0x93, 0x8b, 0x12, 0x93, 0xb3, 0x53, 0x8b, 0xf4, 0x92, 0xf3, 0xf3, 0x4a, 0x12, 0x33, 0xf3, 0x52,
-	0x8b, 0x52, 0xa4, 0x24, 0xd3, 0xf3, 0xf3, 0xd3, 0x73, 0x52, 0xf5, 0xc1, 0xaa, 0x92, 0x4a, 0xd3,
-	0xf4, 0x13, 0xf3, 0x2a, 0x21, 0x5a, 0x94, 0xe2, 0xb9, 0x38, 0x5d, 0x2b, 0x4a, 0x8a, 0x12, 0x5d,
-	0x12, 0x4b, 0x12, 0x85, 0xa4, 0xb8, 0x38, 0xbc, 0x8a, 0xf3, 0xf3, 0x82, 0x0b, 0x52, 0x93, 0x25,
-	0x18, 0x15, 0x18, 0x35, 0x78, 0x82, 0xe0, 0x7c, 0x21, 0x33, 0x2e, 0xee, 0xa0, 0xd2, 0xbc, 0x64,
-	0xff, 0x82, 0x92, 0xcc, 0xfc, 0xbc, 0x62, 0x09, 0x26, 0x05, 0x46, 0x0d, 0x6e, 0x23, 0x11, 0x3d,
-	0x88, 0xc9, 0x7a, 0x30, 0x93, 0xf5, 0x1c, 0xf3, 0x2a, 0x83, 0x90, 0x15, 0x3a, 0xd9, 0x46, 0x59,
-	0xa7, 0x67, 0x96, 0x64, 0x94, 0x26, 0xe9, 0x25, 0xe7, 0xe7, 0xea, 0x23, 0x39, 0x50, 0x37, 0x37,
-	0x33, 0xb9, 0x28, 0xbf, 0x0c, 0x55, 0x0c, 0xe1, 0x68, 0xa8, 0x63, 0xd9, 0xc0, 0x94, 0x31, 0x20,
-	0x00, 0x00, 0xff, 0xff, 0xad, 0x88, 0x4e, 0x21, 0xee, 0x00, 0x00, 0x00,
+func init() { proto.RegisterFile("proto/types.proto", fileDescriptor_types_cfec9a3218eb42c5) }
+
+var fileDescriptor_types_cfec9a3218eb42c5 = []byte{
+	// 288 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x91, 0xc1, 0x4b, 0xc3, 0x30,
+	0x18, 0xc5, 0xa9, 0x82, 0xd8, 0xd4, 0x4b, 0x83, 0xc8, 0x9c, 0x20, 0xa5, 0xa7, 0x5e, 0x4c, 0x61,
+	0x03, 0x2f, 0xe2, 0x61, 0x3a, 0x45, 0x05, 0x27, 0xc4, 0x9b, 0x97, 0x91, 0xa5, 0x5f, 0x6b, 0x98,
+	0xcd, 0x57, 0x92, 0x74, 0xda, 0x7f, 0xca, 0xbf, 0x51, 0xec, 0xd4, 0xd5, 0x29, 0xe2, 0x29, 0xc9,
+	0xe3, 0xcb, 0x7b, 0xbf, 0x97, 0x90, 0xb0, 0x32, 0xe8, 0x30, 0x75, 0x4d, 0x05, 0x96, 0xb5, 0x7b,
+	0xba, 0x97, 0x2b, 0x03, 0xd2, 0x08, 0x39, 0x07, 0xc3, 0x24, 0x6a, 0x27, 0x94, 0x06, 0x93, 0xf5,
+	0xf7, 0x0b, 0xc4, 0xe2, 0x09, 0xd2, 0x76, 0x6a, 0x56, 0xe7, 0xa9, 0xd0, 0xcd, 0xf2, 0x4a, 0x3c,
+	0x25, 0xfe, 0xc5, 0x8b, 0x33, 0x62, 0x2c, 0x9c, 0xa0, 0x7d, 0xb2, 0x7d, 0x63, 0x51, 0xdf, 0x57,
+	0x20, 0x7b, 0x5e, 0xe4, 0x25, 0x3b, 0xfc, 0xeb, 0x4c, 0x8f, 0x49, 0xc0, 0x6b, 0x2d, 0xef, 0x2a,
+	0xa7, 0x50, 0xdb, 0xde, 0x46, 0xe4, 0x25, 0xc1, 0x60, 0x97, 0x2d, 0x9d, 0xd9, 0xa7, 0x33, 0x1b,
+	0xe9, 0x86, 0x77, 0x07, 0xe3, 0x29, 0x39, 0xb8, 0x5c, 0x51, 0x4d, 0xc0, 0x3d, 0xa3, 0x99, 0x5f,
+	0x6b, 0x07, 0x26, 0x17, 0x12, 0xe8, 0x21, 0x21, 0xb7, 0x42, 0x8e, 0xb2, 0xcc, 0x80, 0xb5, 0x6d,
+	0xa8, 0xcf, 0x3b, 0x0a, 0x8d, 0x48, 0x70, 0x85, 0xd6, 0x8d, 0x61, 0x31, 0x11, 0x25, 0xb4, 0xb1,
+	0x3e, 0xef, 0x4a, 0xf1, 0xab, 0x47, 0xc2, 0x4e, 0xc2, 0x39, 0xea, 0x5c, 0x15, 0xeb, 0xb8, 0xde,
+	0x3f, 0x71, 0xa9, 0x20, 0xe1, 0x3a, 0xe3, 0x7b, 0xd9, 0xcd, 0x24, 0x18, 0x0c, 0xd9, 0xef, 0xcf,
+	0xcb, 0xfe, 0xe8, 0xc7, 0x7f, 0xba, 0x9d, 0x9d, 0x3e, 0x9c, 0x14, 0xca, 0x3d, 0xd6, 0x33, 0x26,
+	0xb1, 0x4c, 0x3b, 0x9e, 0x47, 0xa5, 0x92, 0x06, 0x17, 0xdf, 0xb5, 0x55, 0xce, 0xc7, 0xf7, 0x6d,
+	0xb5, 0xcb, 0xf0, 0x2d, 0x00, 0x00, 0xff, 0xff, 0x5a, 0xb5, 0x62, 0x79, 0x00, 0x02, 0x00, 0x00,
 }
diff --git a/proto/types.proto b/proto/types.proto
index 9bea0f1..b78bb36 100644
--- a/proto/types.proto
+++ b/proto/types.proto
@@ -11,3 +11,15 @@ message ExtraData {
 	bytes JsonSpec = 1;
 	google.protobuf.Any RuncOptions = 2;
 }
+
+// Message to specify network config for a Firecracker VM
+message FirecrackerNetworkInterface {
+	string MacAddress = 1;
+	string HostDevName = 2;
+}
+
+// Message to specify the various Firecracker VM configurations for a task
+message FirecrackerConfig {
+	google.protobuf.Any RuncOptions = 1; // Can be used by the CreateTask request needs to relay any Runc options to the agent
+	repeated FirecrackerNetworkInterface NetworkInterfaces = 2; // Can be used to set the networking configuration for a VM
+}
diff --git a/runtime/service.go b/runtime/service.go
index 7703bde..0b1f6f9 100644
--- a/runtime/service.go
+++ b/runtime/service.go
@@ -36,7 +36,8 @@ import (
 	taskAPI "github.com/containerd/containerd/runtime/v2/task"
 	"github.com/containerd/fifo"
 	"github.com/containerd/ttrpc"
-	"github.com/firecracker-microvm/firecracker-go-sdk"
+	"github.com/containerd/typeurl"
+	firecracker "github.com/firecracker-microvm/firecracker-go-sdk"
 	models "github.com/firecracker-microvm/firecracker-go-sdk/client/models"
 	ptypes "github.com/gogo/protobuf/types"
 	"github.com/mdlayher/vsock"
@@ -167,9 +168,19 @@ func (s *service) Create(ctx context.Context, request *taskAPI.CreateTaskRequest
 		"checkpoint": request.Checkpoint,
 	}).Debug("creating task")
 
+	var firecrackerConfig *proto.FirecrackerConfig
+	var err error
+	var runcOpts *ptypes.Any
+
+	if request.Options != nil {
+		firecrackerConfig, runcOpts, err = parseCreateTaskOpts(ctx, request.Options)
+		if err != nil {
+			return nil, errors.Wrapf(err, "unmarshaling task create request options")
+		}
+	}
 	// TODO: should there be a lock here
 	if !s.agentStarted {
-		client, err := s.startVM(ctx, request)
+		client, err := s.startVM(ctx, request, firecrackerConfig)
 		if err != nil {
 			log.G(ctx).WithError(err).Error("failed to start VM")
 			return nil, err
@@ -182,7 +193,7 @@ func (s *service) Create(ctx context.Context, request *taskAPI.CreateTaskRequest
 	log.G(ctx).Infof("creating task '%s'", request.ID)
 
 	// Generate new anyData with bundle/config.json packed inside
-	anyData, err := packBundle(filepath.Join(request.Bundle, "config.json"), request.Options)
+	anyData, err := packBundle(filepath.Join(request.Bundle, "config.json"), runcOpts)
 	if err != nil {
 		return nil, err
 	}
@@ -247,13 +258,13 @@ func (s *service) monitorState(ctx context.Context, id, execID string, pid uint3
 	}
 }
 
-func (s *service) proxyStdio(ctx context.Context, stdin, stdout, stderr string, CID uint32) {
-	go proxyIO(ctx, stdin, CID, internal.StdinPort, true)
-	go proxyIO(ctx, stdout, CID, internal.StdoutPort, false)
-	go proxyIO(ctx, stderr, CID, internal.StderrPort, false)
+func (s *service) proxyStdio(ctx context.Context, stdin, stdout, stderr string, cid uint32) {
+	go proxyIO(ctx, stdin, cid, internal.StdinPort, true)
+	go proxyIO(ctx, stdout, cid, internal.StdoutPort, false)
+	go proxyIO(ctx, stderr, cid, internal.StderrPort, false)
 }
 
-func proxyIO(ctx context.Context, path string, CID, port uint32, in bool) {
+func proxyIO(ctx context.Context, path string, cid, port uint32, in bool) {
 	if path == "" {
 		return
 	}
@@ -263,7 +274,7 @@ func proxyIO(ctx context.Context, path string, CID, port uint32, in bool) {
 		log.G(ctx).WithError(err).Error("error opening fifo")
 		return
 	}
-	conn, err := vsock.Dial(CID, port)
+	conn, err := vsock.Dial(cid, port)
 	if err != nil {
 		log.G(ctx).WithError(err).Error("unable to dial agent vsock")
 		f.Close()
@@ -591,7 +602,10 @@ func findNextAvailableVsockCID(ctx context.Context) (uint32, error) {
 	return 0, errors.New("couldn't find any available vsock context id")
 }
 
-func (s *service) startVM(ctx context.Context, request *taskAPI.CreateTaskRequest) (taskAPI.TaskService, error) {
+func (s *service) startVM(ctx context.Context,
+	request *taskAPI.CreateTaskRequest,
+	vmConfig *proto.FirecrackerConfig,
+) (taskAPI.TaskService, error) {
 	log.G(ctx).Info("starting VM")
 
 	cid, err := findNextAvailableVsockCID(ctx)
@@ -625,7 +639,12 @@ func (s *service) startVM(ctx context.Context, request *taskAPI.CreateTaskReques
 		driveBuilder = driveBuilder.AddDrive(mnt.Source, false)
 	}
 	cfg.Drives = driveBuilder.Build()
-
+	// Override config provided in task's create opts if any.
+	// Note: We've chosen to override here instead of merging in order to
+	// provide a cleaner, simpler interface to reason about for clients.
+	// Any config provided by clients for create task opts will always override
+	// the default cofig generated by the runtime.
+	cfg = overrideVMConfigFromTaskOpts(cfg, vmConfig)
 	cmd := firecracker.VMCommandBuilder{}.
 		WithBin(s.config.FirecrackerBinaryPath).
 		WithSocketPath(s.config.SocketPath).
@@ -662,10 +681,47 @@ func (s *service) startVM(ctx context.Context, request *taskAPI.CreateTaskReques
 	return apiClient, nil
 }
 
+func overrideVMConfigFromTaskOpts(cfg firecracker.Config, vmConfig *proto.FirecrackerConfig) firecracker.Config {
+	if vmConfig == nil {
+		return cfg
+	}
+	// Attach network interface specified in the config
+	if len(vmConfig.NetworkInterfaces) > 0 {
+		nwIfaces := make([]firecracker.NetworkInterface, len(vmConfig.NetworkInterfaces))
+		for i, nw := range vmConfig.NetworkInterfaces {
+			nwIfaces[i] = firecracker.NetworkInterface{
+				MacAddress:  nw.MacAddress,
+				HostDevName: nw.HostDevName,
+			}
+		}
+		cfg.NetworkInterfaces = nwIfaces
+	}
+
+	return cfg
+}
+
 func (s *service) stopVM() error {
 	return s.machine.StopVMM()
 }
 
+func parseCreateTaskOpts(ctx context.Context, opts *ptypes.Any) (*proto.FirecrackerConfig, *ptypes.Any, error) {
+	cfg, err := typeurl.UnmarshalAny(opts)
+	if err != nil {
+		return nil, nil, errors.Wrapf(err, "unmarshaling task create request options")
+	}
+	// We've verified that this is a valid prototype at this point of time.
+	// Check if it's the FirecrackerConfig type
+	firecrackerConfig, ok := cfg.(*proto.FirecrackerConfig)
+	if ok {
+		// We've verified that the proto message was of FirecrackerConfig type.
+		// Get runc options based on what was set in FirecrackerConfig.
+		return firecrackerConfig, firecrackerConfig.RuncOptions, nil
+	}
+	// This is a valid proto message, but is not FirecrackerConfig type.
+	// Treat the message as runc opts
+	return nil, opts, nil
+}
+
 func packBundle(path string, options *ptypes.Any) (*ptypes.Any, error) {
 	// Add the bundle/config.json to the request so it can be recreated
 	// inside the vm:
diff --git a/snapshotter/devmapper/metadata.go b/snapshotter/devmapper/metadata.go
index be79714..5389791 100644
--- a/snapshotter/devmapper/metadata.go
+++ b/snapshotter/devmapper/metadata.go
@@ -20,7 +20,7 @@ import (
 	"strconv"
 
 	"github.com/pkg/errors"
-	"go.etcd.io/bbolt"
+	bolt "go.etcd.io/bbolt"
 )
 
 type (


Test patch:
diff --git a/runtime/service_test.go b/runtime/service_test.go
index 07d3ca4..a2f2593 100644
--- a/runtime/service_test.go
+++ b/runtime/service_test.go
@@ -18,7 +18,16 @@ import (
 	"syscall"
 	"testing"
 
+	"github.com/containerd/typeurl"
+	"github.com/firecracker-microvm/firecracker-containerd/proto"
+	firecracker "github.com/firecracker-microvm/firecracker-go-sdk"
 	"github.com/stretchr/testify/require"
+	"gotest.tools/assert"
+)
+
+const (
+	mac         = "AA:FC:00:00:00:01"
+	hostDevName = "tap0"
 )
 
 func TestFindNextAvailableVsockCID(t *testing.T) {
@@ -40,3 +49,83 @@ func TestFindNextAvailableVsockCID(t *testing.T) {
 	_, err = findNextAvailableVsockCID(ctx)
 	require.Equal(t, context.Canceled, err)
 }
+
+func TestParseCreateTaskOptsParsesFirecrackerConfig(t *testing.T) {
+	inFirecrackerConfig := &proto.FirecrackerConfig{
+		NetworkInterfaces: []*proto.FirecrackerNetworkInterface{
+			{
+				MacAddress:  mac,
+				HostDevName: hostDevName,
+			},
+		},
+	}
+
+	protoFirecrackerConfig, err := typeurl.MarshalAny(inFirecrackerConfig)
+	require.NoError(t, err, "unable to marshal firecracker config proto message")
+	outFirecrackerConfig, outRuncOpts, err := parseCreateTaskOpts(context.TODO(), protoFirecrackerConfig)
+	require.NoError(t, err, "unable to parse firecracker config from proto message")
+	if outRuncOpts != nil {
+		// assert.Equal is insufficient here as the nil comparison fails for
+		// empty Any protobuf message
+		t.Error("unexpected value parsed for runc options")
+	}
+	assert.Equal(t, 1, len(outFirecrackerConfig.NetworkInterfaces))
+	assert.Equal(t, hostDevName, outFirecrackerConfig.NetworkInterfaces[0].HostDevName)
+	assert.Equal(t, mac, outFirecrackerConfig.NetworkInterfaces[0].MacAddress)
+}
+
+func TestParseCreateTaskLeavesNonFirecrackerConfigAlong(t *testing.T) {
+	in := &proto.FirecrackerNetworkInterface{
+		MacAddress:  mac,
+		HostDevName: hostDevName,
+	}
+	protoIn, err := typeurl.MarshalAny(in)
+	require.NoError(t, err, "unable to marshal proto message")
+	outFirecrackerConfig, outOpts, err := parseCreateTaskOpts(context.TODO(), protoIn)
+	require.NoError(t, err, "unable to parse firecracker config from proto message")
+	if outFirecrackerConfig != nil {
+		// assert.Equal is insufficient here as the nil comparison fails for
+		// empty Any protobuf message
+		t.Error("unexpected value parsed for firecracker config")
+	}
+	assert.Equal(t, protoIn, outOpts)
+}
+
+func TestOverrideVMConfigFromTaskOpts(t *testing.T) {
+	var testCases = []struct {
+		name        string
+		in          firecracker.Config
+		vmConfig    *proto.FirecrackerConfig
+		expectedOut firecracker.Config
+	}{
+		{
+			name: "network config",
+			in:   firecracker.Config{},
+			vmConfig: &proto.FirecrackerConfig{
+				NetworkInterfaces: []*proto.FirecrackerNetworkInterface{
+					{
+						MacAddress:  mac,
+						HostDevName: hostDevName,
+					},
+				},
+			},
+			expectedOut: firecracker.Config{
+				NetworkInterfaces: []firecracker.NetworkInterface{
+					{
+						MacAddress:  mac,
+						HostDevName: hostDevName,
+					},
+				},
+			},
+		},
+	}
+	for _, tc := range testCases {
+		// scopelint gets sad if we use tc directly. The specific complaint is
+		// "Using the variable on range scope in function literal".
+		test := tc
+		t.Run(test.name, func(t *testing.T) {
+			out := overrideVMConfigFromTaskOpts(test.in, test.vmConfig)
+			assert.DeepEqual(t, test.expectedOut, out)
+		})
+	}
+}