Skip to content

Commit f375d41

Browse files
committed
Support DriveMount API in CreateVM.
This implements the proposal found in docs/drive-mounts-proposal.md, with the exception for supporting RateLimiters and IsReadOnly in the DriveMount objects, due to the need for further refactoring of the internal stub drive code (will be followed up in firecracker-microvm#296). Signed-off-by: Erik Sipsma <[email protected]>
1 parent 0165b0b commit f375d41

22 files changed

+1590
-384
lines changed

agent/drive_handler.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@
1414
package main
1515

1616
import (
17+
"context"
18+
"fmt"
1719
"io/ioutil"
1820
"os"
1921
"path/filepath"
2022
"strings"
23+
"time"
2124

25+
"github.com/containerd/containerd/log"
26+
"github.com/containerd/containerd/mount"
2227
"github.com/firecracker-microvm/firecracker-containerd/internal"
28+
drivemount "github.com/firecracker-microvm/firecracker-containerd/proto/service/drivemount/ttrpc"
29+
"github.com/golang/protobuf/ptypes/empty"
30+
"github.com/pkg/errors"
2331
)
2432

2533
const (
@@ -28,6 +36,14 @@ const (
2836
blockMajorMinor = "dev"
2937
)
3038

39+
var (
40+
bannedSystemDirs = []string{
41+
"/proc",
42+
"/sys",
43+
"/dev",
44+
}
45+
)
46+
3147
type drive struct {
3248
Name string
3349
DriveID string
@@ -45,6 +61,8 @@ type driveHandler struct {
4561
DrivePath string
4662
}
4763

64+
var _ drivemount.DriveMounterService = &driveHandler{}
65+
4866
func newDriveHandler(blockPath, drivePath string) (*driveHandler, error) {
4967
d := &driveHandler{
5068
drives: map[string]drive{},
@@ -146,3 +164,107 @@ func isStubDrive(d drive) bool {
146164

147165
return internal.IsStubDrive(f)
148166
}
167+
168+
func (dh driveHandler) MountDrive(ctx context.Context, req *drivemount.MountDriveRequest) (*empty.Empty, error) {
169+
logger := log.G(ctx)
170+
logger.Debugf("%+v", req.String())
171+
logger = logger.WithField("drive_id", req.DriveID)
172+
173+
drive, ok := dh.GetDrive(req.DriveID)
174+
if !ok {
175+
return nil, fmt.Errorf("drive %q could not be found", req.DriveID)
176+
}
177+
logger = logger.WithField("drive_path", drive.Path())
178+
179+
// Do a basic check that we won't be mounting over any important system directories
180+
resolvedDest, err := evalAnySymlinks(req.DestinationPath)
181+
if err != nil {
182+
return nil, errors.Wrapf(err,
183+
"failed to evaluate any symlinks in drive mount destination %q", req.DestinationPath)
184+
}
185+
186+
for _, systemDir := range bannedSystemDirs {
187+
if isOrUnderDir(resolvedDest, systemDir) {
188+
return nil, errors.Errorf(
189+
"drive mount destination %q resolves to path %q under banned system directory %q",
190+
req.DestinationPath, resolvedDest, systemDir,
191+
)
192+
}
193+
}
194+
195+
err = os.MkdirAll(req.DestinationPath, 0700)
196+
if err != nil {
197+
return nil, errors.Wrapf(err, "failed to create drive mount destination %q", req.DestinationPath)
198+
}
199+
200+
// Retry the mount in the case of failure a fixed number of times. This works around a rare issue
201+
// where we get to this mount attempt before the guest OS has realized a drive was patched:
202+
// https://github.com/firecracker-microvm/firecracker-containerd/issues/214
203+
const (
204+
maxRetries = 100
205+
retryDelay = 10 * time.Millisecond
206+
)
207+
208+
for i := 0; i < maxRetries; i++ {
209+
err := mount.All([]mount.Mount{{
210+
Source: drive.Path(),
211+
Type: req.FilesytemType,
212+
Options: req.Options,
213+
}}, req.DestinationPath)
214+
if err == nil {
215+
return &empty.Empty{}, nil
216+
}
217+
218+
if isRetryableMountError(err) {
219+
logger.WithError(err).Warnf("retryable failure mounting drive")
220+
time.Sleep(retryDelay)
221+
continue
222+
}
223+
224+
return nil, errors.Wrapf(err, "non-retryable failure mounting drive from %q to %q",
225+
drive.Path(), req.DestinationPath)
226+
}
227+
228+
return nil, errors.Errorf("exhausted retries mounting drive from %q to %q",
229+
drive.Path(), req.DestinationPath)
230+
}
231+
232+
// evalAnySymlinks is similar to filepath.EvalSymlinks, except it will not return an error if part of the
233+
// provided path does not exist. It will evaluate symlinks present in the path up to a component that doesn't
234+
// exist, at which point it will just append the rest of the provided path to what has been resolved so far.
235+
// We validate earlier that input to this function is an absolute path.
236+
func evalAnySymlinks(path string) (string, error) {
237+
curPath := "/"
238+
pathSplit := strings.Split(filepath.Clean(path), "/")
239+
for len(pathSplit) > 0 {
240+
curPath = filepath.Join(curPath, pathSplit[0])
241+
pathSplit = pathSplit[1:]
242+
243+
resolvedPath, err := filepath.EvalSymlinks(curPath)
244+
if os.IsNotExist(err) {
245+
return filepath.Join(append([]string{curPath}, pathSplit...)...), nil
246+
}
247+
if err != nil {
248+
return "", err
249+
}
250+
curPath = resolvedPath
251+
}
252+
253+
return curPath, nil
254+
}
255+
256+
// returns whether the given path is the provided baseDir or is under it
257+
func isOrUnderDir(path, baseDir string) bool {
258+
path = filepath.Clean(path)
259+
baseDir = filepath.Clean(baseDir)
260+
261+
if baseDir == "/" {
262+
return true
263+
}
264+
265+
if path == baseDir {
266+
return true
267+
}
268+
269+
return strings.HasPrefix(path, baseDir+"/")
270+
}

agent/drive_handler_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@
1414
package main
1515

1616
import (
17+
"os"
18+
"path/filepath"
19+
"strconv"
1720
"testing"
21+
"time"
1822

1923
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
2025
)
2126

2227
func TestDiscoverDrives(t *testing.T) {
@@ -39,3 +44,124 @@ func TestDiscoverDrives(t *testing.T) {
3944
})
4045
}
4146
}
47+
48+
func TestEvalAnySymlinks(t *testing.T) {
49+
existingSymlink := "/proc/self/cwd" // will always exist and be a symlink to our cwd
50+
nonExistentPath := filepath.Join(strconv.Itoa(int(time.Now().UnixNano())), "foo")
51+
testPath := filepath.Join(existingSymlink, nonExistentPath)
52+
53+
resolvedPath, err := evalAnySymlinks(testPath)
54+
require.NoError(t, err, "failed to evaluate symlinks in %q", testPath)
55+
56+
cwd, err := os.Getwd()
57+
require.NoError(t, err, "failed to get current working dir")
58+
assert.Equal(t, filepath.Join(cwd, nonExistentPath), resolvedPath)
59+
}
60+
61+
func TestIsOrUnderDir(t *testing.T) {
62+
type testcase struct {
63+
baseDir string
64+
path string
65+
expectedTrue bool
66+
}
67+
68+
for _, tc := range []testcase{
69+
{
70+
baseDir: "/foo",
71+
path: "/foo/bar",
72+
expectedTrue: true,
73+
},
74+
{
75+
baseDir: "/foo/bar",
76+
path: "/foo/bar/baz",
77+
expectedTrue: true,
78+
},
79+
{
80+
baseDir: "/foo",
81+
path: "/foo",
82+
expectedTrue: true,
83+
},
84+
{
85+
baseDir: "/foo/bar",
86+
path: "/foo/bar",
87+
expectedTrue: true,
88+
},
89+
{
90+
baseDir: "/foo",
91+
path: "/foobar",
92+
expectedTrue: false,
93+
},
94+
{
95+
baseDir: "/foo",
96+
path: "/bar",
97+
expectedTrue: false,
98+
},
99+
{
100+
baseDir: "/foo/bar",
101+
path: "/bar",
102+
expectedTrue: false,
103+
},
104+
{
105+
baseDir: "/foo/bar",
106+
path: "/foo",
107+
expectedTrue: false,
108+
},
109+
{
110+
baseDir: "/foo/bar",
111+
path: "/bar/bar",
112+
expectedTrue: false,
113+
},
114+
{
115+
baseDir: "/foo",
116+
path: "foo",
117+
expectedTrue: false,
118+
},
119+
{
120+
baseDir: "/foo",
121+
path: "bar",
122+
expectedTrue: false,
123+
},
124+
{
125+
baseDir: "/foo",
126+
path: "/foo/../foo",
127+
expectedTrue: true,
128+
},
129+
{
130+
baseDir: "/foo/bar",
131+
path: "/foo/../foo/bar",
132+
expectedTrue: true,
133+
},
134+
{
135+
baseDir: "/foo",
136+
path: "/foo/../bar",
137+
expectedTrue: false,
138+
},
139+
{
140+
baseDir: "/foo",
141+
path: "/foo/..bar",
142+
expectedTrue: true,
143+
},
144+
{
145+
baseDir: "/foo",
146+
path: "/foo/..bar/baz",
147+
expectedTrue: true,
148+
},
149+
{
150+
baseDir: "/",
151+
path: "/",
152+
expectedTrue: true,
153+
},
154+
{
155+
baseDir: "/foo",
156+
path: "/",
157+
expectedTrue: false,
158+
},
159+
{
160+
baseDir: "/",
161+
path: "/foo",
162+
expectedTrue: true,
163+
},
164+
} {
165+
assert.Equalf(t, tc.expectedTrue, isOrUnderDir(tc.path, tc.baseDir), "unexpected output for isOrUnderDir case %+v", tc)
166+
}
167+
}

agent/main.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import (
3434
"github.com/firecracker-microvm/firecracker-containerd/eventbridge"
3535
"github.com/firecracker-microvm/firecracker-containerd/internal/event"
3636
"github.com/firecracker-microvm/firecracker-containerd/internal/vm"
37+
38+
drivemount "github.com/firecracker-microvm/firecracker-containerd/proto/service/drivemount/ttrpc"
3739
)
3840

3941
const (
@@ -77,19 +79,25 @@ func main() {
7779

7880
log.G(shimCtx).Info("creating task service")
7981

82+
server, err := ttrpc.NewServer()
83+
if err != nil {
84+
log.G(shimCtx).WithError(err).Fatal("failed to create ttrpc server")
85+
}
86+
8087
eventExchange := &event.ExchangeCloser{Exchange: exchange.NewExchange()}
88+
eventbridge.RegisterGetterService(server, eventbridge.NewGetterService(shimCtx, eventExchange))
89+
8190
taskService, err := NewTaskService(shimCtx, shimCancel, eventExchange)
8291
if err != nil {
8392
log.G(shimCtx).WithError(err).Fatal("failed to create task service")
8493
}
94+
taskAPI.RegisterTaskService(server, taskService)
8595

86-
server, err := ttrpc.NewServer()
96+
dh, err := newDriveHandler(blockPath, drivePath)
8797
if err != nil {
88-
log.G(shimCtx).WithError(err).Fatal("failed to create ttrpc server")
98+
log.G(shimCtx).WithError(err).Fatal("failed to create drive handler")
8999
}
90-
91-
taskAPI.RegisterTaskService(server, taskService)
92-
eventbridge.RegisterGetterService(server, eventbridge.NewGetterService(shimCtx, eventExchange))
100+
drivemount.RegisterDriveMounterService(server, dh)
93101

94102
// Run ttrpc over vsock
95103

0 commit comments

Comments
 (0)