Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 95 additions & 9 deletions internal/controller/httpapi/v1/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,10 @@ func flatToNested(d *dto.Device) dto.DeviceExportRecord {
// buildExportME returns the ME subsystem, or nil for devices without a
// Management Engine
func buildExportME(d *dto.Device, info *dto.DeviceInfo) *dto.ExportME {
network := buildExportMENetwork(info.MENetwork)

hasME := info.FWVersion != "" || info.FWSku != "" || info.CurrentMode != "" ||
(info.AMTEnabledInBIOS != nil && *info.AMTEnabledInBIOS)
(info.AMTEnabledInBIOS != nil && *info.AMTEnabledInBIOS) || network != nil
if !hasME {
return nil
}
Expand All @@ -172,8 +174,9 @@ func buildExportME(d *dto.Device, info *dto.DeviceInfo) *dto.ExportME {
UPID: info.UPID,
}

if info.IPAddress != "" {
// export-gap: only the wired ME IP is stored today; wireless has no source field yet, so it stays null.
if network != nil {
me.Network = network
} else if info.IPAddress != "" {
me.Network = &dto.ExportMENetwork{
Wired: &dto.ExportMEInterface{
IPAddress: info.IPAddress,
Expand All @@ -185,16 +188,51 @@ func buildExportME(d *dto.Device, info *dto.DeviceInfo) *dto.ExportME {
return me
}

func buildExportMENetwork(network *dto.MENetworkInfo) *dto.ExportMENetwork {
if network == nil {
return nil
}

exportNetwork := &dto.ExportMENetwork{
Wired: buildExportMEInterface(network.Wired),
Wireless: buildExportMEInterface(network.Wireless),
}
if exportNetwork.Wired == nil && exportNetwork.Wireless == nil {
return nil
}

return exportNetwork
}

func buildExportMEInterface(adapter *dto.MEInterfaceInfo) *dto.ExportMEInterface {
if adapter == nil {
return nil
}

if adapter.IPAddress == "" && adapter.DHCPEnabled == nil && adapter.DHCPMode == "" &&
adapter.LinkStatus == "" && adapter.MACAddress == "" {
return nil
}

exportAdapter := dto.ExportMEInterface(*adapter)

return &exportAdapter
}

// buildExportOS returns the OS subsystem, or nil when no OS data was reported.
func buildExportOS(info *dto.DeviceInfo) *dto.ExportOS {
network := buildExportOSNetwork(info.OSNetwork)

hasOS := info.OSName != "" || info.OSVersion != "" || info.OSDistro != "" ||
info.OSIPAddress != "" || info.LMSInstalled != nil || info.LMSVersion != "" ||
info.MEInterfaceVersion != "" || info.MonitorConnected != nil || info.IEEE8021XEnabled != nil
info.MEInterfaceVersion != "" || info.MonitorConnected != nil || info.IEEE8021XEnabled != nil ||
info.DNSSuffixOS != "" || network != nil
if !hasOS {
return nil
}

osInfo := &dto.ExportOS{
DNSSuffix: info.DNSSuffixOS,
Name: info.OSName,
Version: info.OSVersion,
Distro: info.OSDistro,
Comment thread
sinchubhat marked this conversation as resolved.
Expand All @@ -205,8 +243,9 @@ func buildExportOS(info *dto.DeviceInfo) *dto.ExportOS {
IEEE8021XEnabled: info.IEEE8021XEnabled,
}

if info.OSIPAddress != "" {
// export-gap: only the wired OS IP is stored today; wireless has no source field yet, so it stays null.
if network != nil {
osInfo.Network = network
} else if info.OSIPAddress != "" {
osInfo.Network = &dto.ExportOSNetwork{
Wired: []dto.ExportOSInterface{{IPAddress: info.OSIPAddress}},
}
Expand All @@ -215,16 +254,63 @@ func buildExportOS(info *dto.DeviceInfo) *dto.ExportOS {
return osInfo
}

func buildExportOSNetwork(network *dto.OSNetworkInfo) *dto.ExportOSNetwork {
if network == nil {
return nil
}

wired := make([]dto.ExportOSInterface, 0, len(network.Wired))
for _, adapter := range network.Wired {
if exportAdapter := buildExportOSInterface(&adapter); exportAdapter != nil {
wired = append(wired, *exportAdapter)
}
}

exportNetwork := &dto.ExportOSNetwork{
Wired: wired,
Wireless: buildExportOSInterface(network.Wireless),
}
if len(exportNetwork.Wired) == 0 && exportNetwork.Wireless == nil {
return nil
}

return exportNetwork
}

func buildExportOSInterface(adapter *dto.OSInterfaceInfo) *dto.ExportOSInterface {
if adapter == nil {
return nil
}

if adapter.Name == "" && adapter.IPAddress == "" && adapter.DHCPEnabled == nil &&
adapter.LinkStatus == "" && adapter.MACAddress == "" {
return nil
}

exportAdapter := dto.ExportOSInterface(*adapter)

return &exportAdapter
}

// buildExportPlatform returns the platform subsystem, or nil when no platform
// data was reported.
func buildExportPlatform(info *dto.DeviceInfo) *dto.ExportPlatform {
if info.CPUModel == "" && info.EthernetAdapterCount == nil {
hasAdapters := info.PlatformAdapters != nil &&
(info.PlatformAdapters.Wired != "" || info.PlatformAdapters.Wireless != "")
if info.CPUModel == "" && info.EthernetAdapterCount == nil && !hasAdapters {
return nil
}

// export-gap: adapters has no source fields yet, so it stays null.
return &dto.ExportPlatform{
platform := &dto.ExportPlatform{
CPU: info.CPUModel,
EthernetAdapterCount: info.EthernetAdapterCount,
}
if hasAdapters {
platform.Adapters = &dto.ExportPlatformAdapters{
Wired: info.PlatformAdapters.Wired,
Wireless: info.PlatformAdapters.Wireless,
}
}

return platform
}
160 changes: 160 additions & 0 deletions internal/controller/httpapi/v1/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func TestExportDevices_Success(t *testing.T) {
lastSynced := time.Date(2026, 7, 20, 14, 22, 15, 0, time.UTC)
amtEnabled := true
dhcp := true
osDhcp := false
adapters := 2

meDevice := dto.Device{
Expand All @@ -67,9 +68,32 @@ func TestExportDevices_Success(t *testing.T) {
FirstDiscovered: &firstDiscovered,
LastSynced: &lastSynced,
OSName: "linux",
DNSSuffixOS: "vpro.demo.com",
OSIPAddress: "10.49.76.163",
CPUModel: "Intel(R) Core(TM) Ultra 7 165H",
EthernetAdapterCount: &adapters,
MENetwork: &dto.MENetworkInfo{
Wired: &dto.MEInterfaceInfo{
IPAddress: "10.0.0.12",
DHCPEnabled: &dhcp,
DHCPMode: "active",
LinkStatus: "up",
MACAddress: "AA:BB:CC:DD:EE:10",
},
},
OSNetwork: &dto.OSNetworkInfo{
Wired: []dto.OSInterfaceInfo{{
Name: "eth0",
IPAddress: "10.49.76.163",
DHCPEnabled: &osDhcp,
LinkStatus: "up",
MACAddress: "AA:BB:CC:DD:EE:FF",
}},
},
PlatformAdapters: &dto.PlatformAdaptersInfo{
Wired: "eth0",
Wireless: "wlan0",
},
},
}

Expand Down Expand Up @@ -117,11 +141,23 @@ func TestExportDevices_Success(t *testing.T) {
require.NotNil(t, me.DeviceInfo.ME.MEBXEnabledInBIOS)
require.NotNil(t, me.DeviceInfo.ME.Network)
require.Equal(t, "10.0.0.12", me.DeviceInfo.ME.Network.Wired.IPAddress)
require.Equal(t, "active", me.DeviceInfo.ME.Network.Wired.DHCPMode)
require.Equal(t, "up", me.DeviceInfo.ME.Network.Wired.LinkStatus)
require.Equal(t, "AA:BB:CC:DD:EE:10", me.DeviceInfo.ME.Network.Wired.MACAddress)
require.NotNil(t, me.DeviceInfo.OS)
require.Equal(t, "vpro.demo.com", me.DeviceInfo.OS.DNSSuffix)
require.Equal(t, "linux", me.DeviceInfo.OS.Name)
require.Equal(t, "eth0", me.DeviceInfo.OS.Network.Wired[0].Name)
require.Equal(t, "10.49.76.163", me.DeviceInfo.OS.Network.Wired[0].IPAddress)
require.NotNil(t, me.DeviceInfo.OS.Network.Wired[0].DHCPEnabled)
require.False(t, *me.DeviceInfo.OS.Network.Wired[0].DHCPEnabled)
require.Equal(t, "up", me.DeviceInfo.OS.Network.Wired[0].LinkStatus)
require.Equal(t, "AA:BB:CC:DD:EE:FF", me.DeviceInfo.OS.Network.Wired[0].MACAddress)
require.NotNil(t, me.DeviceInfo.Platform)
require.Equal(t, "Intel(R) Core(TM) Ultra 7 165H", me.DeviceInfo.Platform.CPU)
require.NotNil(t, me.DeviceInfo.Platform.Adapters)
require.Equal(t, "eth0", me.DeviceInfo.Platform.Adapters.Wired)
require.Equal(t, "wlan0", me.DeviceInfo.Platform.Adapters.Wireless)
require.Nil(t, me.DeviceInfo.BMC)

// Non-ME device has a null me subsystem.
Expand All @@ -137,6 +173,130 @@ func TestExportDevices_Success(t *testing.T) {
require.NotContains(t, body, "mebxpassword")
}

func TestBuildExportME_EmptyMENetworkFallsBackToLegacyIPAddress(t *testing.T) {
t.Parallel()

dhcp := true
device := &dto.Device{}
info := &dto.DeviceInfo{
CurrentMode: "Admin",
IPAddress: "10.0.0.12",
DHCPEnabled: &dhcp,
MENetwork: &dto.MENetworkInfo{},
}

me := buildExportME(device, info)

require.NotNil(t, me)
require.NotNil(t, me.Network)
require.NotNil(t, me.Network.Wired)
require.Equal(t, "10.0.0.12", me.Network.Wired.IPAddress)
}

func TestBuildExportME_MENetworkOnlyStillReturnsME(t *testing.T) {
t.Parallel()

device := &dto.Device{}
info := &dto.DeviceInfo{
MENetwork: &dto.MENetworkInfo{
Wired: &dto.MEInterfaceInfo{MACAddress: "90:49:fa:0a:26:a3"},
},
}

me := buildExportME(device, info)

require.NotNil(t, me)
require.NotNil(t, me.Network)
require.NotNil(t, me.Network.Wired)
require.Equal(t, "90:49:fa:0a:26:a3", me.Network.Wired.MACAddress)
}

func TestBuildExportME_EmptyMEInterfaceOnlyReturnsNil(t *testing.T) {
t.Parallel()

device := &dto.Device{}
info := &dto.DeviceInfo{
MENetwork: &dto.MENetworkInfo{Wired: &dto.MEInterfaceInfo{}},
}

me := buildExportME(device, info)

require.Nil(t, me)
}

func TestBuildExportOS_EmptyOSNetworkFallsBackToLegacyIPAddress(t *testing.T) {
t.Parallel()

info := &dto.DeviceInfo{
OSName: "linux",
OSIPAddress: "10.49.76.163",
OSNetwork: &dto.OSNetworkInfo{},
}

os := buildExportOS(info)

require.NotNil(t, os)
require.NotNil(t, os.Network)
require.Len(t, os.Network.Wired, 1)
require.Equal(t, "10.49.76.163", os.Network.Wired[0].IPAddress)
}

func TestBuildExportOS_DNSSuffixOnlyStillReturnsOS(t *testing.T) {
t.Parallel()

info := &dto.DeviceInfo{
DNSSuffixOS: "corp.example.com",
}

os := buildExportOS(info)

require.NotNil(t, os)
require.Equal(t, "corp.example.com", os.DNSSuffix)
}

func TestBuildExportOS_OSNetworkOnlyStillReturnsOS(t *testing.T) {
t.Parallel()

info := &dto.DeviceInfo{
OSNetwork: &dto.OSNetworkInfo{
Wired: []dto.OSInterfaceInfo{{IPAddress: "192.168.1.50"}},
},
}

os := buildExportOS(info)

require.NotNil(t, os)
require.NotNil(t, os.Network)
require.Len(t, os.Network.Wired, 1)
require.Equal(t, "192.168.1.50", os.Network.Wired[0].IPAddress)
}

func TestBuildExportOS_EmptyOSNetworkOnlyReturnsNil(t *testing.T) {
t.Parallel()

info := &dto.DeviceInfo{
OSNetwork: &dto.OSNetworkInfo{},
}

os := buildExportOS(info)

require.Nil(t, os)
}

func TestBuildExportOS_EmptyOSInterfaceOnlyReturnsNil(t *testing.T) {
t.Parallel()

info := &dto.DeviceInfo{
OSNetwork: &dto.OSNetworkInfo{
Wired: []dto.OSInterfaceInfo{{}},
},
}

os := buildExportOS(info)

require.Nil(t, os)
}

func TestExportDevices_DatabaseError(t *testing.T) {
t.Parallel()

Expand Down
35 changes: 35 additions & 0 deletions internal/entity/dto/v1/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,46 @@ type DeviceInfo struct {
OSName string `json:"osName,omitempty"`
OSVersion string `json:"osVersion,omitempty"`
OSDistro string `json:"osDistro,omitempty"`
DNSSuffixOS string `json:"dnsSuffixOS,omitempty"`
CPUModel string `json:"cpuModel,omitempty"`
OSIPAddress string `json:"osIpAddress,omitempty"`
EthernetAdapterCount *int `json:"ethernetAdapterCount,omitempty"`
MonitorConnected *bool `json:"monitorConnected,omitempty"`
IEEE8021XEnabled *bool `json:"ieee8021xEnabled,omitempty"`
MENetwork *MENetworkInfo `json:"meNetwork,omitempty"`
OSNetwork *OSNetworkInfo `json:"osNetwork,omitempty"`
PlatformAdapters *PlatformAdaptersInfo `json:"platformAdapters,omitempty"`
}

type MENetworkInfo struct {
Wired *MEInterfaceInfo `json:"wired,omitempty"`
Wireless *MEInterfaceInfo `json:"wireless,omitempty"`
}

type MEInterfaceInfo struct {
IPAddress string `json:"ipAddress,omitempty"`
DHCPEnabled *bool `json:"dhcpEnabled,omitempty"`
DHCPMode string `json:"dhcpMode,omitempty"`
LinkStatus string `json:"linkStatus,omitempty"`
MACAddress string `json:"macAddress,omitempty"`
}

type OSNetworkInfo struct {
Wired []OSInterfaceInfo `json:"wired,omitempty"`
Wireless *OSInterfaceInfo `json:"wireless,omitempty"`
}

type OSInterfaceInfo struct {
Name string `json:"name,omitempty"`
IPAddress string `json:"ipAddress,omitempty"`
DHCPEnabled *bool `json:"dhcpEnabled,omitempty"`
LinkStatus string `json:"linkStatus,omitempty"`
MACAddress string `json:"macAddress,omitempty"`
}

type PlatformAdaptersInfo struct {
Wired string `json:"wired,omitempty"`
Wireless string `json:"wireless,omitempty"`
}

// UnmarshalJSON implements custom JSON deserialization to support backwards compatibility
Expand Down
Loading
Loading