From 791d093c62f8cdd48e1e18502371a800491112ed Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Tue, 26 Mar 2024 14:55:37 +0200 Subject: [PATCH] Refactor info API This change adds a PropertyExtractor interface to encapsulate functions that query a system for certain capabilities. The IsTegraSystem has been renamed to HasTegraFiles function and marked as Deprecated. Signed-off-by: Evan Lezar --- pkg/nvlib/info/api.go | 34 ++++ pkg/nvlib/info/builder.go | 35 ++++ pkg/nvlib/info/options.go | 20 +- .../info/{info.go => property-extractor.go} | 50 ++--- pkg/nvlib/info/property-extractor_mock.go | 178 ++++++++++++++++++ pkg/nvlib/info/root.go | 86 +++++++++ 6 files changed, 353 insertions(+), 50 deletions(-) create mode 100644 pkg/nvlib/info/api.go create mode 100644 pkg/nvlib/info/builder.go rename pkg/nvlib/info/{info.go => property-extractor.go} (64%) create mode 100644 pkg/nvlib/info/property-extractor_mock.go create mode 100644 pkg/nvlib/info/root.go diff --git a/pkg/nvlib/info/api.go b/pkg/nvlib/info/api.go new file mode 100644 index 0000000..41b8e22 --- /dev/null +++ b/pkg/nvlib/info/api.go @@ -0,0 +1,34 @@ +/** +# Copyright 2024 NVIDIA CORPORATION +# +# 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 info + +// Interface provides the API to the info package. +type Interface interface { + PropertyExtractor +} + +// PropertyExtractor provides a set of functions to query capabilities of the +// system. +// +//go:generate moq -rm -out property-extractor_mock.go . PropertyExtractor +type PropertyExtractor interface { + HasDXCore() (bool, string) + HasNvml() (bool, string) + HasTegraFiles() (bool, string) + // Deprecated: Use HasTegraFiles instead. + IsTegraSystem() (bool, string) +} diff --git a/pkg/nvlib/info/builder.go b/pkg/nvlib/info/builder.go new file mode 100644 index 0000000..d9275ca --- /dev/null +++ b/pkg/nvlib/info/builder.go @@ -0,0 +1,35 @@ +/** +# Copyright 2024 NVIDIA CORPORATION +# +# 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 info + +type options struct { + root root +} + +// New creates a new instance of the 'info' Interface. +func New(opts ...Option) Interface { + o := &options{} + for _, opt := range opts { + opt(o) + } + if o.root == "" { + o.root = "/" + } + return &propertyExtractor{ + root: o.root, + } +} diff --git a/pkg/nvlib/info/options.go b/pkg/nvlib/info/options.go index ce72150..c4265d1 100644 --- a/pkg/nvlib/info/options.go +++ b/pkg/nvlib/info/options.go @@ -17,23 +17,11 @@ package info // Option defines a function for passing options to the New() call. -type Option func(*infolib) - -// New creates a new instance of the 'info' interface. -func New(opts ...Option) Interface { - i := &infolib{} - for _, opt := range opts { - opt(i) - } - if i.root == "" { - i.root = "/" - } - return i -} +type Option func(*options) // WithRoot provides a Option to set the root of the 'info' interface. -func WithRoot(root string) Option { - return func(i *infolib) { - i.root = root +func WithRoot(r string) Option { + return func(i *options) { + i.root = root(r) } } diff --git a/pkg/nvlib/info/info.go b/pkg/nvlib/info/property-extractor.go similarity index 64% rename from pkg/nvlib/info/info.go rename to pkg/nvlib/info/property-extractor.go index 677270c..9e41a54 100644 --- a/pkg/nvlib/info/info.go +++ b/pkg/nvlib/info/property-extractor.go @@ -19,31 +19,21 @@ package info import ( "fmt" "os" - "path/filepath" "strings" - - "github.com/NVIDIA/go-nvml/pkg/dl" ) -// Interface provides the API to the info package. -type Interface interface { - HasDXCore() (bool, string) - HasNvml() (bool, string) - IsTegraSystem() (bool, string) +type propertyExtractor struct { + root root } -type infolib struct { - root string -} - -var _ Interface = &infolib{} +var _ Interface = &propertyExtractor{} // HasDXCore returns true if DXCore is detected on the system. -func (i *infolib) HasDXCore() (bool, string) { +func (i *propertyExtractor) HasDXCore() (bool, string) { const ( libraryName = "libdxcore.so" ) - if err := assertHasLibrary(libraryName); err != nil { + if err := i.root.assertHasLibrary(libraryName); err != nil { return false, fmt.Sprintf("could not load DXCore library: %v", err) } @@ -51,11 +41,11 @@ func (i *infolib) HasDXCore() (bool, string) { } // HasNvml returns true if NVML is detected on the system. -func (i *infolib) HasNvml() (bool, string) { +func (i *propertyExtractor) HasNvml() (bool, string) { const ( libraryName = "libnvidia-ml.so.1" ) - if err := assertHasLibrary(libraryName); err != nil { + if err := i.root.assertHasLibrary(libraryName); err != nil { return false, fmt.Sprintf("could not load NVML library: %v", err) } @@ -63,9 +53,15 @@ func (i *infolib) HasNvml() (bool, string) { } // IsTegraSystem returns true if the system is detected as a Tegra-based system. -func (i *infolib) IsTegraSystem() (bool, string) { - tegraReleaseFile := filepath.Join(i.root, "/etc/nv_tegra_release") - tegraFamilyFile := filepath.Join(i.root, "/sys/devices/soc0/family") +// Deprecated: Use HasTegraFiles instead. +func (i *propertyExtractor) IsTegraSystem() (bool, string) { + return i.HasTegraFiles() +} + +// HasTegraFiles returns true if tegra-based files are detected on the system. +func (i *propertyExtractor) HasTegraFiles() (bool, string) { + tegraReleaseFile := i.root.join("/etc/nv_tegra_release") + tegraFamilyFile := i.root.join("/sys/devices/soc0/family") if info, err := os.Stat(tegraReleaseFile); err == nil && !info.IsDir() { return true, fmt.Sprintf("%v found", tegraReleaseFile) @@ -86,17 +82,3 @@ func (i *infolib) IsTegraSystem() (bool, string) { return false, fmt.Sprintf("%v has no 'tegra' prefix", tegraFamilyFile) } - -// assertHasLibrary returns an error if the specified library cannot be loaded. -func assertHasLibrary(libraryName string) error { - const ( - libraryLoadFlags = dl.RTLD_LAZY - ) - lib := dl.New(libraryName, libraryLoadFlags) - if err := lib.Open(); err != nil { - return err - } - defer lib.Close() - - return nil -} diff --git a/pkg/nvlib/info/property-extractor_mock.go b/pkg/nvlib/info/property-extractor_mock.go new file mode 100644 index 0000000..570dfe3 --- /dev/null +++ b/pkg/nvlib/info/property-extractor_mock.go @@ -0,0 +1,178 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package info + +import ( + "sync" +) + +// Ensure, that PropertyExtractorMock does implement PropertyExtractor. +// If this is not the case, regenerate this file with moq. +var _ PropertyExtractor = &PropertyExtractorMock{} + +// PropertyExtractorMock is a mock implementation of PropertyExtractor. +// +// func TestSomethingThatUsesPropertyExtractor(t *testing.T) { +// +// // make and configure a mocked PropertyExtractor +// mockedPropertyExtractor := &PropertyExtractorMock{ +// HasDXCoreFunc: func() (bool, string) { +// panic("mock out the HasDXCore method") +// }, +// HasNvmlFunc: func() (bool, string) { +// panic("mock out the HasNvml method") +// }, +// HasTegraFilesFunc: func() (bool, string) { +// panic("mock out the HasTegraFiles method") +// }, +// IsTegraSystemFunc: func() (bool, string) { +// panic("mock out the IsTegraSystem method") +// }, +// } +// +// // use mockedPropertyExtractor in code that requires PropertyExtractor +// // and then make assertions. +// +// } +type PropertyExtractorMock struct { + // HasDXCoreFunc mocks the HasDXCore method. + HasDXCoreFunc func() (bool, string) + + // HasNvmlFunc mocks the HasNvml method. + HasNvmlFunc func() (bool, string) + + // HasTegraFilesFunc mocks the HasTegraFiles method. + HasTegraFilesFunc func() (bool, string) + + // IsTegraSystemFunc mocks the IsTegraSystem method. + IsTegraSystemFunc func() (bool, string) + + // calls tracks calls to the methods. + calls struct { + // HasDXCore holds details about calls to the HasDXCore method. + HasDXCore []struct { + } + // HasNvml holds details about calls to the HasNvml method. + HasNvml []struct { + } + // HasTegraFiles holds details about calls to the HasTegraFiles method. + HasTegraFiles []struct { + } + // IsTegraSystem holds details about calls to the IsTegraSystem method. + IsTegraSystem []struct { + } + } + lockHasDXCore sync.RWMutex + lockHasNvml sync.RWMutex + lockHasTegraFiles sync.RWMutex + lockIsTegraSystem sync.RWMutex +} + +// HasDXCore calls HasDXCoreFunc. +func (mock *PropertyExtractorMock) HasDXCore() (bool, string) { + if mock.HasDXCoreFunc == nil { + panic("PropertyExtractorMock.HasDXCoreFunc: method is nil but PropertyExtractor.HasDXCore was just called") + } + callInfo := struct { + }{} + mock.lockHasDXCore.Lock() + mock.calls.HasDXCore = append(mock.calls.HasDXCore, callInfo) + mock.lockHasDXCore.Unlock() + return mock.HasDXCoreFunc() +} + +// HasDXCoreCalls gets all the calls that were made to HasDXCore. +// Check the length with: +// +// len(mockedPropertyExtractor.HasDXCoreCalls()) +func (mock *PropertyExtractorMock) HasDXCoreCalls() []struct { +} { + var calls []struct { + } + mock.lockHasDXCore.RLock() + calls = mock.calls.HasDXCore + mock.lockHasDXCore.RUnlock() + return calls +} + +// HasNvml calls HasNvmlFunc. +func (mock *PropertyExtractorMock) HasNvml() (bool, string) { + if mock.HasNvmlFunc == nil { + panic("PropertyExtractorMock.HasNvmlFunc: method is nil but PropertyExtractor.HasNvml was just called") + } + callInfo := struct { + }{} + mock.lockHasNvml.Lock() + mock.calls.HasNvml = append(mock.calls.HasNvml, callInfo) + mock.lockHasNvml.Unlock() + return mock.HasNvmlFunc() +} + +// HasNvmlCalls gets all the calls that were made to HasNvml. +// Check the length with: +// +// len(mockedPropertyExtractor.HasNvmlCalls()) +func (mock *PropertyExtractorMock) HasNvmlCalls() []struct { +} { + var calls []struct { + } + mock.lockHasNvml.RLock() + calls = mock.calls.HasNvml + mock.lockHasNvml.RUnlock() + return calls +} + +// HasTegraFiles calls HasTegraFilesFunc. +func (mock *PropertyExtractorMock) HasTegraFiles() (bool, string) { + if mock.HasTegraFilesFunc == nil { + panic("PropertyExtractorMock.HasTegraFilesFunc: method is nil but PropertyExtractor.HasTegraFiles was just called") + } + callInfo := struct { + }{} + mock.lockHasTegraFiles.Lock() + mock.calls.HasTegraFiles = append(mock.calls.HasTegraFiles, callInfo) + mock.lockHasTegraFiles.Unlock() + return mock.HasTegraFilesFunc() +} + +// HasTegraFilesCalls gets all the calls that were made to HasTegraFiles. +// Check the length with: +// +// len(mockedPropertyExtractor.HasTegraFilesCalls()) +func (mock *PropertyExtractorMock) HasTegraFilesCalls() []struct { +} { + var calls []struct { + } + mock.lockHasTegraFiles.RLock() + calls = mock.calls.HasTegraFiles + mock.lockHasTegraFiles.RUnlock() + return calls +} + +// IsTegraSystem calls IsTegraSystemFunc. +func (mock *PropertyExtractorMock) IsTegraSystem() (bool, string) { + if mock.IsTegraSystemFunc == nil { + panic("PropertyExtractorMock.IsTegraSystemFunc: method is nil but PropertyExtractor.IsTegraSystem was just called") + } + callInfo := struct { + }{} + mock.lockIsTegraSystem.Lock() + mock.calls.IsTegraSystem = append(mock.calls.IsTegraSystem, callInfo) + mock.lockIsTegraSystem.Unlock() + return mock.IsTegraSystemFunc() +} + +// IsTegraSystemCalls gets all the calls that were made to IsTegraSystem. +// Check the length with: +// +// len(mockedPropertyExtractor.IsTegraSystemCalls()) +func (mock *PropertyExtractorMock) IsTegraSystemCalls() []struct { +} { + var calls []struct { + } + mock.lockIsTegraSystem.RLock() + calls = mock.calls.IsTegraSystem + mock.lockIsTegraSystem.RUnlock() + return calls +} diff --git a/pkg/nvlib/info/root.go b/pkg/nvlib/info/root.go new file mode 100644 index 0000000..d38dc73 --- /dev/null +++ b/pkg/nvlib/info/root.go @@ -0,0 +1,86 @@ +/** +# Copyright 2024 NVIDIA CORPORATION +# +# 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 info + +import ( + "fmt" + "path/filepath" + + "github.com/NVIDIA/go-nvml/pkg/dl" +) + +// root represents a directory on the filesystem relative to which libraries +// such as the NVIDIA driver libraries can be found. +type root string + +func (r root) join(parts ...string) string { + return filepath.Join(append([]string{string(r)}, parts...)...) +} + +// assertHasLibrary returns an error if the specified library cannot be loaded. +func (r root) assertHasLibrary(libraryName string) error { + const ( + libraryLoadFlags = dl.RTLD_LAZY + ) + lib := dl.New(r.tryResolveLibrary(libraryName), libraryLoadFlags) + if err := lib.Open(); err != nil { + return err + } + defer lib.Close() + + return nil +} + +// tryResolveLibrary attempts to locate the specified library in the root. +// If the root is not specified, is "/", or the library cannot be found in the +// set of predefined paths, the input is returned as is. +func (r root) tryResolveLibrary(libraryName string) string { + if r == "" || r == "/" { + return libraryName + } + + librarySearchPaths := []string{ + "/usr/lib64", + "/usr/lib/x86_64-linux-gnu", + "/usr/lib/aarch64-linux-gnu", + "/lib64", + "/lib/x86_64-linux-gnu", + "/lib/aarch64-linux-gnu", + } + + for _, d := range librarySearchPaths { + l := r.join(d, libraryName) + resolved, err := resolveLink(l) + if err != nil { + continue + } + return resolved + } + + return libraryName +} + +// resolveLink finds the target of a symlink or the file itself in the +// case of a regular file. +// This is equivalent to running `readlink -f ${l}`. +func resolveLink(l string) (string, error) { + resolved, err := filepath.EvalSymlinks(l) + if err != nil { + return "", fmt.Errorf("error resolving link '%v': %w", l, err) + } + return resolved, nil +}