Repository: elastic/go-sysinfo
Pull request #248 (2024-11-13 08:57:16)

Enhance OS version parsing to handle SUSE SP notation
## Description
This PR enhances the `versionGrok` regex pattern within makeOSInfo to support SUSE Linux Enterprise Server (**SLES**) versions that use `SP` notation for Service Packs, such as `15-SP5`.

The updated regex now recognizes `SP` as a valid separator for the minor version, allowing accurate parsing of **SLES** versions alongside standard major.minor.patch formats (e.g., `22.04`). This change resolves issues where Minor was incorrectly set to `0` for `SP`-based versions and ensures compatibility with both SUSE-specific and conventional version formats.

## Changes
Updated `versionGrok` regex: Added optional SP notation support as a prefix for the minor version.
Compatibility maintained: Supports both `.` and `-` delimiters to handle multiple versioning schemes.
## Testing
Test cases added: Included tests for both `SLES 15-SP5` and `SLED 15-SP5` to validate correct assignment of major, minor, and patch values.

andrewkroh: > not that anybody can read regex

That also means the maintainability of the regex will suffer. I would like to see another revision where the code is modified to have a separate regex for suse. I think is will be more readable and better to maintain. Then either we try each regex to see which one matches OR we use the ID_LIKE="suse" to choose the right regex to apply.
sidis-zafran: > > not that anybody can read regex
> 
> That also means the maintainability of the regex will suffer. I would like to see another revision where the code is modified to have a separate regex for suse. I think is will be more readable and better to maintain. Then either we try each regex to see which one matches OR we use the ID_LIKE="suse" to choose the right regex to apply.

Totally agreed. Updated with slight changes.
sidis-zafran: @andrewkroh 
Added release-note. Please rerun checks so we may merge this one 🙏 

Patch:
diff --git a/.changelog/248.txt b/.changelog/248.txt
new file mode 100644
index 0000000..5dd3c3d
--- /dev/null
+++ b/.changelog/248.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+linux: Support SLES/SLED OS version extraction.
+```
diff --git a/providers/linux/os.go b/providers/linux/os.go
index ece0f37..f2e366a 100644
--- a/providers/linux/os.go
+++ b/providers/linux/os.go
@@ -32,10 +32,11 @@ import (
 )
 
 const (
-	osRelease      = "/etc/os-release"
-	lsbRelease     = "/etc/lsb-release"
-	distribRelease = "/etc/*-release"
-	versionGrok    = `(?P<version>(?P<major>[0-9]+)\.?(?P<minor>[0-9]+)?\.?(?P<patch>\w+)?)(?: \((?P<codename>[-\w ]+)\))?`
+	osRelease       = "/etc/os-release"
+	lsbRelease      = "/etc/lsb-release"
+	distribRelease  = "/etc/*-release"
+	versionGrok     = `(?P<version>(?P<major>[0-9]+)\.?(?P<minor>[0-9]+)?\.?(?P<patch>\w+)?)(?: \((?P<codename>[-\w ]+)\))?`
+	versionGrokSuse = `(?P<version>(?P<major>[0-9]+)(?:[.-]?(?:SP)?(?P<minor>[0-9]+))?(?:[.-](?P<patch>[0-9]+|\w+))?)(?: \((?P<codename>[-\w ]+)\))?`
 )
 
 var (
@@ -44,6 +45,9 @@ var (
 
 	// versionRegexp parses version numbers (e.g. 6 or 6.1 or 6.1.0 or 6.1.0_20150102).
 	versionRegexp = regexp.MustCompile(versionGrok)
+
+	// versionRegexpSuse parses version numbers for SUSE (e.g. 15-SP1).
+	versionRegexpSuse = regexp.MustCompile(versionGrokSuse)
 )
 
 // familyMap contains a mapping of family -> []platforms.
@@ -186,28 +190,33 @@ func makeOSInfo(osRelease map[string]string) (*types.OSInfo, error) {
 		}
 	}
 
-	if os.Version != "" {
-		// Try parsing info from the version.
-		keys := versionRegexp.SubexpNames()
-		for i, m := range versionRegexp.FindStringSubmatch(os.Version) {
-			switch keys[i] {
-			case "major":
-				os.Major, _ = strconv.Atoi(m)
-			case "minor":
-				os.Minor, _ = strconv.Atoi(m)
-			case "patch":
-				os.Patch, _ = strconv.Atoi(m)
-			case "codename":
-				if os.Codename == "" {
-					os.Codename = m
-				}
-			}
-		}
+	if osRelease["ID_LIKE"] == "suse" {
+		extractVersionDetails(os, os.Version, versionRegexpSuse)
+	} else if os.Version != "" {
+		extractVersionDetails(os, os.Version, versionRegexp)
 	}
 
 	return os, nil
 }
 
+func extractVersionDetails(os *types.OSInfo, version string, re *regexp.Regexp) {
+	keys := re.SubexpNames()
+	for i, match := range re.FindStringSubmatch(version) {
+		switch keys[i] {
+		case "major":
+			os.Major, _ = strconv.Atoi(match)
+		case "minor":
+			os.Minor, _ = strconv.Atoi(match)
+		case "patch":
+			os.Patch, _ = strconv.Atoi(match)
+		case "codename":
+			if os.Codename == "" {
+				os.Codename = match
+			}
+		}
+	}
+}
+
 func findDistribRelease(baseDir string) (*types.OSInfo, error) {
 	matches, err := filepath.Glob(filepath.Join(baseDir, distribRelease))
 	if err != nil {


Test patch:
diff --git a/providers/linux/os_test.go b/providers/linux/os_test.go
index 7f3f677..e780886 100644
--- a/providers/linux/os_test.go
+++ b/providers/linux/os_test.go
@@ -418,4 +418,36 @@ func TestOperatingSystem(t *testing.T) {
 		}, *os)
 		t.Logf("%#v", os)
 	})
+	t.Run("sles15sp5", func(t *testing.T) {
+		os, err := getOSInfo("testdata/sles15sp5")
+		if err != nil {
+			t.Fatal(err)
+		}
+		assert.Equal(t, types.OSInfo{
+			Type:     "linux",
+			Family:   "suse",
+			Platform: "sles",
+			Name:     "SLES",
+			Version:  "15-SP5",
+			Major:    15,
+			Minor:    5,
+		}, *os)
+		t.Logf("%#v", os)
+	})
+	t.Run("sled15sp5", func(t *testing.T) {
+		os, err := getOSInfo("testdata/sled15sp5")
+		if err != nil {
+			t.Fatal(err)
+		}
+		assert.Equal(t, types.OSInfo{
+			Type:     "linux",
+			Family:   "suse",
+			Platform: "sled",
+			Name:     "SLED",
+			Version:  "15-SP5",
+			Major:    15,
+			Minor:    5,
+		}, *os)
+		t.Logf("%#v", os)
+	})
 }
diff --git a/providers/linux/testdata/sled15sp5/etc/os-release b/providers/linux/testdata/sled15sp5/etc/os-release
new file mode 100644
index 0000000..84cea18
--- /dev/null
+++ b/providers/linux/testdata/sled15sp5/etc/os-release
@@ -0,0 +1,9 @@
+NAME="SLED"
+VERSION="15-SP5"
+VERSION_ID="15.5"
+PRETTY_NAME="SUSE Linux Enterprise Desktop 15 SP5"
+ID="sled"
+ID_LIKE="suse"
+ANSI_COLOR="0;32"
+CPE_NAME="cpe:/o:suse:sled:15:sp5"
+DOCUMENTATION_URL="https://documentation.suse.com/"
diff --git a/providers/linux/testdata/sles15sp5/etc/os-release b/providers/linux/testdata/sles15sp5/etc/os-release
new file mode 100644
index 0000000..1d867e3
--- /dev/null
+++ b/providers/linux/testdata/sles15sp5/etc/os-release
@@ -0,0 +1,9 @@
+NAME="SLES"
+VERSION="15-SP5"
+VERSION_ID="15.5"
+PRETTY_NAME="SUSE Linux Enterprise Server 15 SP5"
+ID="sles"
+ID_LIKE="suse"
+ANSI_COLOR="0;32"
+CPE_NAME="cpe:/o:suse:sles:15:sp5"
+DOCUMENTATION_URL="https://documentation.suse.com/"