Skip to content

Commit 67435b8

Browse files
authored
✨ Detect activated RHEL modules (#5720)
* ✨ Detect activated RHEL modules Implements: #5695 Signed-off-by: Christian Zunker <christian@mondoo.com> * Use filepath Signed-off-by: Christian Zunker <christian@mondoo.com> --------- Signed-off-by: Christian Zunker <christian@mondoo.com>
1 parent b35bbf6 commit 67435b8

File tree

3 files changed

+187
-0
lines changed

3 files changed

+187
-0
lines changed

providers/os/detector/detector_all.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,10 @@ var redhatFamily = &PlatformResolver{
821821
pf.Version = release
822822
}
823823

824+
// RHEL can have various modules activated, identify them via filesystem
825+
modules := getActivatedRhelModules(conn)
826+
pf.Metadata["redhat/modules"] = strings.Join(modules, ",")
827+
824828
return true, nil
825829
}
826830

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package detector
5+
6+
import (
7+
"bufio"
8+
"bytes"
9+
"path/filepath"
10+
"strings"
11+
12+
"github.com/spf13/afero"
13+
"go.mondoo.com/cnquery/v11/providers/os/connection/shared"
14+
)
15+
16+
const (
17+
modulesDir = "/etc/dnf/modules.d"
18+
)
19+
20+
type RhelModule struct {
21+
Name string
22+
State string
23+
}
24+
25+
func getActivatedRhelModules(conn shared.Connection) []string {
26+
afs := &afero.Afero{Fs: conn.FileSystem()}
27+
ok, err := afs.DirExists(modulesDir)
28+
if err != nil || !ok {
29+
return []string{}
30+
}
31+
32+
files, err := afs.ReadDir(modulesDir)
33+
if err != nil {
34+
return []string{}
35+
}
36+
37+
modules := []string{}
38+
for _, file := range files {
39+
if !strings.HasSuffix(file.Name(), ".module") {
40+
continue
41+
}
42+
43+
content, err := afs.ReadFile(filepath.Join(modulesDir, file.Name()))
44+
if err != nil {
45+
continue
46+
}
47+
48+
module := RhelModule{}
49+
scanner := bufio.NewScanner(bytes.NewReader(content))
50+
for scanner.Scan() {
51+
s := strings.Split(scanner.Text(), "=")
52+
if len(s) != 2 {
53+
continue
54+
}
55+
56+
switch strings.ToLower(s[0]) {
57+
case "name":
58+
module.Name = strings.TrimSpace(s[1])
59+
case "state":
60+
module.State = strings.TrimSpace(s[1])
61+
}
62+
}
63+
64+
// We are only interested in enabled modules
65+
if module.State != "enabled" {
66+
continue
67+
}
68+
69+
modules = append(modules, module.Name)
70+
}
71+
72+
return modules
73+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package detector
5+
6+
import (
7+
"testing"
8+
9+
"github.com/spf13/afero"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestGetActivatedRhelModules(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
files map[string]string
17+
expected []string
18+
}{
19+
{
20+
name: "no modules directory",
21+
files: map[string]string{},
22+
expected: []string{},
23+
},
24+
{
25+
name: "empty modules directory",
26+
files: map[string]string{
27+
"/etc/dnf/modules.d": "",
28+
},
29+
expected: []string{},
30+
},
31+
{
32+
name: "valid modules",
33+
files: map[string]string{
34+
"/etc/dnf/modules.d/maven.module": `[maven]
35+
name=maven
36+
stream=3.8
37+
profiles=
38+
state=enabled`,
39+
},
40+
expected: []string{"maven"},
41+
},
42+
{
43+
name: "disabled module",
44+
files: map[string]string{
45+
"/etc/dnf/modules.d/maven.module": `[maven]
46+
name=maven
47+
stream=3.8
48+
profiles=
49+
state=disabled`,
50+
},
51+
expected: []string{},
52+
},
53+
{
54+
name: "multiple modules",
55+
files: map[string]string{
56+
"/etc/dnf/modules.d/maven.module": `[maven]
57+
name=maven
58+
stream=3.8
59+
profiles=
60+
state=enabled`,
61+
"/etc/dnf/modules.d/python36.module": `[python36]
62+
name=python36
63+
stream=3.6
64+
profiles=
65+
state=enabled`,
66+
},
67+
expected: []string{"maven", "python36"},
68+
},
69+
{
70+
name: "invalid content",
71+
files: map[string]string{
72+
"/etc/dnf/modules.d/invalid.module": `invalid content`,
73+
},
74+
expected: []string{},
75+
},
76+
}
77+
78+
for _, tt := range tests {
79+
t.Run(tt.name, func(t *testing.T) {
80+
// Create a mock filesystem
81+
fs := afero.NewMemMapFs()
82+
83+
// Create the directory structure
84+
if len(tt.files) > 0 {
85+
err := fs.MkdirAll("/etc/dnf/modules.d", 0o755)
86+
assert.NoError(t, err)
87+
}
88+
89+
// Create the files
90+
for path, content := range tt.files {
91+
if path == "/etc/dnf/modules.d" {
92+
continue
93+
}
94+
err := afero.WriteFile(fs, path, []byte(content), 0o644)
95+
assert.NoError(t, err)
96+
}
97+
98+
// Create a mock connection
99+
conn := &mockConnection{
100+
fs: fs,
101+
}
102+
103+
// Call the function
104+
result := getActivatedRhelModules(conn)
105+
106+
// Compare results
107+
assert.Equal(t, tt.expected, result)
108+
})
109+
}
110+
}

0 commit comments

Comments
 (0)