Repository: srl-labs/containerlab
Pull request #1415 (2023-05-31 15:15:19)

Host kernel version check for SRL
Check the host kernel version and compare it to the SRL required minimum version.
If the minimum requirement is not met, we log a warning. 


Patch:
diff --git a/nodes/srl/srl.go b/nodes/srl/srl.go
index 1cd18e12..867dbc08 100644
--- a/nodes/srl/srl.go
+++ b/nodes/srl/srl.go
@@ -109,6 +109,12 @@ var (
 	srlCfgTpl, _ = template.New("srl-tls-profile").
 			Funcs(gomplate.CreateFuncs(context.Background(), new(data.Data))).
 			Parse(srlConfigCmdsTpl)
+
+	requiredKernelVersion = &utils.KernelVersion{
+		Major:    4,
+		Minor:    10,
+		Revision: 0,
+	}
 )
 
 // Register registers the node in the NodeRegistry.
@@ -366,6 +372,31 @@ func (s *srl) Ready(ctx context.Context) error {
 	}
 }
 
+// checkKernelVersion emits a warning if the present kernel version is lower than the required one.
+func (s *srl) checkKernelVersion() error {
+	// retrieve running kernel version
+	kv, err := utils.GetKernelVersion()
+	if err != nil {
+		return err
+	}
+
+	// do the comparison
+	if !kv.GreaterOrEqual(requiredKernelVersion) {
+		log.Infof("Nokia SR Linux v23.3.1+ requires a kernel version greater than %s. Detected kernel version: %s", requiredKernelVersion, kv)
+	}
+	return nil
+}
+
+func (s *srl) CheckDeploymentConditions(ctx context.Context) error {
+	// perform the srl specific kernel version check
+	err := s.checkKernelVersion()
+	if err != nil {
+		return err
+	}
+
+	return s.DefaultNode.CheckDeploymentConditions(ctx)
+}
+
 func (s *srl) createSRLFiles() error {
 	log.Debugf("Creating directory structure for SRL container: %s", s.Cfg.ShortName)
 	var src string
diff --git a/utils/file.go b/utils/file.go
index ae7a3b16..daef9ade 100644
--- a/utils/file.go
+++ b/utils/file.go
@@ -178,7 +178,8 @@ func ExpandHome(p string) string {
 
 	userId, isSet := os.LookupEnv("SUDO_UID")
 	if !isSet {
-		return curUserHomeDir
+		p = strings.Replace(p, "~", curUserHomeDir, 1)
+		return p
 	}
 
 	// lookup user to figure out Home Directory
diff --git a/utils/kernel_module.go b/utils/kernel_module.go
index 83b023c7..5c36525e 100644
--- a/utils/kernel_module.go
+++ b/utils/kernel_module.go
@@ -2,8 +2,13 @@ package utils
 
 import (
 	"bufio"
+	"fmt"
 	"os"
+	"regexp"
+	"strconv"
 	"strings"
+
+	log "github.com/sirupsen/logrus"
 )
 
 // IsKernelModuleLoaded checks if a kernel module is loaded by parsing /proc/modules file.
@@ -21,3 +26,67 @@ func IsKernelModuleLoaded(name string) (bool, error) {
 	}
 	return false, f.Close()
 }
+
+const kernelOSReleasePath = "/proc/sys/kernel/osrelease"
+
+// GetKernelVersion returns the parsed OS kernel version.
+func GetKernelVersion() (*KernelVersion, error) {
+	ver, err := os.ReadFile(kernelOSReleasePath)
+	if err != nil {
+		return nil, err
+	}
+
+	log.Debugf("kernel version: %s", string(ver))
+
+	return parseKernelVersion(ver)
+}
+
+// KernelVersion holds the parsed OS kernel version.
+type KernelVersion struct {
+	Major     int
+	Minor     int
+	Revision  int
+	Remainder string // the rest of the version string, e.g. "-amd64"
+}
+
+func parseKernelVersion(v []byte) (*KernelVersion, error) {
+	// https: //regex101.com/r/cWqad0/1
+	re := regexp.MustCompile(`(?P<Major>\d+)\.(?P<Minor>\d+)\.(?P<Revision>\d+)(?P<Remainder>.*)`)
+
+	matches := re.FindSubmatch(v)
+
+	if len(matches) > 0 {
+		kv := &KernelVersion{}
+
+		kv.Major, _ = strconv.Atoi(string(matches[re.SubexpIndex("Major")]))
+		kv.Minor, _ = strconv.Atoi(string(matches[re.SubexpIndex("Minor")]))
+		kv.Revision, _ = strconv.Atoi(string(matches[re.SubexpIndex("Revision")]))
+		kv.Remainder = string(matches[re.SubexpIndex("Remainder")])
+
+		return kv, nil
+	}
+
+	return nil, fmt.Errorf("failed to parse kernel version")
+}
+
+// String returns the Kernel version as string.
+func (kv *KernelVersion) String() string {
+	return fmt.Sprintf("%d.%d.%d", kv.Major, kv.Minor, kv.Revision)
+}
+
+// GreaterOrEqual returns true if the Kernel version is greater or equal to the compared Kernel version.
+func (kv *KernelVersion) GreaterOrEqual(cmpKv *KernelVersion) bool {
+	if kv.Major < cmpKv.Major {
+		return false
+	}
+	if kv.Minor < cmpKv.Minor {
+		return false
+	}
+	// this must be >= because we're implementing GreaterEqual
+	// and this is the last position
+	if kv.Revision < cmpKv.Revision {
+		return false
+	}
+
+	return true
+}


Test patch:
diff --git a/utils/kernel_module_test.go b/utils/kernel_module_test.go
index d2409099..a0584efe 100644
--- a/utils/kernel_module_test.go
+++ b/utils/kernel_module_test.go
@@ -1,6 +1,10 @@
 package utils
 
-import "testing"
+import (
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
 
 func TestIsKernelModuleLoaded(t *testing.T) {
 	type args struct {
@@ -42,3 +46,52 @@ func TestIsKernelModuleLoaded(t *testing.T) {
 		})
 	}
 }
+
+func TestParseKernelVersion(t *testing.T) {
+	tests := []struct {
+		input     []byte
+		expectErr bool
+		expected  *KernelVersion
+	}{
+		{[]byte("123.45.6789 example"), false, &KernelVersion{Major: 123, Minor: 45, Revision: 6789, Remainder: " example"}},
+		{[]byte("1.2.3-abc"), false, &KernelVersion{Major: 1, Minor: 2, Revision: 3, Remainder: "-abc"}},
+		{[]byte("invalid"), true, nil},
+		{[]byte(""), true, nil},
+	}
+
+	for _, test := range tests {
+		result, err := parseKernelVersion(test.input)
+
+		if (err != nil) != test.expectErr {
+			t.Errorf("Error expectation mismatch. Input: %s, Expected Error: %v, Actual Error: %v", test.input, test.expectErr, err)
+		}
+
+		if d := cmp.Diff(result, test.expected); d != "" {
+			t.Errorf("parseKernelVersion got = %+v, want %+v; Diff: %s", result, test.expected, d)
+		}
+	}
+}
+
+func TestKernelVersionGreaterOrEqual(t *testing.T) {
+	tests := []struct {
+		version      *KernelVersion
+		compare      *KernelVersion
+		expectResult bool
+	}{
+		{&KernelVersion{Major: 1, Minor: 2, Revision: 3}, &KernelVersion{Major: 1, Minor: 2, Revision: 3}, true},
+		{&KernelVersion{Major: 1, Minor: 2, Revision: 3}, &KernelVersion{Major: 1, Minor: 2, Revision: 2}, true},
+		{&KernelVersion{Major: 1, Minor: 2, Revision: 3}, &KernelVersion{Major: 1, Minor: 3, Revision: 3}, false},
+		{&KernelVersion{Major: 2, Minor: 3, Revision: 4}, &KernelVersion{Major: 1, Minor: 2, Revision: 3}, true},
+		{&KernelVersion{Major: 2, Minor: 3, Revision: 4}, &KernelVersion{Major: 3, Minor: 4, Revision: 5}, false},
+		{&KernelVersion{Major: 2, Minor: 3, Revision: 4}, &KernelVersion{Major: 2, Minor: 3, Revision: 5}, false},
+	}
+
+	for _, test := range tests {
+		result := test.version.GreaterOrEqual(test.compare)
+
+		if result != test.expectResult {
+			t.Errorf("Result mismatch. Version: %+v, Compare: %+v, Expected: %v, Actual: %v",
+				test.version, test.compare, test.expectResult, result)
+		}
+	}
+}