diff --git a/cmd/nvidia-ctk/hook/chmod/chmod.go b/cmd/nvidia-ctk/hook/chmod/chmod.go new file mode 100644 index 00000000..cad6edfa --- /dev/null +++ b/cmd/nvidia-ctk/hook/chmod/chmod.go @@ -0,0 +1,140 @@ +/** +# Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved. +# +# 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 chmod + +import ( + "fmt" + "path/filepath" + "strings" + "syscall" + + "github.com/NVIDIA/nvidia-container-toolkit/internal/lookup" + "github.com/NVIDIA/nvidia-container-toolkit/internal/oci" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +type command struct { + logger *logrus.Logger +} + +type config struct { + paths cli.StringSlice + mode string + containerSpec string +} + +// NewCommand constructs a chmod command with the specified logger +func NewCommand(logger *logrus.Logger) *cli.Command { + c := command{ + logger: logger, + } + return c.build() +} + +// build the chmod command +func (m command) build() *cli.Command { + cfg := config{} + + // Create the 'chmod' command + c := cli.Command{ + Name: "chmod", + Usage: "Set the permissions of folders in the container by running chmod. The container root is prefixed to the specified paths.", + Before: func(c *cli.Context) error { + return validateFlags(c, &cfg) + }, + Action: func(c *cli.Context) error { + return m.run(c, &cfg) + }, + } + + c.Flags = []cli.Flag{ + &cli.StringSliceFlag{ + Name: "path", + Usage: "Specifiy a path to apply the specified mode to", + Destination: &cfg.paths, + }, + &cli.StringFlag{ + Name: "mode", + Usage: "Specify the file mode", + Destination: &cfg.mode, + }, + &cli.StringFlag{ + Name: "container-spec", + 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 validateFlags(c *cli.Context, cfg *config) error { + if strings.TrimSpace(cfg.mode) == "" { + return fmt.Errorf("a non-empty mode must be specified") + } + + for _, p := range cfg.paths.Value() { + if strings.TrimSpace(p) == "" { + return fmt.Errorf("paths must not be empty") + } + } + + return nil +} + +func (m command) run(c *cli.Context, cfg *config) error { + s, err := oci.LoadContainerState(cfg.containerSpec) + if err != nil { + return fmt.Errorf("failed to load container state: %v", err) + } + + containerRoot, err := s.GetContainerRoot() + if err != nil { + return fmt.Errorf("failed to determined container root: %v", err) + } + if containerRoot == "" { + return fmt.Errorf("empty container root detected") + } + + paths := m.getPaths(containerRoot, cfg.paths.Value()) + if len(paths) == 0 { + m.logger.Debugf("No paths specified; exiting") + return nil + } + + locator := lookup.NewExecutableLocator(m.logger, "") + targets, err := locator.Locate("chmod") + if err != nil { + return fmt.Errorf("failed to locate chmod: %v", err) + } + chmodPath := targets[0] + + args := append([]string{filepath.Base(chmodPath), cfg.mode}, paths...) + + return syscall.Exec(chmodPath, args, nil) +} + +// getPaths updates the specified paths relative to the root. +func (m command) getPaths(root string, paths []string) []string { + var pathsInRoot []string + for _, f := range paths { + pathsInRoot = append(pathsInRoot, filepath.Join(root, f)) + } + + return pathsInRoot +} diff --git a/cmd/nvidia-ctk/hook/hook.go b/cmd/nvidia-ctk/hook/hook.go index feac8f5d..7deed271 100644 --- a/cmd/nvidia-ctk/hook/hook.go +++ b/cmd/nvidia-ctk/hook/hook.go @@ -17,6 +17,7 @@ package hook import ( + chmod "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-ctk/hook/chmod" symlinks "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-ctk/hook/create-symlinks" ldcache "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-ctk/hook/update-ldcache" "github.com/sirupsen/logrus" @@ -46,6 +47,7 @@ func (m hookCommand) build() *cli.Command { hook.Subcommands = []*cli.Command{ ldcache.NewCommand(m.logger), symlinks.NewCommand(m.logger), + chmod.NewCommand(m.logger), } return &hook diff --git a/cmd/nvidia-ctk/info/generate-cdi/generate-cdi.go b/cmd/nvidia-ctk/info/generate-cdi/generate-cdi.go index 7ef290b4..fb262a85 100644 --- a/cmd/nvidia-ctk/info/generate-cdi/generate-cdi.go +++ b/cmd/nvidia-ctk/info/generate-cdi/generate-cdi.go @@ -26,6 +26,7 @@ import ( "github.com/NVIDIA/nvidia-container-toolkit/internal/discover" "github.com/NVIDIA/nvidia-container-toolkit/internal/ldcache" "github.com/NVIDIA/nvidia-container-toolkit/internal/lookup" + "github.com/container-orchestrated-devices/container-device-interface/pkg/cdi" specs "github.com/container-orchestrated-devices/container-device-interface/specs-go" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" @@ -212,7 +213,12 @@ func (m command) generateSpec() (*specs.Spec, error) { ldcacheUpdateHook := m.generateUpdateLdCacheHook(libraries) - spec.ContainerEdits.Hooks = []*specs.Hook{ldcacheUpdateHook} + deviceFolderPermissionHooks, err := m.generateDeviceFolderPermissionHooks(ldcacheUpdateHook.Path, allDeviceNodes) + if err != nil { + return nil, fmt.Errorf("failed to generated permission hooks for device nodes: %v", err) + } + + spec.ContainerEdits.Hooks = append([]*specs.Hook{ldcacheUpdateHook}, deviceFolderPermissionHooks...) return &spec, nil } @@ -384,3 +390,49 @@ func (m command) generateUpdateLdCacheHook(libraries []string) *specs.Hook { Args: hook.Args, } } + +func (m command) generateDeviceFolderPermissionHooks(nvidiaCTKPath string, deviceNodes []*specs.DeviceNode) ([]*specs.Hook, error) { + var deviceFolders []string + seen := make(map[string]bool) + + for _, dn := range deviceNodes { + if !strings.HasPrefix(dn.Path, "/dev") { + m.logger.Warningf("Skipping unexpected device folder path for device %v", dn.Path) + continue + } + for df := filepath.Dir(dn.Path); df != "/dev"; df = filepath.Dir(df) { + if seen[df] { + continue + } + deviceFolders = append(deviceFolders, df) + seen[df] = true + } + } + + foldersByMode := make(map[string][]string) + for _, p := range deviceFolders { + info, err := os.Stat(p) + if err != nil { + return nil, fmt.Errorf("failed to get info for path %v: %v", p, err) + } + mode := fmt.Sprintf("%o", info.Mode().Perm()) + foldersByMode[mode] = append(foldersByMode[mode], p) + } + + var hooks []*specs.Hook + for mode, folders := range foldersByMode { + args := []string{filepath.Base(nvidiaCTKPath), "hook", "chmod", "--mode", mode} + for _, folder := range folders { + args = append(args, "--path", folder) + } + hook := specs.Hook{ + HookName: cdi.CreateContainerHook, + Path: nvidiaCTKPath, + Args: args, + } + + hooks = append(hooks, &hook) + } + + return hooks, nil +}