Skip to content

Commit 51b4aa1

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 51b4aa1

20 files changed

+1452
-376
lines changed

agent/drive_handler.go

Lines changed: 102 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,87 @@ 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).WithField("MountDriveRequest", req.String())
170+
logger.Debug()
171+
172+
driveID := strings.TrimSpace(req.DriveID)
173+
drive, ok := dh.GetDrive(driveID)
174+
if !ok {
175+
return nil, fmt.Errorf("Drive %q could not be found", 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 strings.HasPrefix(filepath.Clean(resolvedDest), filepath.Clean(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+
const (
201+
maxRetries = 100
202+
retryDelay = 10 * time.Millisecond
203+
)
204+
205+
for i := 0; i < maxRetries; i++ {
206+
err := mount.All([]mount.Mount{{
207+
Source: drive.Path(),
208+
Type: req.FilesytemType,
209+
Options: req.Options,
210+
}}, req.DestinationPath)
211+
if err == nil {
212+
return &empty.Empty{}, nil
213+
}
214+
215+
if isRetryableMountError(err) {
216+
logger.WithError(err).Warnf("retryable failure mounting drive")
217+
time.Sleep(retryDelay)
218+
continue
219+
}
220+
221+
return nil, errors.Wrapf(err, "non-retryable failure mounting drive from %q to %q",
222+
drive.Path(), req.DestinationPath)
223+
}
224+
225+
return nil, errors.Errorf("exhausted retries mounting drive from %q to %q",
226+
drive.Path(), req.DestinationPath)
227+
}
228+
229+
// evalAnySymlinks is similar to filepath.EvalSymlinks, except it will not return an error if part of the
230+
// provided path does not exist. It will evaluate symlinks present in the path up to a component that doesn't
231+
// exist, at which point it will just append the rest of the provided path to what has been resolved so far.
232+
func evalAnySymlinks(path string) (string, error) {
233+
curPath := "/"
234+
pathSplit := strings.Split(filepath.Clean(path), "/")
235+
for len(pathSplit) > 0 {
236+
curPath = filepath.Join(curPath, pathSplit[0])
237+
pathSplit = pathSplit[1:]
238+
239+
resolvedPath, err := filepath.EvalSymlinks(curPath)
240+
if os.IsNotExist(err) {
241+
return filepath.Join(append([]string{curPath}, pathSplit...)...), nil
242+
}
243+
if err != nil {
244+
return "", err
245+
}
246+
curPath = resolvedPath
247+
}
248+
249+
return curPath, nil
250+
}

agent/drive_handler_test.go

Lines changed: 18 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,16 @@ 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+
}

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)