Skip to content
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

Add tool to install modules in lcow and plumb through shim #1195

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ SRCROOT=$(dir $(abspath $(firstword $(MAKEFILE_LIST))))

# The link aliases for gcstools
GCS_TOOLS=\
generichook
generichook \
install-drivers

.PHONY: all always rootfs test

Expand Down
2 changes: 2 additions & 0 deletions cmd/gcstools/generichook.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build linux

package main

import (
Expand Down
96 changes: 96 additions & 0 deletions cmd/gcstools/installdrivers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// +build linux

package main

import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/Microsoft/hcsshim/internal/guest/storage/overlay"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

const (
lcowGlobalDriversFormat = "/run/drivers/%s"

moduleExtension = ".ko"
)

func install(ctx context.Context) error {
args := []string(os.Args[1:])

if len(args) == 0 {
return errors.New("no driver paths provided for install")
}

for _, driver := range args {
modules := []string{}

driverGUID, err := uuid.NewRandom()
Copy link
Contributor

@dcantah dcantah Oct 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this new dep only because go-winio/pkg/guid doesn't build on Linux? 😔 microsoft/go-winio#169 We should move this forward, I'll approve and we should get a new release of go-winio out with this, didn't realize this (or know about that pr) until now. We shouldn't have to bring in a new dep for this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok now that that's in, we'll just need to cut a release and revendor here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we come to a conclusion on cutting a new tag?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, but since this is an official google package I'd think the risk is probably minimal either way.

Copy link
Contributor

@dcantah dcantah Oct 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not worried about risk, more that we're introducing a new dep just to generate an ID when a project we maintain has the same functionality (if we cut a new tag) haha

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just do a follow up pr to remove the dep with a new tag of winio so we can move this forward

if err != nil {
return err
}

// create an overlay mount from the driver's UVM path so we can write to the
// mount path in the UVM despite having mounted in the driver originally as
// readonly
runDriverPath := fmt.Sprintf(lcowGlobalDriversFormat, driverGUID.String())
upperPath := filepath.Join(runDriverPath, "upper")
workPath := filepath.Join(runDriverPath, "work")
rootPath := filepath.Join(runDriverPath, "content")
if err := overlay.Mount(ctx, []string{driver}, upperPath, workPath, rootPath, false); err != nil {
return err
}

// find all module files, which end with ".ko" extension, and remove extension
// for use when calling `modprobe` below.
if walkErr := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return errors.Wrap(err, "failed to read directory while walking dir")
}
if !info.IsDir() && filepath.Ext(info.Name()) == moduleExtension {
moduleName := strings.TrimSuffix(info.Name(), moduleExtension)
modules = append(modules, moduleName)
}
return nil
}); walkErr != nil {
return walkErr
}

// create a new module dependency map database for the driver
depmodArgs := []string{"-b", rootPath}
cmd := exec.Command("depmod", depmodArgs...)
out, err := cmd.CombinedOutput()
if err != nil {
return errors.Wrapf(err, "failed to run depmod with args %v: %s", depmodArgs, out)
}

// run modprobe for every module name found
modprobeArgs := append([]string{"-d", rootPath, "-a"}, modules...)
cmd = exec.Command(
"modprobe",
modprobeArgs...,
)

out, err = cmd.CombinedOutput()
if err != nil {
return errors.Wrapf(err, "failed to run modporbe with args %v: %s", modprobeArgs, out)
}
}

return nil
}

func installDriversMain() {
ctx := context.Background()
logrus.SetOutput(os.Stderr)
if err := install(ctx); err != nil {
logrus.Fatalf("error in install drivers: %s", err)
}
}
5 changes: 4 additions & 1 deletion cmd/gcstools/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build linux

package main

import (
Expand All @@ -7,7 +9,8 @@ import (
)

var commands = map[string]func(){
"generichook": genericHookMain,
"generichook": genericHookMain,
"install-drivers": installDriversMain,
}

func main() {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.5.6
github.com/google/go-containerregistry v0.5.1
github.com/google/uuid v1.3.0
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3
github.com/mattn/go-shellwords v1.0.6
github.com/opencontainers/runc v1.0.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
Expand Down
32 changes: 0 additions & 32 deletions internal/devices/assigned_devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ package devices
import (
"context"
"fmt"
"io/ioutil"
"net"
"strings"

"github.com/Microsoft/hcsshim/internal/cmd"
"github.com/Microsoft/hcsshim/internal/log"
Expand Down Expand Up @@ -107,32 +104,3 @@ func createDeviceUtilChildrenCommand(deviceUtilPath string, vmBusInstanceID stri
args := []string{deviceUtilPath, "children", parentIDsFlag, "--property=location"}
return args
}

// readCsPipeOutput is a helper function that connects to a listener and reads
// the connection's comma separated output until done. resulting comma separated
// values are returned in the `result` param. The `errChan` param is used to
// propagate an errors to the calling function.
func readCsPipeOutput(l net.Listener, errChan chan<- error, result *[]string) {
defer close(errChan)
c, err := l.Accept()
if err != nil {
errChan <- errors.Wrapf(err, "failed to accept named pipe")
return
}
bytes, err := ioutil.ReadAll(c)
if err != nil {
errChan <- err
return
}

elementsAsString := strings.TrimSuffix(string(bytes), "\n")
elements := strings.Split(elementsAsString, ",")
*result = append(*result, elements...)

if len(*result) == 0 {
errChan <- errors.Wrapf(err, "failed to get any pipe output")
return
}

errChan <- nil
}
71 changes: 62 additions & 9 deletions internal/devices/drivers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,23 @@ import (
"context"
"fmt"

"github.com/Microsoft/hcsshim/internal/cmd"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/resources"
"github.com/Microsoft/hcsshim/internal/uvm"
"github.com/pkg/errors"
)

// InstallWindowsDriver mounts a specified kernel driver using vsmb, then installs it in the UVM.
// InstallKernelDriver mounts a specified kernel driver, then installs it in the UVM.
//
// `driver` is a directory path on the host that contains driver files for standard installation.
// For windows this means files for pnp installation (.inf, .cat, .sys, .cert files).
// For linux this means a vhd file that contains the drivers under /lib/modules/`uname -r` for use
// with depmod and modprobe.
//
// Returns a ResourceCloser for the added vsmb share. On failure, the vsmb share will be released,
// Returns a ResourceCloser for the added mount. On failure, the mounted share will be released,
// the returned ResourceCloser will be nil, and an error will be returned.
func InstallWindowsDriver(ctx context.Context, vm *uvm.UtilityVM, driver string) (closer resources.ResourceCloser, err error) {
func InstallKernelDriver(ctx context.Context, vm *uvm.UtilityVM, driver string) (closer resources.ResourceCloser, err error) {
defer func() {
if err != nil && closer != nil {
// best effort clean up allocated resource on failure
Expand All @@ -27,14 +32,62 @@ func InstallWindowsDriver(ctx context.Context, vm *uvm.UtilityVM, driver string)
closer = nil
}
}()
options := vm.DefaultVSMBOptions(true)
closer, err = vm.AddVSMB(ctx, driver, options)
if vm.OS() == "windows" {
options := vm.DefaultVSMBOptions(true)
msscotb marked this conversation as resolved.
Show resolved Hide resolved
closer, err = vm.AddVSMB(ctx, driver, options)
if err != nil {
return closer, fmt.Errorf("failed to add VSMB share to utility VM for path %+v: %s", driver, err)
}
uvmPath, err := vm.GetVSMBUvmPath(ctx, driver, true)
if err != nil {
return closer, err
}
return closer, execPnPInstallDriver(ctx, vm, uvmPath)
}
uvmPathForShare := fmt.Sprintf(uvm.LCOWGlobalMountPrefix, vm.UVMMountCounter())
scsiCloser, err := vm.AddSCSI(ctx, driver, uvmPathForShare, true, false, []string{}, uvm.VMAccessTypeIndividual)
if err != nil {
return closer, fmt.Errorf("failed to add VSMB share to utility VM for path %+v: %s", driver, err)
return closer, fmt.Errorf("failed to add SCSI disk to utility VM for path %+v: %s", driver, err)
}
uvmPath, err := vm.GetVSMBUvmPath(ctx, driver, true)
return scsiCloser, execModprobeInstallDriver(ctx, vm, uvmPathForShare)
}

func execModprobeInstallDriver(ctx context.Context, vm *uvm.UtilityVM, driverDir string) error {
p, l, err := cmd.CreateNamedPipeListener()
if err != nil {
return closer, err
return err
}
defer l.Close()

var pipeResults []string
errChan := make(chan error)

go readCsPipeOutput(l, errChan, &pipeResults)

args := []string{
"/bin/install-drivers",
driverDir,
}
req := &cmd.CmdProcessRequest{
Args: args,
Stderr: p,
}

exitCode, err := cmd.ExecInUvm(ctx, vm, req)
if err != nil && err != noExecOutputErr {
return errors.Wrapf(err, "failed to install driver %s in uvm with exit code %d", driverDir, exitCode)
}
return closer, execPnPInstallDriver(ctx, vm, uvmPath)

// wait to finish parsing stdout results
select {
case err := <-errChan:
if err != nil {
return err
}
case <-ctx.Done():
return ctx.Err()
}

log.G(ctx).WithField("added drivers", driverDir).Debug("installed drivers")
return nil
}
34 changes: 34 additions & 0 deletions internal/devices/pnp.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ package devices
import (
"context"
"fmt"
"io/ioutil"
"net"
"strings"

"github.com/Microsoft/hcsshim/internal/cmd"
"github.com/Microsoft/hcsshim/internal/log"
Expand All @@ -22,6 +25,8 @@ const (
is an expected race and can be ignored.`
)

var noExecOutputErr = errors.New("failed to get any pipe output")

// createPnPInstallDriverCommand creates a pnputil command to add and install drivers
// present in `driverUVMPath` and all subdirectories.
func createPnPInstallDriverCommand(driverUVMPath string) []string {
Expand Down Expand Up @@ -61,3 +66,32 @@ func execPnPInstallDriver(ctx context.Context, vm *uvm.UtilityVM, driverDir stri
log.G(ctx).WithField("added drivers", driverDir).Debug("installed drivers")
return nil
}

// readCsPipeOutput is a helper function that connects to a listener and reads
// the connection's comma separated output until done. resulting comma separated
// values are returned in the `result` param. The `errChan` param is used to
// propagate an errors to the calling function.
func readCsPipeOutput(l net.Listener, errChan chan<- error, result *[]string) {
defer close(errChan)
c, err := l.Accept()
if err != nil {
errChan <- errors.Wrapf(err, "failed to accept named pipe")
return
}
bytes, err := ioutil.ReadAll(c)
if err != nil {
errChan <- err
return
}

elementsAsString := strings.TrimSuffix(string(bytes), "\n")
elements := strings.Split(elementsAsString, ",")
*result = append(*result, elements...)

if len(*result) == 0 {
errChan <- noExecOutputErr
return
}

errChan <- nil
}
55 changes: 55 additions & 0 deletions internal/guest/runtime/hcsv2/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,58 @@ func applyAnnotationsToSpec(ctx context.Context, spec *oci.Spec) error {

return nil
}

// Helper function to create an oci prestart hook to run ldconfig
func addLDConfigHook(ctx context.Context, spec *oci.Spec, args, env []string) error {
if spec.Hooks == nil {
spec.Hooks = &oci.Hooks{}
}

ldConfigHook := oci.Hook{
Path: "/sbin/ldconfig",
Args: args,
Env: env,
}

spec.Hooks.Prestart = append(spec.Hooks.Prestart, ldConfigHook)
return nil
}

func addLinuxDeviceToSpec(ctx context.Context, hostDevice *devices.Device, spec *oci.Spec, addCgroupDevice bool) {
rd := oci.LinuxDevice{
Path: hostDevice.Path,
Type: string(hostDevice.Type),
Major: hostDevice.Major,
Minor: hostDevice.Minor,
UID: &hostDevice.Uid,
GID: &hostDevice.Gid,
}
if hostDevice.Major == 0 && hostDevice.Minor == 0 {
// Invalid device, most likely a symbolic link, skip it.
return
}
found := false
for i, dev := range spec.Linux.Devices {
if dev.Path == rd.Path {
found = true
spec.Linux.Devices[i] = rd
break
}
if dev.Type == rd.Type && dev.Major == rd.Major && dev.Minor == rd.Minor {
log.G(ctx).Warnf("The same type '%s', major '%d' and minor '%d', should not be used for multiple devices.", dev.Type, dev.Major, dev.Minor)
}
}
if !found {
spec.Linux.Devices = append(spec.Linux.Devices, rd)
if addCgroupDevice {
deviceCgroup := oci.LinuxDeviceCgroup{
Allow: true,
Type: string(hostDevice.Type),
Major: &hostDevice.Major,
Minor: &hostDevice.Minor,
Access: string(hostDevice.Permissions),
}
spec.Linux.Resources.Devices = append(spec.Linux.Resources.Devices, deviceCgroup)
}
}
}
Loading