-
Notifications
You must be signed in to change notification settings - Fork 200
Support DriveMount API in CreateVM. #297
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,12 +14,20 @@ | |
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io/ioutil" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
"time" | ||
|
||
"github.com/containerd/containerd/log" | ||
"github.com/containerd/containerd/mount" | ||
"github.com/firecracker-microvm/firecracker-containerd/internal" | ||
drivemount "github.com/firecracker-microvm/firecracker-containerd/proto/service/drivemount/ttrpc" | ||
"github.com/golang/protobuf/ptypes/empty" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
const ( | ||
|
@@ -28,6 +36,14 @@ const ( | |
blockMajorMinor = "dev" | ||
) | ||
|
||
var ( | ||
bannedSystemDirs = []string{ | ||
"/proc", | ||
"/sys", | ||
"/dev", | ||
} | ||
) | ||
|
||
type drive struct { | ||
Name string | ||
DriveID string | ||
|
@@ -45,6 +61,8 @@ type driveHandler struct { | |
DrivePath string | ||
} | ||
|
||
var _ drivemount.DriveMounterService = &driveHandler{} | ||
|
||
func newDriveHandler(blockPath, drivePath string) (*driveHandler, error) { | ||
d := &driveHandler{ | ||
drives: map[string]drive{}, | ||
|
@@ -146,3 +164,107 @@ func isStubDrive(d drive) bool { | |
|
||
return internal.IsStubDrive(f) | ||
} | ||
|
||
func (dh driveHandler) MountDrive(ctx context.Context, req *drivemount.MountDriveRequest) (*empty.Empty, error) { | ||
logger := log.G(ctx) | ||
logger.Debugf("%+v", req.String()) | ||
logger = logger.WithField("drive_id", req.DriveID) | ||
|
||
drive, ok := dh.GetDrive(req.DriveID) | ||
if !ok { | ||
return nil, fmt.Errorf("drive %q could not be found", req.DriveID) | ||
} | ||
logger = logger.WithField("drive_path", drive.Path()) | ||
|
||
// Do a basic check that we won't be mounting over any important system directories | ||
resolvedDest, err := evalAnySymlinks(req.DestinationPath) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, | ||
"failed to evaluate any symlinks in drive mount destination %q", req.DestinationPath) | ||
} | ||
|
||
for _, systemDir := range bannedSystemDirs { | ||
if isOrUnderDir(resolvedDest, systemDir) { | ||
return nil, errors.Errorf( | ||
"drive mount destination %q resolves to path %q under banned system directory %q", | ||
req.DestinationPath, resolvedDest, systemDir, | ||
) | ||
} | ||
} | ||
|
||
err = os.MkdirAll(req.DestinationPath, 0700) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "failed to create drive mount destination %q", req.DestinationPath) | ||
} | ||
|
||
// Retry the mount in the case of failure a fixed number of times. This works around a rare issue | ||
// where we get to this mount attempt before the guest OS has realized a drive was patched: | ||
// https://github.com/firecracker-microvm/firecracker-containerd/issues/214 | ||
const ( | ||
maxRetries = 100 | ||
retryDelay = 10 * time.Millisecond | ||
) | ||
|
||
for i := 0; i < maxRetries; i++ { | ||
err := mount.All([]mount.Mount{{ | ||
Source: drive.Path(), | ||
Type: req.FilesytemType, | ||
Options: req.Options, | ||
}}, req.DestinationPath) | ||
if err == nil { | ||
return &empty.Empty{}, nil | ||
} | ||
|
||
if isRetryableMountError(err) { | ||
logger.WithError(err).Warnf("retryable failure mounting drive") | ||
time.Sleep(retryDelay) | ||
continue | ||
} | ||
|
||
return nil, errors.Wrapf(err, "non-retryable failure mounting drive from %q to %q", | ||
drive.Path(), req.DestinationPath) | ||
} | ||
|
||
return nil, errors.Errorf("exhausted retries mounting drive from %q to %q", | ||
drive.Path(), req.DestinationPath) | ||
} | ||
|
||
// evalAnySymlinks is similar to filepath.EvalSymlinks, except it will not return an error if part of the | ||
// provided path does not exist. It will evaluate symlinks present in the path up to a component that doesn't | ||
// exist, at which point it will just append the rest of the provided path to what has been resolved so far. | ||
// We validate earlier that input to this function is an absolute path. | ||
func evalAnySymlinks(path string) (string, error) { | ||
curPath := "/" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a comment here saying that we validate that the curPath must be at There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, not only do we validate it, but we override it if it isn't. IMO this function should be given a name that more clearly indicates what it does. Converting a relative path to an absolute path doesn't have anything to do with symlinks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The point of this function is not to convert a relative path into an absolute path, it's to see if any part of the provided path already exists and, if so, evaluate any symlinks in the part of the path that exists. I needed to add it because the standard I.e. if you are given I updated the comment on the function to also state that it's assumed |
||
pathSplit := strings.Split(filepath.Clean(path), "/") | ||
for len(pathSplit) > 0 { | ||
curPath = filepath.Join(curPath, pathSplit[0]) | ||
pathSplit = pathSplit[1:] | ||
|
||
resolvedPath, err := filepath.EvalSymlinks(curPath) | ||
if os.IsNotExist(err) { | ||
return filepath.Join(append([]string{curPath}, pathSplit...)...), nil | ||
} | ||
if err != nil { | ||
return "", err | ||
} | ||
curPath = resolvedPath | ||
} | ||
|
||
return curPath, nil | ||
} | ||
|
||
// returns whether the given path is the provided baseDir or is under it | ||
func isOrUnderDir(path, baseDir string) bool { | ||
path = filepath.Clean(path) | ||
baseDir = filepath.Clean(baseDir) | ||
|
||
if baseDir == "/" { | ||
return true | ||
} | ||
|
||
if path == baseDir { | ||
return true | ||
} | ||
|
||
return strings.HasPrefix(path, baseDir+"/") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In which cases drive might not be mountable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can happen when we've patched a stub drive but the OS has not yet realized a change was made, which is rare but did happen in practice occasionally. We needed these retries in the previous implementation too, I just migrated the code as part of this change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think this is useful context and it worth to leave comments explaining the motivation behind retries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah good call, added a comment w/ link to original issue