From 9bd6cbf5ca8281b4e8ec108a42c13f2bdf6dbe04 Mon Sep 17 00:00:00 2001 From: Niels de Vos Date: Mon, 3 Mar 2025 18:33:55 +0100 Subject: [PATCH] util: move kernel version functions to pkg/util/kernel Signed-off-by: Niels de Vos --- cmd/cephcsi.go | 4 +- e2e/rbd.go | 17 +-- e2e/rbd_helper.go | 10 +- internal/cephfs/mounter/volumemounter.go | 8 +- internal/rbd/nodeserver.go | 8 +- internal/rbd/rbd_attach.go | 8 +- internal/rbd/rbd_util.go | 19 +-- internal/util/fscrypt/fscrypt.go | 8 +- internal/util/util.go | 108 ----------------- internal/util/util_test.go | 123 ------------------- pkg/util/kernel/version.go | 132 +++++++++++++++++++++ pkg/util/kernel/version_test.go | 144 +++++++++++++++++++++++ 12 files changed, 323 insertions(+), 266 deletions(-) create mode 100644 pkg/util/kernel/version.go create mode 100644 pkg/util/kernel/version_test.go diff --git a/cmd/cephcsi.go b/cmd/cephcsi.go index 4f32c6c2f..c51a78d19 100644 --- a/cmd/cephcsi.go +++ b/cmd/cephcsi.go @@ -23,6 +23,8 @@ import ( "runtime" "time" + "github.com/ceph/ceph-csi/pkg/util/kernel" + "github.com/ceph/ceph-csi/internal/cephfs" "github.com/ceph/ceph-csi/internal/controller" "github.com/ceph/ceph-csi/internal/controller/persistentvolume" @@ -191,7 +193,7 @@ func printVersion() { fmt.Println("Go Version:", runtime.Version()) fmt.Println("Compiler:", runtime.Compiler) fmt.Printf("Platform: %s/%s\n", runtime.GOOS, runtime.GOARCH) - if kv, err := util.GetKernelVersion(); err == nil { + if kv, err := kernel.GetKernelVersion(); err == nil { fmt.Println("Kernel:", kv) } } diff --git a/e2e/rbd.go b/e2e/rbd.go index 6f4098f26..c15cedbbb 100644 --- a/e2e/rbd.go +++ b/e2e/rbd.go @@ -23,7 +23,8 @@ import ( "strings" "time" - "github.com/ceph/ceph-csi/internal/util" + "github.com/ceph/ceph-csi/pkg/util/crypto" + "github.com/ceph/ceph-csi/pkg/util/kernel" . "github.com/onsi/ginkgo/v2" v1 "k8s.io/api/core/v1" @@ -357,7 +358,7 @@ var _ = Describe("RBD", func() { framework.Failf("failed to get the kernel version: %v", err) } // default io-timeout=0, needs kernel >= 5.4 - if !util.CheckKernelSupport(kernelRelease, nbdZeroIOtimeoutSupport) { + if !kernel.CheckKernelSupport(kernelRelease, nbdZeroIOtimeoutSupport) { nbdMapOptions = "nbd:debug-rbd=20,io-timeout=330" } @@ -1223,7 +1224,7 @@ var _ = Describe("RBD", func() { return } - if util.CheckKernelSupport(kernelRelease, nbdResizeSupport) { + if kernel.CheckKernelSupport(kernelRelease, nbdResizeSupport) { err := deleteResource(rbdExamplePath + "storageclass.yaml") if err != nil { framework.Failf("failed to delete storageclass: %v", err) @@ -1273,7 +1274,7 @@ var _ = Describe("RBD", func() { By("create PVC with layering,fast-diff image-features and bind it to an app", func() { - if util.CheckKernelSupport(kernelRelease, fastDiffSupport) { + if kernel.CheckKernelSupport(kernelRelease, fastDiffSupport) { err := deleteResource(rbdExamplePath + "storageclass.yaml") if err != nil { framework.Failf("failed to delete storageclass: %v", err) @@ -1340,7 +1341,7 @@ var _ = Describe("RBD", func() { validateRBDImageCount(f, 1, defaultRBDPool) validateOmapCount(f, 1, rbdType, defaultRBDPool, volumesType) - if util.CheckKernelSupport(kernelRelease, deepFlattenSupport) { + if kernel.CheckKernelSupport(kernelRelease, deepFlattenSupport) { app, aErr := loadApp(appPath) if aErr != nil { framework.Failf("failed to load application: %v", aErr) @@ -1401,7 +1402,7 @@ var _ = Describe("RBD", func() { // checking the minimal kernel version for fast-diff as its // higher kernel version than other default image features. - if util.CheckKernelSupport(kernelRelease, fastDiffSupport) { + if kernel.CheckKernelSupport(kernelRelease, fastDiffSupport) { app, aErr := loadApp(appPath) if aErr != nil { framework.Failf("failed to load application: %v", aErr) @@ -1436,7 +1437,7 @@ var _ = Describe("RBD", func() { return } - if util.CheckKernelSupport(kernelRelease, fastDiffSupport) { + if kernel.CheckKernelSupport(kernelRelease, fastDiffSupport) { err := deleteResource(rbdExamplePath + "storageclass.yaml") if err != nil { framework.Failf("failed to delete storageclass: %v", err) @@ -1968,7 +1969,7 @@ var _ = Describe("RBD", func() { // Writes on kernel < 5.4 are failing due to a bug in NBD driver, // NBD zero cmd timeout handling is fixed with kernel >= 5.4 // see https://github.com/ceph/ceph-csi/issues/2204#issuecomment-930941047 - if util.CheckKernelSupport(kernelRelease, nbdZeroIOtimeoutSupport) { + if kernel.CheckKernelSupport(kernelRelease, nbdZeroIOtimeoutSupport) { filePath := app.Spec.Containers[0].VolumeMounts[0].MountPath + "/test" _, stdErr, err = execCommandInPod( f, diff --git a/e2e/rbd_helper.go b/e2e/rbd_helper.go index 02da4bfbc..05b59979a 100644 --- a/e2e/rbd_helper.go +++ b/e2e/rbd_helper.go @@ -25,7 +25,7 @@ import ( "sync" "time" - "github.com/ceph/ceph-csi/internal/util" + "github.com/ceph/ceph-csi/pkg/util/kernel" snapapi "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" v1 "k8s.io/api/core/v1" @@ -37,7 +37,7 @@ import ( ) //nolint:mnd // numbers specify Kernel versions. -var nbdResizeSupport = []util.KernelVersion{ +var nbdResizeSupport = []kernel.KernelVersion{ { Version: 5, PatchLevel: 3, @@ -49,7 +49,7 @@ var nbdResizeSupport = []util.KernelVersion{ } //nolint:mnd // numbers specify Kernel versions. -var fastDiffSupport = []util.KernelVersion{ +var fastDiffSupport = []kernel.KernelVersion{ { Version: 5, PatchLevel: 3, @@ -61,7 +61,7 @@ var fastDiffSupport = []util.KernelVersion{ } //nolint:mnd // numbers specify Kernel versions. -var deepFlattenSupport = []util.KernelVersion{ +var deepFlattenSupport = []kernel.KernelVersion{ { Version: 5, PatchLevel: 1, @@ -76,7 +76,7 @@ var deepFlattenSupport = []util.KernelVersion{ // www.mail-archive.com/linux-block@vger.kernel.org/msg38060.html // //nolint:mnd // numbers specify Kernel versions. -var nbdZeroIOtimeoutSupport = []util.KernelVersion{ +var nbdZeroIOtimeoutSupport = []kernel.KernelVersion{ { Version: 5, PatchLevel: 4, diff --git a/internal/cephfs/mounter/volumemounter.go b/internal/cephfs/mounter/volumemounter.go index edf85f5d0..72f28c23f 100644 --- a/internal/cephfs/mounter/volumemounter.go +++ b/internal/cephfs/mounter/volumemounter.go @@ -23,6 +23,8 @@ import ( "os/exec" "strings" + "github.com/ceph/ceph-csi/pkg/util/kernel" + "github.com/ceph/ceph-csi/internal/cephfs/store" "github.com/ceph/ceph-csi/internal/util" "github.com/ceph/ceph-csi/internal/util/log" @@ -32,7 +34,7 @@ var ( availableMounters []string //nolint:mnd // numbers specify Kernel versions. - quotaSupport = []util.KernelVersion{ + quotaSupport = []kernel.KernelVersion{ { Version: 4, PatchLevel: 17, @@ -70,12 +72,12 @@ func LoadAvailableMounters(conf *util.Config) error { log.ErrorLogMsg("failed to run mount.ceph %v", err) } else { // fetch the current running kernel info - release, kvErr := util.GetKernelVersion() + release, kvErr := kernel.GetKernelVersion() if kvErr != nil { return kvErr } - if conf.ForceKernelCephFS || util.CheckKernelSupport(release, quotaSupport) { + if conf.ForceKernelCephFS || kernel.CheckKernelSupport(release, quotaSupport) { log.DefaultLog("loaded mounter: %s", volumeMounterKernel) availableMounters = append(availableMounters, volumeMounterKernel) } else { diff --git a/internal/rbd/nodeserver.go b/internal/rbd/nodeserver.go index 309a118e0..1522559c6 100644 --- a/internal/rbd/nodeserver.go +++ b/internal/rbd/nodeserver.go @@ -24,6 +24,8 @@ import ( "strconv" "strings" + "github.com/ceph/ceph-csi/pkg/util/kernel" + csicommon "github.com/ceph/ceph-csi/internal/csi-common" "github.com/ceph/ceph-csi/internal/util" "github.com/ceph/ceph-csi/internal/util/file" @@ -88,7 +90,7 @@ var ( // deepFlattenSupport holds the list of kernel which support mapping rbd // image with deep-flatten image feature //nolint:mnd // numbers specify Kernel versions. - deepFlattenSupport = []util.KernelVersion{ + deepFlattenSupport = []kernel.KernelVersion{ { Version: 5, PatchLevel: 1, @@ -587,12 +589,12 @@ func flattenImageBeforeMapping( if kernelRelease == "" { // fetch the current running kernel info - kernelRelease, err = util.GetKernelVersion() + kernelRelease, err = kernel.GetKernelVersion() if err != nil { return err } } - if !util.CheckKernelSupport(kernelRelease, deepFlattenSupport) && !skipForceFlatten { + if !kernel.CheckKernelSupport(kernelRelease, deepFlattenSupport) && !skipForceFlatten { feature, err = volOptions.checkImageChainHasFeature(ctx, librbd.FeatureDeepFlatten) if err != nil { return err diff --git a/internal/rbd/rbd_attach.go b/internal/rbd/rbd_attach.go index a9e1ba434..a2c76ebab 100644 --- a/internal/rbd/rbd_attach.go +++ b/internal/rbd/rbd_attach.go @@ -25,6 +25,8 @@ import ( "strings" "time" + "github.com/ceph/ceph-csi/pkg/util/kernel" + "github.com/ceph/ceph-csi/internal/util" "github.com/ceph/ceph-csi/internal/util/log" @@ -71,7 +73,7 @@ var ( hasNBD = true hasNBDCookieSupport = false - kernelCookieSupport = []util.KernelVersion{ + kernelCookieSupport = []kernel.KernelVersion{ { Version: 5, PatchLevel: 14, @@ -243,13 +245,13 @@ func SetRbdNbdToolFeatures() { log.DefaultLog("nbd module loaded") // fetch the current running kernel info - release, err := util.GetKernelVersion() + release, err := kernel.GetKernelVersion() if err != nil { log.WarningLogMsg("fetching current kernel version failed (%v)", err) return } - if !util.CheckKernelSupport(release, kernelCookieSupport) { + if !kernel.CheckKernelSupport(release, kernelCookieSupport) { log.WarningLogMsg("kernel version %q doesn't support cookie feature", release) return diff --git a/internal/rbd/rbd_util.go b/internal/rbd/rbd_util.go index 016499df2..7e36b6b1b 100644 --- a/internal/rbd/rbd_util.go +++ b/internal/rbd/rbd_util.go @@ -29,6 +29,7 @@ import ( "time" "github.com/ceph/ceph-csi/pkg/util/crypto" + "github.com/ceph/ceph-csi/pkg/util/kernel" "github.com/ceph/ceph-csi/internal/rbd/types" "github.com/ceph/ceph-csi/internal/util" @@ -245,28 +246,28 @@ var ( }, } - krbdLayeringSupport = []util.KernelVersion{ + krbdLayeringSupport = []kernel.KernelVersion{ { Version: 3, PatchLevel: 8, SubLevel: 0, }, } - krbdStripingV2Support = []util.KernelVersion{ + krbdStripingV2Support = []kernel.KernelVersion{ { Version: 3, PatchLevel: 10, SubLevel: 0, }, } - krbdExclusiveLockSupport = []util.KernelVersion{ + krbdExclusiveLockSupport = []kernel.KernelVersion{ { Version: 4, PatchLevel: 9, SubLevel: 0, }, } - krbdDataPoolSupport = []util.KernelVersion{ + krbdDataPoolSupport = []kernel.KernelVersion{ { Version: 4, PatchLevel: 11, @@ -279,19 +280,19 @@ var ( // Minimum kernel version should be 3.8, else it will return error. func prepareKrbdFeatureAttrs() (uint64, error) { // fetch the current running kernel info - release, err := util.GetKernelVersion() + release, err := kernel.GetKernelVersion() if err != nil { return 0, fmt.Errorf("fetching current kernel version failed: %w", err) } switch { - case util.CheckKernelSupport(release, krbdDataPoolSupport): + case kernel.CheckKernelSupport(release, krbdDataPoolSupport): return librbd.FeatureDataPool, nil - case util.CheckKernelSupport(release, krbdExclusiveLockSupport): + case kernel.CheckKernelSupport(release, krbdExclusiveLockSupport): return librbd.FeatureExclusiveLock, nil - case util.CheckKernelSupport(release, krbdStripingV2Support): + case kernel.CheckKernelSupport(release, krbdStripingV2Support): return librbd.FeatureStripingV2, nil - case util.CheckKernelSupport(release, krbdLayeringSupport): + case kernel.CheckKernelSupport(release, krbdLayeringSupport): return librbd.FeatureLayering, nil } log.ErrorLogMsg("kernel version is too old: %q", release) diff --git a/internal/util/fscrypt/fscrypt.go b/internal/util/fscrypt/fscrypt.go index a1d05d3a2..360922878 100644 --- a/internal/util/fscrypt/fscrypt.go +++ b/internal/util/fscrypt/fscrypt.go @@ -35,6 +35,8 @@ import ( "github.com/pkg/xattr" "golang.org/x/sys/unix" + "github.com/ceph/ceph-csi/pkg/util/kernel" + "github.com/ceph/ceph-csi/internal/kms" "github.com/ceph/ceph-csi/internal/util" "github.com/ceph/ceph-csi/internal/util/log" @@ -47,7 +49,7 @@ const ( encryptionPassphraseSize = 64 ) -var policyV2Support = []util.KernelVersion{ +var policyV2Support = []kernel.KernelVersion{ { Version: 5, PatchLevel: 4, @@ -327,13 +329,13 @@ func IsDirectoryUnlocked(directoryPath, filesystem string) error { func getBestPolicyVersion() (int64, error) { // fetch the current running kernel info - release, err := util.GetKernelVersion() + release, err := kernel.GetKernelVersion() if err != nil { return 0, fmt.Errorf("fetching current kernel version failed: %w", err) } switch { - case util.CheckKernelSupport(release, policyV2Support): + case kernel.CheckKernelSupport(release, policyV2Support): return 2, nil default: return 1, nil diff --git a/internal/util/util.go b/internal/util/util.go index 1d62c6502..16f8974d2 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -28,9 +28,7 @@ import ( "time" "github.com/ceph/ceph-csi/internal/util/k8s" - "github.com/ceph/ceph-csi/internal/util/log" - "golang.org/x/sys/unix" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/cloud-provider/volume/helpers" mount "k8s.io/mount-utils" @@ -177,112 +175,6 @@ func ValidateDriverName(driverName string) error { return err } -// GetKernelVersion returns the version of the running Unix (like) system from the -// 'utsname' structs 'release' component. -func GetKernelVersion() (string, error) { - utsname := unix.Utsname{} - if err := unix.Uname(&utsname); err != nil { - return "", err - } - - return strings.TrimRight(string(utsname.Release[:]), "\x00"), nil -} - -// KernelVersion holds kernel related information. -type KernelVersion struct { - Version int - PatchLevel int - SubLevel int - ExtraVersion int // prefix of the part after the first "-" - Distribution string // component of full extraversion - Backport bool // backport have a fixed version/patchlevel/sublevel -} - -// parseKernelRelease parses a kernel release version string into: -// version, patch version, sub version and extra version. -func parseKernelRelease(release string) (int, int, int, int, error) { - version := 0 - patchlevel := 0 - minVersions := 2 - - extra := "" - n, err := fmt.Sscanf(release, "%d.%d%s", &version, &patchlevel, &extra) - if n < minVersions && err != nil { - return 0, 0, 0, 0, fmt.Errorf("failed to parse version and patchlevel from %s: %w", release, err) - } - - sublevel := 0 - extraversion := 0 - if n > minVersions { - n, err = fmt.Sscanf(extra, ".%d%s", &sublevel, &extra) - if err != nil && n == 0 && extra != "" && extra[0] != '-' && extra[0] == '.' { - return 0, 0, 0, 0, fmt.Errorf("failed to parse subversion from %s: %w", release, err) - } - - extra = strings.TrimPrefix(extra, "-") - // ignore errors, 1st component of extraversion does not need to be an int - _, err = fmt.Sscanf(extra, "%d", &extraversion) - if err != nil { - // "go lint" wants err to be checked... - extraversion = 0 - } - } - - return version, patchlevel, sublevel, extraversion, nil -} - -// CheckKernelSupport checks the running kernel and comparing it to known -// versions that have support for required features . Distributors of -// enterprise Linux have backport quota support to previous versions. This -// function checks if the running kernel is one of the versions that have the -// feature/fixes backport. -// -// `uname -r` (or Uname().Utsname.Release has a format like 1.2.3-rc.vendor -// This can be slit up in the following components: - version (1) - patchlevel -// (2) - sublevel (3) - optional, defaults to 0 - extraversion (rc) - optional, -// matching integers only - distribution (.vendor) - optional, match against -// whole `uname -r` string -// -// For matching multiple versions, the kernelSupport type contains a backport -// bool, which will cause matching -// version+patchlevel+sublevel+(>=extraversion)+(~distribution) -// -// In case the backport bool is false, a simple check for higher versions than -// version+patchlevel+sublevel is done. -func CheckKernelSupport(release string, supportedVersions []KernelVersion) bool { - version, patchlevel, sublevel, extraversion, err := parseKernelRelease(release) - if err != nil { - log.ErrorLogMsg("%v", err) - - return false - } - - // compare running kernel against known versions - for _, kernel := range supportedVersions { - if !kernel.Backport { - // deal with the default case(s), find >= match for version, patchlevel, sublevel - if version > kernel.Version || (version == kernel.Version && patchlevel > kernel.PatchLevel) || - (version == kernel.Version && patchlevel == kernel.PatchLevel && sublevel >= kernel.SubLevel) { - return true - } - } else { - // specific backport, match distribution initially - if !strings.Contains(release, kernel.Distribution) { - continue - } - - // strict match version, patchlevel, sublevel, and >= match extraversion - if version == kernel.Version && patchlevel == kernel.PatchLevel && - sublevel == kernel.SubLevel && extraversion >= kernel.ExtraVersion { - return true - } - } - } - log.WarningLogMsg("kernel %s does not support required features", release) - - return false -} - // GenerateVolID generates a volume ID based on passed in parameters and version, to be returned // to the CO system. func GenerateVolID( diff --git a/internal/util/util_test.go b/internal/util/util_test.go index a39078114..ace1e31c5 100644 --- a/internal/util/util_test.go +++ b/internal/util/util_test.go @@ -16,7 +16,6 @@ limitations under the License. package util import ( - "strings" "testing" ) @@ -146,20 +145,6 @@ func TestRoundOffVolSize(t *testing.T) { } } -func TestGetKernelVersion(t *testing.T) { - t.Parallel() - version, err := GetKernelVersion() - if err != nil { - t.Errorf("failed to get kernel version: %s", err) - } - if version == "" { - t.Error("version is empty, this is unexpected?!") - } - if strings.HasSuffix(version, "\x00") { - t.Error("version ends with \\x00 byte(s)") - } -} - func TestMountOptionsAdd(t *testing.T) { t.Parallel() moaTests := []struct { @@ -241,114 +226,6 @@ func TestMountOptionsAdd(t *testing.T) { } } -func TestParseKernelRelease(t *testing.T) { - t.Parallel() - - badReleases := []string{"x", "5", "5.", "5.4.", "5.x-2-oops", "4.1.x-7-oh", "5.12.x"} - for _, release := range badReleases { - _, _, _, _, err := parseKernelRelease(release) - if err == nil { - t.Errorf("release %q must not be parsed successfully", release) - } - } - - goodReleases := []string{ - "5.12", "5.12xlinux", "5.1-2-yam", "3.1-5-x", "5.12.14", "5.12.14xlinux", - "5.12.14-xlinux", "5.12.14-99-x", "3.3x-3", - } - goodVersions := [][]int{ - {5, 12, 0, 0}, - {5, 12, 0, 0}, - {5, 1, 0, 2}, - {3, 1, 0, 5}, - {5, 12, 14, 0}, - {5, 12, 14, 0}, - {5, 12, 14, 0}, - {5, 12, 14, 99}, - {3, 3, 0, 0}, - } - for i, release := range goodReleases { - version, patchlevel, sublevel, extraversion, err := parseKernelRelease(release) - if err != nil { - t.Errorf("parsing error for release %q: %s", release, err) - } - good := goodVersions[i] - if version != good[0] || patchlevel != good[1] || sublevel != good[2] || extraversion != good[3] { - t.Errorf("release %q parsed incorrectly: expected (%d.%d.%d-%d), actual (%d.%d.%d-%d)", - release, good[0], good[1], good[2], good[3], - version, patchlevel, sublevel, extraversion) - } - } -} - -func TestCheckKernelSupport(t *testing.T) { - t.Parallel() - supportsQuota := []string{ - "4.17.0", - "5.0.0", - "4.17.0-rc1", - "4.18.0-80.el8", - "3.10.0-1062.el7.x86_64", // 1st backport - "3.10.0-1062.4.1.el7.x86_64", // updated backport - } - - noQuota := []string{ - "2.6.32-754.15.3.el6.x86_64", // too old - "3.10.0-123.el7.x86_64", // too old for backport - "3.10.0-1062.4.1.el8.x86_64", // nonexisting RHEL-8 kernel - "3.11.0-123.el7.x86_64", // nonexisting RHEL-7 kernel - } - - quotaSupport := []KernelVersion{ - {4, 17, 0, 0, "", false}, // standard 4.17+ versions - {3, 10, 0, 1062, ".el7", true}, // RHEL-7.7 - } - for _, kernel := range supportsQuota { - ok := CheckKernelSupport(kernel, quotaSupport) - if !ok { - t.Errorf("support expected for %s", kernel) - } - } - - for _, kernel := range noQuota { - ok := CheckKernelSupport(kernel, quotaSupport) - if ok { - t.Errorf("no support expected for %s", kernel) - } - } - - supportsDeepFlatten := []string{ - "5.1.0", // 5.1+ supports deep-flatten - "5.3.0", - "4.18.0-193.9.1.el8_2.x86_64", // RHEL 8.2 kernel - } - - noDeepFlatten := []string{ - "4.18.0", // too old - "3.10.0-123.el7.x86_64", // too old for backport - "3.10.0-1062.4.1.el8.x86_64", // nonexisting RHEL-8 kernel - "3.11.0-123.el7.x86_64", // nonexisting RHEL-7 kernel - } - - deepFlattenSupport := []KernelVersion{ - {5, 1, 0, 0, "", false}, // standard 5.1+ versions - {4, 18, 0, 193, ".el8", true}, // RHEL 8.2 backport - } - for _, kernel := range supportsDeepFlatten { - ok := CheckKernelSupport(kernel, deepFlattenSupport) - if !ok { - t.Errorf("support expected for %s", kernel) - } - } - - for _, kernel := range noDeepFlatten { - ok := CheckKernelSupport(kernel, deepFlattenSupport) - if ok { - t.Errorf("no support expected for %s", kernel) - } - } -} - func TestRoundOffCephFSVolSize(t *testing.T) { t.Parallel() tests := []struct { diff --git a/pkg/util/kernel/version.go b/pkg/util/kernel/version.go new file mode 100644 index 000000000..25f33e1f6 --- /dev/null +++ b/pkg/util/kernel/version.go @@ -0,0 +1,132 @@ +/* +Copyright 2025 The Ceph-CSI Authors. + +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 kernel + +import ( + "fmt" + "strings" + + "golang.org/x/sys/unix" + + "github.com/ceph/ceph-csi/internal/util/log" +) + +// GetKernelVersion returns the version of the running Unix (like) system from the +// 'utsname' structs 'release' component. +func GetKernelVersion() (string, error) { + utsname := unix.Utsname{} + if err := unix.Uname(&utsname); err != nil { + return "", err + } + + return strings.TrimRight(string(utsname.Release[:]), "\x00"), nil +} + +// KernelVersion holds kernel related information. +type KernelVersion struct { + Version int + PatchLevel int + SubLevel int + ExtraVersion int // prefix of the part after the first "-" + Distribution string // component of full extraversion + Backport bool // backport have a fixed version/patchlevel/sublevel +} + +// parseKernelRelease parses a kernel release version string into: +// version, patch version, sub version and extra version. +func parseKernelRelease(release string) (int, int, int, int, error) { + version := 0 + patchlevel := 0 + minVersions := 2 + + extra := "" + n, err := fmt.Sscanf(release, "%d.%d%s", &version, &patchlevel, &extra) + if n < minVersions && err != nil { + return 0, 0, 0, 0, fmt.Errorf("failed to parse version and patchlevel from %s: %w", release, err) + } + + sublevel := 0 + extraversion := 0 + if n > minVersions { + n, err = fmt.Sscanf(extra, ".%d%s", &sublevel, &extra) + if err != nil && n == 0 && extra != "" && extra[0] != '-' && extra[0] == '.' { + return 0, 0, 0, 0, fmt.Errorf("failed to parse subversion from %s: %w", release, err) + } + + extra = strings.TrimPrefix(extra, "-") + // ignore errors, 1st component of extraversion does not need to be an int + _, err = fmt.Sscanf(extra, "%d", &extraversion) + if err != nil { + // "go lint" wants err to be checked... + extraversion = 0 + } + } + + return version, patchlevel, sublevel, extraversion, nil +} + +// CheckKernelSupport checks the running kernel and comparing it to known +// versions that have support for required features . Distributors of +// enterprise Linux have backport quota support to previous versions. This +// function checks if the running kernel is one of the versions that have the +// feature/fixes backport. +// +// `uname -r` (or Uname().Utsname.Release has a format like 1.2.3-rc.vendor +// This can be slit up in the following components: - version (1) - patchlevel +// (2) - sublevel (3) - optional, defaults to 0 - extraversion (rc) - optional, +// matching integers only - distribution (.vendor) - optional, match against +// whole `uname -r` string +// +// For matching multiple versions, the kernelSupport type contains a backport +// bool, which will cause matching +// version+patchlevel+sublevel+(>=extraversion)+(~distribution) +// +// In case the backport bool is false, a simple check for higher versions than +// version+patchlevel+sublevel is done. +func CheckKernelSupport(release string, supportedVersions []KernelVersion) bool { + version, patchlevel, sublevel, extraversion, err := parseKernelRelease(release) + if err != nil { + log.ErrorLogMsg("%v", err) + + return false + } + + // compare running kernel against known versions + for _, kernel := range supportedVersions { + if !kernel.Backport { + // deal with the default case(s), find >= match for version, patchlevel, sublevel + if version > kernel.Version || (version == kernel.Version && patchlevel > kernel.PatchLevel) || + (version == kernel.Version && patchlevel == kernel.PatchLevel && sublevel >= kernel.SubLevel) { + return true + } + } else { + // specific backport, match distribution initially + if !strings.Contains(release, kernel.Distribution) { + continue + } + + // strict match version, patchlevel, sublevel, and >= match extraversion + if version == kernel.Version && patchlevel == kernel.PatchLevel && + sublevel == kernel.SubLevel && extraversion >= kernel.ExtraVersion { + return true + } + } + } + log.WarningLogMsg("kernel %s does not support required features", release) + + return false +} diff --git a/pkg/util/kernel/version_test.go b/pkg/util/kernel/version_test.go new file mode 100644 index 000000000..7e239c3cc --- /dev/null +++ b/pkg/util/kernel/version_test.go @@ -0,0 +1,144 @@ +/* +Copyright 2025 The Ceph-CSI Authors. + +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 kernel + +import ( + "strings" + "testing" +) + +func TestGetKernelVersion(t *testing.T) { + t.Parallel() + version, err := GetKernelVersion() + if err != nil { + t.Errorf("failed to get kernel version: %s", err) + } + if version == "" { + t.Error("version is empty, this is unexpected?!") + } + if strings.HasSuffix(version, "\x00") { + t.Error("version ends with \\x00 byte(s)") + } +} + +func TestParseKernelRelease(t *testing.T) { + t.Parallel() + + badReleases := []string{"x", "5", "5.", "5.4.", "5.x-2-oops", "4.1.x-7-oh", "5.12.x"} + for _, release := range badReleases { + _, _, _, _, err := parseKernelRelease(release) + if err == nil { + t.Errorf("release %q must not be parsed successfully", release) + } + } + + goodReleases := []string{ + "5.12", "5.12xlinux", "5.1-2-yam", "3.1-5-x", "5.12.14", "5.12.14xlinux", + "5.12.14-xlinux", "5.12.14-99-x", "3.3x-3", + } + goodVersions := [][]int{ + {5, 12, 0, 0}, + {5, 12, 0, 0}, + {5, 1, 0, 2}, + {3, 1, 0, 5}, + {5, 12, 14, 0}, + {5, 12, 14, 0}, + {5, 12, 14, 0}, + {5, 12, 14, 99}, + {3, 3, 0, 0}, + } + for i, release := range goodReleases { + version, patchlevel, sublevel, extraversion, err := parseKernelRelease(release) + if err != nil { + t.Errorf("parsing error for release %q: %s", release, err) + } + good := goodVersions[i] + if version != good[0] || patchlevel != good[1] || sublevel != good[2] || extraversion != good[3] { + t.Errorf("release %q parsed incorrectly: expected (%d.%d.%d-%d), actual (%d.%d.%d-%d)", + release, good[0], good[1], good[2], good[3], + version, patchlevel, sublevel, extraversion) + } + } +} + +func TestCheckKernelSupport(t *testing.T) { + t.Parallel() + supportsQuota := []string{ + "4.17.0", + "5.0.0", + "4.17.0-rc1", + "4.18.0-80.el8", + "3.10.0-1062.el7.x86_64", // 1st backport + "3.10.0-1062.4.1.el7.x86_64", // updated backport + } + + noQuota := []string{ + "2.6.32-754.15.3.el6.x86_64", // too old + "3.10.0-123.el7.x86_64", // too old for backport + "3.10.0-1062.4.1.el8.x86_64", // nonexisting RHEL-8 kernel + "3.11.0-123.el7.x86_64", // nonexisting RHEL-7 kernel + } + + quotaSupport := []KernelVersion{ + {4, 17, 0, 0, "", false}, // standard 4.17+ versions + {3, 10, 0, 1062, ".el7", true}, // RHEL-7.7 + } + for _, kernel := range supportsQuota { + ok := CheckKernelSupport(kernel, quotaSupport) + if !ok { + t.Errorf("support expected for %s", kernel) + } + } + + for _, kernel := range noQuota { + ok := CheckKernelSupport(kernel, quotaSupport) + if ok { + t.Errorf("no support expected for %s", kernel) + } + } + + supportsDeepFlatten := []string{ + "5.1.0", // 5.1+ supports deep-flatten + "5.3.0", + "4.18.0-193.9.1.el8_2.x86_64", // RHEL 8.2 kernel + } + + noDeepFlatten := []string{ + "4.18.0", // too old + "3.10.0-123.el7.x86_64", // too old for backport + "3.10.0-1062.4.1.el8.x86_64", // nonexisting RHEL-8 kernel + "3.11.0-123.el7.x86_64", // nonexisting RHEL-7 kernel + } + + deepFlattenSupport := []KernelVersion{ + {5, 1, 0, 0, "", false}, // standard 5.1+ versions + {4, 18, 0, 193, ".el8", true}, // RHEL 8.2 backport + } + for _, kernel := range supportsDeepFlatten { + ok := CheckKernelSupport(kernel, deepFlattenSupport) + if !ok { + t.Errorf("support expected for %s", kernel) + } + } + + for _, kernel := range noDeepFlatten { + ok := CheckKernelSupport(kernel, deepFlattenSupport) + if ok { + t.Errorf("no support expected for %s", kernel) + } + } +}