Skip to content

Commit 0f7ac63

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 #296). Signed-off-by: Erik Sipsma <[email protected]>
1 parent 65cc752 commit 0f7ac63

23 files changed

+1621
-389
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)