Skip to content
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
2 changes: 2 additions & 0 deletions cmd/nvidia-cdi-hook/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
symlinks "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/create-symlinks"
"github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/cudacompat"
disabledevicenodemodification "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/disable-device-node-modification"
updateapplicationprofile "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/update-application-profile"
ldcache "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-cdi-hook/update-ldcache"
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
)
Expand Down Expand Up @@ -90,6 +91,7 @@ func ConfigureCDIHookCommand(logger logger.Interface, base *cli.Command) *cli.Co
chmod.NewCommand(logger),
cudacompat.NewCommand(logger),
disabledevicenodemodification.NewCommand(logger),
updateapplicationprofile.NewCommand(logger),
{
Name: "noop",
Usage: "The noop hook performs no actions and is only added to facilitate basic testing of the CLI",
Expand Down
88 changes: 88 additions & 0 deletions cmd/nvidia-cdi-hook/update-application-profile/minors_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
# SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
**/

package updateapplicationprofile

import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"syscall"

"golang.org/x/sys/unix"
)

// readDirBatchSize is the number of directory entries read from the
// container's /dev directory per ReadDir call.
const readDirBatchSize = 100
Comment thread
tariq1890 marked this conversation as resolved.

// listDeviceNodes enumerates the entries under the container's /dev directory.
// The char-device check uses the cheap readdir type; the minor number is
// resolved for char-device entries.
func listDeviceNodes(containerRoot *os.Root) ([]deviceNode, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Potentially for a follow-up -- There may be an opportunity for code reuse here. We are essentially globbing for files in a root. We do this as well in the cudacompat hook, see

func (r root) globFiles(pattern string) ([]string, error) {
dir := filepath.Dir(pattern)
basePattern := filepath.Base(pattern)
d, err := pathrs.OpenatInRoot(r.File, dir)
if err != nil {
return nil, err
}
defer d.Close()
dirInRoot, err := pathrs.Reopen(d, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC)
if err != nil {
return nil, err
}
defer dirInRoot.Close()
dEntries, err := dirInRoot.ReadDir(-1)
if err != nil {
return nil, fmt.Errorf("failed to get dentries from %q: %w", dir, err)
}
var files []string
dir = strings.TrimPrefix(dirInRoot.Name(), r.Name())
for _, dentry := range dEntries {
if match, err := filepath.Match(basePattern, dentry.Name()); err != nil || !match {
continue
}
if dentry.IsDir() {
continue
}
if !dentry.Type().IsRegular() {
continue
}
files = append(files, filepath.Join(dir, dentry.Name()))
}
sort.Strings(files)
return files, nil
}
. We may benefit from a internal/root package that can be reused by all of our hooks. It should provide the ability to perform file operations "safely" in a root fs.

I was going to suggest we reuse the CharDeviceLocator, from https://github.com/NVIDIA/nvidia-container-toolkit/blob/main/pkg/lookup/device.go, but this code operates on file paths and therefore is not suitable for a hook where we don't trust the root fs we are operating on.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good suggestion. Let me create another PR for this refactor work to limit its scope.

devDir, err := containerRoot.Open("dev")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, fmt.Errorf("failed to open /dev in container: %w", err)
}
defer devDir.Close()

var nodes []deviceNode
for {
entries, readErr := devDir.ReadDir(readDirBatchSize)
Comment thread
tariq1890 marked this conversation as resolved.
for _, entry := range entries {
node := deviceNode{
name: entry.Name(),
isCharDevice: entry.Type()&os.ModeCharDevice != 0,
}
if node.isCharDevice {
minor, err := deviceNodeMinor(containerRoot, entry.Name())
if err != nil {
return nil, err
}
node.minor = minor
}
nodes = append(nodes, node)
}
if readErr == io.EOF {
break
}
if readErr != nil {
return nil, fmt.Errorf("failed to read /dev in container: %w", readErr)
Comment thread
henry118 marked this conversation as resolved.
}
}

return nodes, nil
}

// deviceNodeMinor returns the device minor number for the named entry under the
// container's /dev directory.
func deviceNodeMinor(containerRoot *os.Root, name string) (int, error) {
info, err := containerRoot.Lstat(filepath.Join("dev", name))
if err != nil {
return 0, fmt.Errorf("failed to stat %s: %w", name, err)
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return 0, fmt.Errorf("unexpected stat type for %s", name)
}
return int(unix.Minor(stat.Rdev)), nil
}
27 changes: 27 additions & 0 deletions cmd/nvidia-cdi-hook/update-application-profile/minors_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//go:build !linux

/**
# SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
**/

package updateapplicationprofile

import "os"

// listDeviceNodes is not supported on non-Linux platforms.
func listDeviceNodes(_ *os.Root) ([]deviceNode, error) {
return nil, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
# SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
**/

package updateapplicationprofile

import (
"context"
"fmt"
"os"
"regexp"

"github.com/urfave/cli/v3"

"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
)

const (
applicationProfileDir = "etc/nvidia/nvidia-application-profiles-rc.d"
applicationProfileFile = applicationProfileDir + "/10-container.conf"
)

// nvidiaGPUDeviceNode matches the device node names for physical NVIDIA GPUs,
// e.g. nvidia0, nvidia12.
var nvidiaGPUDeviceNode = regexp.MustCompile(`^nvidia[0-9]+$`)

// A deviceNode describes a single entry discovered under the container's /dev
// directory. minor is only meaningful when isCharDevice is true.
type deviceNode struct {
name string
isCharDevice bool
minor int
}

// selectGPUMinors returns the minor numbers of the entries that are physical
// NVIDIA GPU device nodes (/dev/nvidiaN char devices).
func selectGPUMinors(nodes []deviceNode) []int {
var minors []int
for _, n := range nodes {
if n.isCharDevice && nvidiaGPUDeviceNode.MatchString(n.name) {
minors = append(minors, n.minor)
}
}
return minors
}

type options struct {
containerSpec string
}

// NewCommand constructs an update-application-profile subcommand with the specified logger
func NewCommand(logger logger.Interface) *cli.Command {
cfg := options{}

c := cli.Command{
Name: "update-application-profile",
Usage: `Updates driver settings through "application profiles". Currently, this hook sets EGLVisibleDGPUDevices to restrict EGL/Vulkan GPU visibility inside the container`,
Action: func(ctx context.Context, cmd *cli.Command) error {
return run(ctx, cmd, &cfg, logger)
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "container-spec",
Hidden: true,
Usage: "Specify the path to the OCI container spec. If empty or '-' the spec will be read from STDIN",
Destination: &cfg.containerSpec,
},
},
}

return &c
}

func run(_ context.Context, _ *cli.Command, cfg *options, logger logger.Interface) error {
s, err := oci.LoadContainerState(cfg.containerSpec)
if err != nil {
return fmt.Errorf("failed to load container state: %w", err)
}

containerRootDirPath, err := s.GetContainerRoot()
if err != nil {
return fmt.Errorf("failed to determine container root: %w", err)
}

containerRoot, err := os.OpenRoot(containerRootDirPath)
if err != nil {
return fmt.Errorf("failed to open root: %w", err)
}
defer containerRoot.Close()

nodes, err := listDeviceNodes(containerRoot)
if err != nil {
return fmt.Errorf("failed to enumerate device nodes in container: %w", err)
}

minors := selectGPUMinors(nodes)
if len(minors) == 0 {
return nil
}

var mask uint64
for _, minor := range minors {
if minor < 0 || minor >= 64 {
logger.Warningf("dropping invalid minor number %d: must be in range [0, 63]", minor)
continue
}
mask |= uint64(1) << uint(minor)
Comment thread
tariq1890 marked this conversation as resolved.
}

if mask == 0 {
return nil
}

if err := containerRoot.MkdirAll(applicationProfileDir, 0555); err != nil {
return fmt.Errorf("failed to create application profiles directory: %w", err)
}

if err := containerRoot.WriteFile(applicationProfileFile, buildApplicationProfileConfig(mask), 0444); err != nil {
return fmt.Errorf("failed to write application profile: %w", err)
}

return nil
}

// buildApplicationProfileConfig returns the contents of the NVIDIA driver application profile.
func buildApplicationProfileConfig(mask uint64) []byte {
return fmt.Appendf([]byte{},
`{"profiles":[{"name":"_container_","settings":["EGLVisibleDGPUDevices",0x%x]}],"rules":[{"pattern":[],"profile":"_container_"}]}`,
mask,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
# SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
**/

package updateapplicationprofile

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestSelectGPUMinors(t *testing.T) {
testCases := map[string]struct {
nodes []deviceNode
expected []int
}{
"single GPU": {
nodes: []deviceNode{{name: "nvidia0", isCharDevice: true, minor: 0}},
expected: []int{0},
},
"multiple GPUs, minor != filename order": {
nodes: []deviceNode{
{name: "nvidia0", isCharDevice: true, minor: 1},
{name: "nvidia1", isCharDevice: true, minor: 0},
},
expected: []int{1, 0},
},
"multi-digit minor": {
nodes: []deviceNode{{name: "nvidia10", isCharDevice: true, minor: 10}},
expected: []int{10},
},
"control and modeset nodes are excluded": {
nodes: []deviceNode{
{name: "nvidia0", isCharDevice: true, minor: 0},
{name: "nvidiactl", isCharDevice: true, minor: 255},
{name: "nvidia-modeset", isCharDevice: true, minor: 254},
{name: "nvidia-uvm", isCharDevice: true, minor: 511},
{name: "nvidia-uvm-tools", isCharDevice: true, minor: 510},
},
expected: []int{0},
},
"caps and imex nodes are excluded": {
nodes: []deviceNode{
{name: "nvidia-caps", isCharDevice: false, minor: 0},
{name: "nvidia-caps-imex-channels", isCharDevice: false, minor: 0},
{name: "nvidia1", isCharDevice: true, minor: 1},
},
expected: []int{1},
},
"non-char-device with matching name is excluded": {
nodes: []deviceNode{
{name: "nvidia0", isCharDevice: false, minor: 0},
},
expected: nil,
},
"unrelated device nodes are excluded": {
nodes: []deviceNode{
{name: "null", isCharDevice: true, minor: 3},
{name: "zero", isCharDevice: true, minor: 5},
},
expected: nil,
},
"no nodes": {
nodes: nil,
expected: nil,
},
}

for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
require.Equal(t, tc.expected, selectGPUMinors(tc.nodes))
})
}
}

func TestBuildApplicationProfileConfig(t *testing.T) {
testCases := map[string]struct {
mask uint64
expected string
}{
"single GPU, minor 0": {
mask: 1 << 0,
expected: `{"profiles":[{"name":"_container_","settings":["EGLVisibleDGPUDevices",0x1]}],"rules":[{"pattern":[],"profile":"_container_"}]}`,
},
"minor 8": {
mask: 1 << 8,
expected: `{"profiles":[{"name":"_container_","settings":["EGLVisibleDGPUDevices",0x100]}],"rules":[{"pattern":[],"profile":"_container_"}]}`,
},
"minors 0 and 15 combined": {
mask: (1 << 0) | (1 << 15),
expected: `{"profiles":[{"name":"_container_","settings":["EGLVisibleDGPUDevices",0x8001]}],"rules":[{"pattern":[],"profile":"_container_"}]}`,
},
"no GPUs": {
mask: 0,
expected: `{"profiles":[{"name":"_container_","settings":["EGLVisibleDGPUDevices",0x0]}],"rules":[{"pattern":[],"profile":"_container_"}]}`,
},
}

for description, tc := range testCases {
t.Run(description, func(t *testing.T) {
require.EqualValues(t, tc.expected, string(buildApplicationProfileConfig(tc.mask)))
})
}
}
Loading
Loading