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 committed Dec 5, 2024
1 parent 4d6c3a8 commit f2866ae
Show file tree
Hide file tree
Showing 6 changed files with 437 additions and 0 deletions.
90 changes: 90 additions & 0 deletions nfs/nfs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package nfs

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

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

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, an os.ErrNotExist error will be returned.
func GetSystemDefaultNFSVersion(configDir string) (major, minor int, err error) {
if configDir == "" {
configDir = types.SysEtcDirectory
}

Check warning on line 26 in nfs/nfs.go

View check run for this annotation

Codecov / codecov/patch

nfs/nfs.go#L25-L26

Added lines #L25 - L26 were not covered by tests

configMap, err := getSystemNFSMountConfigMap(configDir)
if os.IsNotExist(err) {
return 0, 0, err
} else if err != nil {
return 0, 0, errors.Wrap(err, "failed to read default NFS version from "+types.NFSMountFileName)
}

Check warning on line 33 in nfs/nfs.go

View check run for this annotation

Codecov / codecov/patch

nfs/nfs.go#L32-L33

Added lines #L32 - L33 were not covered by tests
if globalSection, ok := configMap[nfsmountGlobalSection]; ok {
if configured, ok := globalSection[nfsmountDefaultVersion]; ok {
majorStr, minorStr, _ := strings.Cut(configured, ".")
if major, err = strconv.Atoi(majorStr); err != nil {
return 0, 0, fmt.Errorf("invalid NFS Version %q", configured)
} else if minorStr == "" {
minor = 0
} else if minor, err = strconv.Atoi(minorStr); err != nil {
return 0, 0, fmt.Errorf("invalid NFS Version %q", configured)
}

Check warning on line 43 in nfs/nfs.go

View check run for this annotation

Codecov / codecov/patch

nfs/nfs.go#L42-L43

Added lines #L42 - L43 were not covered by tests
return major, minor, nil
}
}
return 0, 0, os.ErrNotExist
}

// 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

Check warning on line 69 in nfs/nfs.go

View check run for this annotation

Codecov / codecov/patch

nfs/nfs.go#L69

Added line #L69 was not covered by tests
}
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)
}

Check warning on line 83 in nfs/nfs.go

View check run for this annotation

Codecov / codecov/patch

nfs/nfs.go#L82-L83

Added lines #L82 - L83 were not covered by tests
}

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

Check warning on line 88 in nfs/nfs.go

View check run for this annotation

Codecov / codecov/patch

nfs/nfs.go#L87-L88

Added lines #L87 - L88 were not covered by tests
return configMap, nil
}
123 changes: 123 additions & 0 deletions nfs/nfs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package nfs

import (
"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(os.IsNotExist(err), 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")
}

Check warning on line 141 in sys/sys.go

View check run for this annotation

Codecov / codecov/patch

sys/sys.go#L140-L141

Added lines #L140 - L141 were not covered by tests
if bootDir == "" {
bootDir = types.SysBootDirectory
}

Check warning on line 144 in sys/sys.go

View check run for this annotation

Codecov / codecov/patch

sys/sys.go#L143-L144

Added lines #L143 - L144 were not covered by tests

defer func() {
err = errors.Wrap(err, "failed to get kernel config map")
}()

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

Check warning on line 154 in sys/sys.go

View check run for this annotation

Codecov / codecov/patch

sys/sys.go#L153-L154

Added lines #L153 - L154 were not covered by tests
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
}

Check warning on line 164 in sys/sys.go

View check run for this annotation

Codecov / codecov/patch

sys/sys.go#L163-L164

Added lines #L163 - L164 were not covered by tests

defer func() {
err = errors.Wrap(err, "failed to get kernel config map")
}()

configPath := filepath.Join(procDir, types.SysKernelConfigGz)
configFile, err := os.Open(configPath)
if err != nil {
return nil, err
}

Check warning on line 174 in sys/sys.go

View check run for this annotation

Codecov / codecov/patch

sys/sys.go#L173-L174

Added lines #L173 - L174 were not covered by tests
defer configFile.Close()
gzReader, err := gzip.NewReader(configFile)
if err != nil {
return nil, err
}

Check warning on line 179 in sys/sys.go

View check run for this annotation

Codecov / codecov/patch

sys/sys.go#L178-L179

Added lines #L178 - L179 were not covered by tests
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
}

Check warning on line 203 in sys/sys.go

View check run for this annotation

Codecov / codecov/patch

sys/sys.go#L202-L203

Added lines #L202 - L203 were not covered by tests
return configMap, nil
}
Loading

0 comments on commit f2866ae

Please sign in to comment.