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 #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)