Skip to content

Commit 47bfd7b

Browse files
briantumrafi97
andauthored
[EUDM] Collect Mac system information (#44301)
### What does this PR do? Mac implementation to collect system hardware information. Stacked on top of @mrafi97's branch `mrafi/host-hardware` #44952. ### Motivation https://datadoghq.atlassian.net/browse/WINA-2081 ### Describe how you validated your changes - Added unit tests - Diagnose command 1. `dda inv agent.build --build-exclude=systemd` 2. Add `infrastructure_mode: end_user_device` and `cmd_port: 5051` (IPC server doesn't automatically run for dev builds, required for diagnose command) to `bin/agent/dist/datadog.yaml` 3. `./bin/agent/agent run -c bin/agent/dist/datadog.yaml &` 4. `./bin/agent/agent -c bin/agent/dist/datadog.yaml diagnose show-metadata host-system-info` ``` { "hostname": "test-hostname", "timestamp": 1768324940565060000, "host_system_info_metadata": { "manufacturer": "Apple Inc.", "model_number": "Z1FD000AYLL/A", "serial_number": "G0FHNH5647", "model_name": "MacBook Pro (14-inch, Nov 2024)", "chassis_type": "Laptop", "identifier": "Mac16,6" }, "uuid": "18951416-a4d7-5ab4-b253-49733fcc5f59" } ``` ### Additional Notes Co-authored-by: mrafi97 <36865458+mrafi97@users.noreply.github.com> Co-authored-by: brian.tu <brian.tu@datadoghq.com>
1 parent 68082df commit 47bfd7b

File tree

10 files changed

+366
-25
lines changed

10 files changed

+366
-25
lines changed

comp/metadata/hostsysteminfo/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The payload contains physical system identification attributes collected from th
1414

1515
The system information is collected using platform-specific APIs:
1616
- **Windows**: WMI queries (`Win32_ComputerSystem`, `Win32_BIOS`, `Win32_SystemEnclosure`)
17-
- **MacOS**: Work in progress
17+
- **MacOS**: IOKit queries (`IOPlatformExpertDevice`, `product`)
1818
- **Linux/Unix**: Will not run as it is currently not implemented
1919

2020
Collection includes:

comp/metadata/hostsysteminfo/impl/hostsysteminfo.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,19 +89,19 @@ func NewSystemInfoProvider(deps Requires) Provides {
8989
hh.InventoryPayload.MinInterval = 1 * time.Hour
9090
hh.InventoryPayload.MaxInterval = 1 * time.Hour
9191

92-
// Only enable system info metadata collection for end user device infrastructure mode on Windows
92+
// Only enable system info metadata collection for end user device infrastructure mode on Windows and Darwin
9393
infraMode := deps.Config.GetString("infrastructure_mode")
9494
isEndUserDevice := infraMode == "end_user_device"
95-
isWindows := runtime.GOOS == "windows"
96-
hh.InventoryPayload.Enabled = hh.InventoryPayload.Enabled && isEndUserDevice && isWindows
95+
isSupportedOS := runtime.GOOS == "windows" || runtime.GOOS == "darwin"
96+
hh.InventoryPayload.Enabled = hh.InventoryPayload.Enabled && isEndUserDevice && isSupportedOS
9797

9898
var provider runnerimpl.Provider
9999
if hh.InventoryPayload.Enabled {
100100
provider = hh.MetadataProvider()
101101
deps.Log.Info("System info metadata collection enabled for end user device mode")
102102
} else {
103-
if !isWindows {
104-
deps.Log.Debugf("System info metadata collection disabled: only supported on Windows (current OS: %s)", runtime.GOOS)
103+
if !isSupportedOS {
104+
deps.Log.Debugf("System info metadata collection disabled: only supported on Windows and macOS (current OS: %s)", runtime.GOOS)
105105
} else {
106106
deps.Log.Debugf("System info metadata collection disabled: infrastructure_mode is '%s' (requires 'end_user_device')", infraMode)
107107
}
@@ -116,8 +116,8 @@ func NewSystemInfoProvider(deps Requires) Provides {
116116
}
117117

118118
func (hh *hostSystemInfo) fillData() error {
119-
// System info collection is only supported on Windows
120-
if runtime.GOOS != "windows" {
119+
// System info collection is only supported on Windows and Darwin
120+
if runtime.GOOS != "windows" && runtime.GOOS != "darwin" {
121121
hh.log.Debugf("System information collection not supported on %s", runtime.GOOS)
122122
hh.data = nil
123123
return nil

comp/metadata/hostsysteminfo/impl/hostsysteminfo_nix_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/).
44
// Copyright 2016-present Datadog, Inc.
55

6-
//go:build test && !windows
6+
//go:build test && !windows && !darwin
77

88
package hostsysteminfoimpl
99

@@ -16,6 +16,6 @@ import (
1616
func TestNewSystemInfoProvider_EndUserDeviceMode(t *testing.T) {
1717

1818
hh := getTestHostSystemInfo(t, nil)
19-
// Should be disabled for non-Windows platforms
20-
assert.False(t, hh.InventoryPayload.Enabled, "Should not be enabled for non-Windows platforms")
19+
// Should be disabled for non-Windows and non-Darwin platforms
20+
assert.False(t, hh.InventoryPayload.Enabled, "Should not be enabled for non-Windows and non-Darwin platforms")
2121
}

comp/metadata/hostsysteminfo/impl/hostsysteminfo_windows_test.go renamed to comp/metadata/hostsysteminfo/impl/hostsysteminfo_supported_test.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/).
44
// Copyright 2016-present Datadog, Inc.
55

6-
//go:build test && windows
6+
//go:build test && (windows || darwin)
77

88
package hostsysteminfoimpl
99

@@ -88,12 +88,12 @@ func TestPayloadMarshalJSON(t *testing.T) {
8888
Timestamp: time.Now().UnixNano(),
8989
UUID: "test-uuid-12345",
9090
Metadata: &hostSystemInfoMetadata{
91-
Manufacturer: "Lenovo",
92-
ModelNumber: "Thinkpad T14s",
93-
SerialNumber: "ABC123XYZ",
94-
ModelName: "Thinkpad",
95-
ChassisType: "Laptop",
96-
Identifier: "SKU123",
91+
Manufacturer: "Test Manufacturer",
92+
ModelNumber: "Test Model",
93+
SerialNumber: "TEST123",
94+
ModelName: "Test Name",
95+
ChassisType: "Desktop",
96+
Identifier: "ID123",
9797
},
9898
}
9999

@@ -114,12 +114,12 @@ func TestPayloadMarshalJSON(t *testing.T) {
114114

115115
// Verify metadata fields
116116
metadata := result["host_system_info_metadata"].(map[string]interface{})
117-
assert.Equal(t, "Lenovo", metadata["manufacturer"])
118-
assert.Equal(t, "Thinkpad T14s", metadata["model_number"])
119-
assert.Equal(t, "ABC123XYZ", metadata["serial_number"])
120-
assert.Equal(t, "Thinkpad", metadata["model_name"])
121-
assert.Equal(t, "Laptop", metadata["chassis_type"])
122-
assert.Equal(t, "SKU123", metadata["identifier"])
117+
assert.Equal(t, "Test Manufacturer", metadata["manufacturer"])
118+
assert.Equal(t, "Test Model", metadata["model_number"])
119+
assert.Equal(t, "TEST123", metadata["serial_number"])
120+
assert.Equal(t, "Test Name", metadata["model_name"])
121+
assert.Equal(t, "Desktop", metadata["chassis_type"])
122+
assert.Equal(t, "ID123", metadata["identifier"])
123123
}
124124

125125
func TestFillData(t *testing.T) {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016-present Datadog, Inc.
5+
6+
//go:build darwin
7+
8+
package systeminfo
9+
10+
/*
11+
#cgo CFLAGS: -x objective-c -fobjc-arc
12+
#cgo LDFLAGS: -framework Foundation -framework IOKit
13+
14+
#include <stdlib.h>
15+
#include "systeminfo_darwin.h"
16+
*/
17+
import "C"
18+
import (
19+
"strings"
20+
"unsafe"
21+
)
22+
23+
func collect() (*SystemInfo, error) {
24+
cInfo := C.getDeviceInfo()
25+
defer C.free(unsafe.Pointer(cInfo.modelIdentifier))
26+
defer C.free(unsafe.Pointer(cInfo.modelNumber))
27+
defer C.free(unsafe.Pointer(cInfo.productName))
28+
defer C.free(unsafe.Pointer(cInfo.serialNumber))
29+
30+
return &SystemInfo{
31+
Manufacturer: "Apple Inc.",
32+
ModelNumber: C.GoString(cInfo.modelNumber),
33+
SerialNumber: C.GoString(cInfo.serialNumber),
34+
ModelName: C.GoString(cInfo.productName),
35+
ChassisType: getChassisType(C.GoString(cInfo.productName), C.GoString(cInfo.modelIdentifier)),
36+
Identifier: C.GoString(cInfo.modelIdentifier),
37+
}, nil
38+
}
39+
40+
func getChassisType(productName string, modelIdentifier string) string {
41+
lowerName := strings.ToLower(productName)
42+
lowerModel := strings.ToLower(modelIdentifier)
43+
44+
// Check for virtual machines first
45+
// VMware VMs have modelIdentifier like "VMware7,1"
46+
// Apple Silicon VMs have modelIdentifier like "VirtualMac2,1" and productName "Apple Virtual Machine 1"
47+
// Parallels VMs have "Parallels" in the modelIdentifier
48+
if strings.Contains(lowerModel, "vmware") ||
49+
strings.Contains(lowerModel, "virtual") ||
50+
strings.Contains(lowerModel, "parallels") ||
51+
strings.Contains(lowerName, "virtual") {
52+
return "Virtual Machine"
53+
}
54+
55+
if strings.HasPrefix(lowerName, "macbook") {
56+
return "Laptop"
57+
}
58+
59+
if strings.HasPrefix(lowerName, "imac") ||
60+
strings.HasPrefix(lowerName, "mac mini") ||
61+
strings.HasPrefix(lowerName, "mac pro") ||
62+
strings.HasPrefix(lowerName, "mac studio") {
63+
return "Desktop"
64+
}
65+
66+
return "Other"
67+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016-present Datadog, Inc.
5+
6+
//go:build darwin
7+
8+
package systeminfo
9+
10+
import (
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
func TestCollect(t *testing.T) {
18+
info, err := Collect()
19+
require.NoError(t, err, "Collect should not return an error")
20+
require.NotNil(t, info, "Collect should return system info")
21+
22+
// On Darwin, manufacturer should always be Apple Inc.
23+
assert.Equal(t, "Apple Inc.", info.Manufacturer, "Manufacturer should be Apple Inc.")
24+
25+
// Verify that required fields are populated
26+
// Note: We don't test exact values as they depend on the test machine
27+
t.Logf("Collected System Info:")
28+
t.Logf(" Manufacturer: %s", info.Manufacturer)
29+
t.Logf(" ModelNumber: %s", info.ModelNumber)
30+
t.Logf(" SerialNumber: %s", info.SerialNumber)
31+
t.Logf(" ModelName: %s", info.ModelName)
32+
t.Logf(" ChassisType: %s", info.ChassisType)
33+
t.Logf(" Identifier: %s", info.Identifier)
34+
35+
// Chassis type should be one of the expected values
36+
validChassisTypes := []string{"Laptop", "Desktop", "Virtual Machine", "Other"}
37+
assert.Contains(t, validChassisTypes, info.ChassisType, "ChassisType should be a valid type")
38+
}
39+
40+
func TestGetChassisType(t *testing.T) {
41+
tests := []struct {
42+
name string
43+
productName string
44+
modelIdentifier string
45+
expected string
46+
}{
47+
{
48+
name: "MacBook Pro",
49+
productName: "MacBook Pro",
50+
modelIdentifier: "MacBookPro18,1",
51+
expected: "Laptop",
52+
},
53+
{
54+
name: "MacBook Air",
55+
productName: "MacBook Air",
56+
modelIdentifier: "MacBookAir10,1",
57+
expected: "Laptop",
58+
},
59+
{
60+
name: "MacBook (generic)",
61+
productName: "MacBook",
62+
modelIdentifier: "MacBook10,1",
63+
expected: "Laptop",
64+
},
65+
{
66+
name: "iMac",
67+
productName: "iMac",
68+
modelIdentifier: "iMac21,1",
69+
expected: "Desktop",
70+
},
71+
{
72+
name: "Mac mini",
73+
productName: "Mac mini",
74+
modelIdentifier: "Macmini9,1",
75+
expected: "Desktop",
76+
},
77+
{
78+
name: "Mac Pro",
79+
productName: "Mac Pro",
80+
modelIdentifier: "MacPro7,1",
81+
expected: "Desktop",
82+
},
83+
{
84+
name: "Mac Studio",
85+
productName: "Mac Studio",
86+
modelIdentifier: "Mac13,1",
87+
expected: "Desktop",
88+
},
89+
{
90+
name: "VMware VM",
91+
productName: "VMware Virtual Platform",
92+
modelIdentifier: "VMware7,1",
93+
expected: "Virtual Machine",
94+
},
95+
{
96+
name: "VMware VM (mixed case)",
97+
productName: "VMware Virtual Platform",
98+
modelIdentifier: "vmware7,1",
99+
expected: "Virtual Machine",
100+
},
101+
{
102+
name: "Apple Virtual Machine",
103+
productName: "Apple Virtual Machine 1",
104+
modelIdentifier: "VirtualMac2,1",
105+
expected: "Virtual Machine",
106+
},
107+
{
108+
name: "Virtual in model identifier",
109+
productName: "Some Mac",
110+
modelIdentifier: "VirtualMac2,1",
111+
expected: "Virtual Machine",
112+
},
113+
{
114+
name: "Virtual in product name",
115+
productName: "Virtual Device",
116+
modelIdentifier: "Mac1,1",
117+
expected: "Virtual Machine",
118+
},
119+
{
120+
name: "Parallels VM",
121+
productName: "Parallels Virtual Platform",
122+
modelIdentifier: "Parallels-ARM",
123+
expected: "Virtual Machine",
124+
},
125+
{
126+
name: "Parallels in identifier (mixed case)",
127+
productName: "Some Device",
128+
modelIdentifier: "parallels-x86",
129+
expected: "Virtual Machine",
130+
},
131+
{
132+
name: "Unknown Apple device",
133+
productName: "Apple Device",
134+
modelIdentifier: "Unknown1,1",
135+
expected: "Other",
136+
},
137+
{
138+
name: "Empty strings",
139+
productName: "",
140+
modelIdentifier: "",
141+
expected: "Other",
142+
},
143+
}
144+
145+
for _, tt := range tests {
146+
t.Run(tt.name, func(t *testing.T) {
147+
result := getChassisType(tt.productName, tt.modelIdentifier)
148+
assert.Equal(t, tt.expected, result, "getChassisType(%q, %q) = %q, want %q",
149+
tt.productName, tt.modelIdentifier, result, tt.expected)
150+
})
151+
}
152+
}
153+
154+
func TestGetChassisType_CaseInsensitive(t *testing.T) {
155+
// Test that the function handles different cases correctly
156+
testCases := []struct {
157+
name string
158+
productName string
159+
modelIdentifier string
160+
expected string
161+
}{
162+
{"Uppercase MacBook", "MACBOOK PRO", "MacBookPro18,1", "Laptop"},
163+
{"Lowercase macbook", "macbook pro", "MacBookPro18,1", "Laptop"},
164+
{"Mixed case macBook", "macBook Pro", "MacBookPro18,1", "Laptop"},
165+
{"Uppercase iMac", "IMAC", "iMac21,1", "Desktop"},
166+
{"Lowercase imac", "imac", "iMac21,1", "Desktop"},
167+
{"Uppercase VM identifier", "Some Device", "VMWARE7,1", "Virtual Machine"},
168+
{"Lowercase vm identifier", "Some Device", "vmware7,1", "Virtual Machine"},
169+
}
170+
171+
for _, tt := range testCases {
172+
t.Run(tt.name, func(t *testing.T) {
173+
result := getChassisType(tt.productName, tt.modelIdentifier)
174+
assert.Equal(t, tt.expected, result)
175+
})
176+
}
177+
}

pkg/inventory/systeminfo/collector_nix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// This product includes software developed at Datadog (https://www.datadoghq.com/).
44
// Copyright 2016-present Datadog, Inc.
55

6-
//go:build !windows
6+
//go:build !windows && !darwin
77

88
package systeminfo
99

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#ifndef SYSTEMINFO_DARWIN_H
2+
#define SYSTEMINFO_DARWIN_H
3+
4+
#include <stdbool.h>
5+
6+
typedef struct {
7+
char *modelNumber;
8+
char *serialNumber;
9+
char *productName;
10+
char *modelIdentifier;
11+
} DeviceInfo;
12+
13+
DeviceInfo getDeviceInfo(void);
14+
15+
#endif

0 commit comments

Comments
 (0)