Skip to content

Commit

Permalink
feat: getter of kernel and NFS mount config
Browse files Browse the repository at this point in the history
Signed-off-by: Raphanus Lo <[email protected]>
  • Loading branch information
COLDTURNIP authored and derekbit committed Dec 6, 2024
1 parent 4d6c3a8 commit b60ef86
Show file tree
Hide file tree
Showing 7 changed files with 468 additions and 0 deletions.
97 changes: 97 additions & 0 deletions nfs/nfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package nfs

import (
"bufio"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/pkg/errors"

"github.com/longhorn/go-common-libs/types"
)

const (
nfsmountGlobalSection = "NFSMount_Global_Options"
nfsmountDefaultVersion = "Defaultvers"
)

// GetSystemDefaultNFSVersion reads the system default NFS version. This config can be overridden by nfsmount.conf under
// configDir. If configDir is empty, it will be /etc by default. If the nfsmount.conf is absent, or the default
// version is not overridden in the configuration file, a wrapped ErrNotConfigured error will be returned. It is possible
// for the caller to tell if the configuration is overridden by errors.Is(err, types.ErrNotConfigured).
func GetSystemDefaultNFSVersion(configDir string) (major, minor int, err error) {
if configDir == "" {
configDir = types.SysEtcDirectory
}

configMap, err := getSystemNFSMountConfigMap(configDir)
if os.IsNotExist(err) {
return 0, 0, fmt.Errorf("system default NFS version is not overridden under %q: %w", configDir, types.ErrNotConfigured)
} else if err != nil {
return 0, 0, err
}
if globalSection, ok := configMap[nfsmountGlobalSection]; ok {
if configured, ok := globalSection[nfsmountDefaultVersion]; ok {
majorStr, minorStr, _ := strings.Cut(configured, ".")
major, err := strconv.Atoi(majorStr)
if err != nil {
return 0, 0, errors.Wrapf(err, "invalid NFS major version %q", configured)
}

minor := 0
if minorStr != "" {
minor, err = strconv.Atoi(minorStr)
if err != nil {
return 0, 0, errors.Wrapf(err, "invalid NFS minor version %q", configured)
}
}
return major, minor, nil
}
}
return 0, 0, fmt.Errorf("system default NFS version is not overridden under %q: %w", configDir, types.ErrNotConfigured)
}

// getSystemNFSMountConfigMap reads the nfsmount.conf under configDir. The returned result is in
// map[section]map[key]value structure, and the global options is result["NFSMount_Global_Options"]. Refer to man page
// for more detail: man 5 nfsmount.conf
func getSystemNFSMountConfigMap(configDir string) (map[string]map[string]string, error) {
configFilePath := filepath.Join(configDir, types.NFSMountFileName)
configFile, err := os.Open(configFilePath)
if err != nil {
return nil, err
}
defer configFile.Close()

configMap := make(map[string]map[string]string)
scanner := bufio.NewScanner(configFile)
var section = ""
var sectionMap map[string]string

for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "[") {
section = strings.TrimSpace(strings.Trim(line, "[]"))
continue
}
if sectionMap = configMap[section]; sectionMap == nil {
sectionMap = make(map[string]string)
configMap[section] = sectionMap
}
if key, val, isParsable := strings.Cut(line, "="); isParsable {
sectionMap[strings.TrimSpace(key)] = strings.TrimSpace(val)
} else {
return nil, fmt.Errorf("invalid key-value pair: '%s'", line)
}
}

if err := scanner.Err(); err != nil {
return nil, err
}
return configMap, nil
}
124 changes: 124 additions & 0 deletions nfs/nfs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package nfs

import (
"errors"
"fmt"
"os"
"path/filepath"
"testing"

. "gopkg.in/check.v1"

"github.com/longhorn/go-common-libs/types"

"github.com/longhorn/go-common-libs/test"
)

func Test(t *testing.T) { TestingT(t) }

type TestSuite struct{}

var _ = Suite(&TestSuite{})

func (s *TestSuite) TestGetSystemDefaultNFSVersion(c *C) {
genNFSMountConf := func(dir string, nfsVer string) {
data := fmt.Sprintf("[ NFSMount_Global_Options ]\nDefaultvers=%s\n", nfsVer)
err := os.WriteFile(filepath.Join(dir, types.NFSMountFileName), []byte(data), 0644)
c.Assert(err, IsNil)
}

type testCase struct {
setup func(dir string)
expectedMajor int
expectedMinor int
expectedMissing bool
expectedError bool
}
testCases := map[string]testCase{
"GetSystemDefaultNFSVersion(...): system NFS mount config absent": {
setup: func(dir string) {
_, err := os.Stat(filepath.Join(dir, types.NFSMountFileName))
c.Assert(os.IsNotExist(err), Equals, true)
},
expectedMajor: 0,
expectedMinor: 0,
expectedMissing: true,
expectedError: true,
},
"GetSystemDefaultNFSVersion(...): system NFS mount config exist without default ver": {
setup: func(dir string) {
data := "[ NFSMount_Global_Options ]\notherkey=val\n"
err := os.WriteFile(filepath.Join(dir, types.NFSMountFileName), []byte(data), 0644)
c.Assert(err, IsNil)
},
expectedMajor: 0,
expectedMinor: 0,
expectedMissing: true,
expectedError: true,
},
"GetSystemDefaultNFSVersion(...): system NFS mount config set default ver to 3": {
setup: func(dir string) { genNFSMountConf(dir, "3") },
expectedMajor: 3,
expectedMinor: 0,
expectedMissing: false,
expectedError: false,
},
"GetSystemDefaultNFSVersion(...): system NFS mount config set default ver to 4": {
setup: func(dir string) { genNFSMountConf(dir, "4") },
expectedMajor: 4,
expectedMinor: 0,
expectedMissing: false,
expectedError: false,
},
"GetSystemDefaultNFSVersion(...): system NFS mount config set default ver to 4.0": {
setup: func(dir string) { genNFSMountConf(dir, "4.0") },
expectedMajor: 4,
expectedMinor: 0,
expectedMissing: false,
expectedError: false,
},
"GetSystemDefaultNFSVersion(...): system NFS mount config set default ver to 4.2": {
setup: func(dir string) { genNFSMountConf(dir, "4.2") },
expectedMajor: 4,
expectedMinor: 2,
expectedMissing: false,
expectedError: false,
},
"GetSystemDefaultNFSVersion(...): system NFS mount config default ver to invalid value": {
setup: func(dir string) { genNFSMountConf(dir, "???") },
expectedMajor: 0,
expectedMinor: 0,
expectedMissing: false,
expectedError: true,
},
"GetSystemDefaultNFSVersion(...): system NFS mount config default ver to empty value": {
setup: func(dir string) { genNFSMountConf(dir, "") },
expectedMajor: 0,
expectedMinor: 0,
expectedMissing: false,
expectedError: true,
},
}

configDir := c.MkDir()

for testName, testCase := range testCases {
func() {
c.Logf("testing utils.%v", testName)

defer os.Remove(filepath.Join(configDir, types.NFSMountFileName))
testCase.setup(configDir)

major, minor, err := GetSystemDefaultNFSVersion(configDir)
c.Assert(major, Equals, testCase.expectedMajor, Commentf(test.ErrResultFmt, testName))
c.Assert(minor, Equals, testCase.expectedMinor, Commentf(test.ErrResultFmt, testName))
if testCase.expectedMissing {
c.Assert(errors.Is(err, types.ErrNotConfigured), Equals, true, Commentf(test.ErrResultFmt, testName))
} else if testCase.expectedError {
c.Assert(err, NotNil, Commentf(test.ErrErrorFmt, testName))
} else {
c.Assert(err, IsNil, Commentf(test.ErrErrorFmt, testName))
}
}()
}
}
76 changes: 76 additions & 0 deletions sys/sys.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package sys

import (
"bufio"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
Expand All @@ -12,6 +15,8 @@ import (
"golang.org/x/sys/unix"

"github.com/longhorn/go-common-libs/types"

commonio "github.com/longhorn/go-common-libs/io"
)

// GetKernelRelease returns the kernel release string.
Expand Down Expand Up @@ -127,3 +132,74 @@ func getSystemBlockDeviceInfo(sysClassBlockDirectory string, readDirFn func(stri
}
return deviceInfo, nil
}

// GetBootKernelConfigMap reads the kernel config into a key-value map. It tries to read kernel config from
// ${bootDir}/config-${kernelVersion}, and comments are ignored. If the bootDir is empty, it points to /boot by default.
func GetBootKernelConfigMap(bootDir, kernelVersion string) (configMap map[string]string, err error) {
if kernelVersion == "" {
return nil, fmt.Errorf("kernelVersion cannot be empty")
}
if bootDir == "" {
bootDir = types.SysBootDirectory
}

defer func() {
err = errors.Wrapf(err, "failed to get kernel config map from %s", bootDir)
}()

configPath := filepath.Join(bootDir, "config-"+kernelVersion)
configContent, err := commonio.ReadFileContent(configPath)
if err != nil {
return nil, err
}
return parseKernelModuleConfigMap(strings.NewReader(configContent))
}

// GetProcKernelConfigMap reads the kernel config into a key-value map. It tries to read kernel config from
// procfs mounted at procDir from the view of processName in namespace. If the procDir is empty, it points to /proc by
// default.
func GetProcKernelConfigMap(procDir string) (configMap map[string]string, err error) {
if procDir == "" {
procDir = types.SysProcDirectory
}

defer func() {
err = errors.Wrapf(err, "failed to get kernel config map from %s", procDir)
}()

configPath := filepath.Join(procDir, types.SysKernelConfigGz)
configFile, err := os.Open(configPath)
if err != nil {
return nil, err
}
defer configFile.Close()
gzReader, err := gzip.NewReader(configFile)
if err != nil {
return nil, err
}
defer gzReader.Close()
return parseKernelModuleConfigMap(gzReader)
}

// parseKernelModuleConfigMap parses the kernel config into key-value map. All commented items will be ignored.
func parseKernelModuleConfigMap(contentReader io.Reader) (map[string]string, error) {
configMap := map[string]string{}

scanner := bufio.NewScanner(contentReader)
for scanner.Scan() {
config := scanner.Text()
if !strings.HasPrefix(config, "CONFIG_") {
continue
}
key, val, parsable := strings.Cut(config, "=")
if !parsable {
return nil, fmt.Errorf("failed to parse kernel config %s", config)
}
configMap[strings.TrimSpace(key)] = strings.TrimSpace(val)
}

if err := scanner.Err(); err != nil {
return nil, err
}
return configMap, nil
}
Loading

0 comments on commit b60ef86

Please sign in to comment.