Skip to content
Open
584 changes: 359 additions & 225 deletions addons/intel/clock-chain.go

Large diffs are not rendered by default.

196 changes: 196 additions & 0 deletions addons/intel/clock-chain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package intel

import (
"os"
"testing"

"github.com/stretchr/testify/assert"
)

func Test_ProcessProfileTbcClockChain(t *testing.T) {
_, restoreDPLLPins := setupMockDPLLPinsFromJSON("./testdata/dpll-pins.json")
defer restoreDPLLPins()
restoreDelay := setupMockDelayCompensation()
defer restoreDelay()
mockPinSet, restorePinSet := setupBatchPinSetMock()
defer restorePinSet()
// Setup filesystem mock for TBC profile (3 devices with pins)
mockFS, restoreFs := setupMockFS()
defer restoreFs()
phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}}

// EnableE810Outputs is called for the leading NIC (ens4f0) - needs specific paths
mockFS.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil)
mockFS.AllowReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist)
// mock applyPinSet does not hit the filesystem; only the period write from EnableE810Outputs is real.
mockFS.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/period", []byte("2 0 0 1 0"), os.FileMode(0o666), nil)

mockPinConfig, restorePins := setupMockPinConfig()
defer restorePins()

// Can read test profile
profile, err := loadProfile("./testdata/profile-tbc.yaml")
assert.NoError(t, err)

mockClockIDsFromProfile(mockFS, profile)

// Can run PTP config change handler without errors
p, d := E810("e810")
err = p.OnPTPConfigChange(d, profile)
assert.NoError(t, err)
ccData := clockChain.(*ClockChain)
assert.Equal(t, ClockTypeTBC, ccData.Type, "identified a wrong clock type")
assert.Equal(t, uint64(5799633565432596414), ccData.LeadingNIC.DpllClockID, "identified a wrong clock ID ")
assert.Equal(t, "ens4f1", ccData.LeadingNIC.UpstreamPort, "wrong upstream port")
assert.Equal(t, 1, mockPinConfig.actualPinSetCount, "SDP22 sysfs channel assignment for 1PPS")
assert.Equal(t, 0, mockPinConfig.actualPinFrqCount)
assert.NotNil(t, mockPinSet.commands, "DPLL commands should have been issued")
assert.Greater(t, len(mockPinSet.commands), 0, "should have DPLL pin commands")

// Test holdover entry
mockPinSet.reset()
err = clockChain.EnterHoldoverTBC()
assert.NoError(t, err)
assert.Equal(t, 2, len(mockPinSet.commands))

// Test holdover exit
mockPinSet.reset()
err = clockChain.EnterNormalTBC()
assert.NoError(t, err)
assert.Equal(t, 2, len(mockPinSet.commands))

// Ensure switching back to TGM resets any pins
mockPinSet.reset()
tgmProfile, err := loadProfile("./testdata/profile-tgm.yaml")
assert.NoError(t, err)
err = OnPTPConfigChangeE810(nil, tgmProfile)
assert.NoError(t, err)
assert.NotNil(t, mockPinSet.commands, "Ensure clockChain.SetPinDefaults was called")
}

func Test_ProcessProfileTtscClockChain(t *testing.T) {
_, restoreDPLLPins := setupMockDPLLPinsFromJSON("./testdata/dpll-pins.json")
defer restoreDPLLPins()
restoreDelay := setupMockDelayCompensation()
defer restoreDelay()
mockPinSet, restorePinSet := setupBatchPinSetMock()
defer restorePinSet()
// Setup filesystem mock for T-TSC profile (1 device with pins)
mockFS, restoreFs := setupMockFS()
defer restoreFs()
phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}}

// EnableE810Outputs is called for the leading NIC (ens4f0) - needs specific paths
mockFS.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil)
mockFS.AllowReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist)
mockFS.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/period", []byte("2 0 0 1 0"), os.FileMode(0o666), nil)

mockPinConfig, restorePins := setupMockPinConfig()
defer restorePins()

// Can read test profile
profile, err := loadProfile("./testdata/profile-t-tsc.yaml")
assert.NoError(t, err)

mockClockIDsFromProfile(mockFS, profile)

// Can run PTP config change handler without errors
p, d := E810("e810")
err = p.OnPTPConfigChange(d, profile)
assert.NoError(t, err)
ccData := clockChain.(*ClockChain)
assert.Equal(t, ClockTypeTBC, ccData.Type, "identified a wrong clock type")
assert.Equal(t, uint64(5799633565432596414), ccData.LeadingNIC.DpllClockID, "identified a wrong clock ID ")
assert.Equal(t, "ens4f1", ccData.LeadingNIC.UpstreamPort, "wrong upstream port")
assert.NotNil(t, mockPinSet.commands, "Ensure some pins were set")
assert.Equal(t, 1, mockPinConfig.actualPinSetCount, "SDP22 sysfs channel assignment for 1PPS")
assert.Equal(t, 0, mockPinConfig.actualPinFrqCount)

// Test holdover entry
mockPinSet.reset()
err = clockChain.EnterHoldoverTBC()
assert.NoError(t, err)
assert.Equal(t, 2, len(mockPinSet.commands))

// Test holdover exit
mockPinSet.reset()
err = clockChain.EnterNormalTBC()
assert.NoError(t, err)
assert.Equal(t, 2, len(mockPinSet.commands))
}

func Test_SetPinDefaults_AllNICs(t *testing.T) {
_, restoreDPLLPins := setupMockDPLLPinsFromJSON("./testdata/dpll-pins.json")
defer restoreDPLLPins()
restoreDelay := setupMockDelayCompensation()
defer restoreDelay()
mockPinSet, restorePinSet := setupBatchPinSetMock()
defer restorePinSet()

mockPinConfig, restorePinConfig := setupMockPinConfig()
defer restorePinConfig()

// Setup filesystem mock for EnableE810Outputs
mockFS, restoreFs := setupMockFS()
defer restoreFs()
phcEntries := []os.DirEntry{MockDirEntry{name: "ptp0", isDir: true}}
mockFS.AllowReadDir("/sys/class/net/ens4f0/device/ptp/", phcEntries, nil)
mockFS.AllowReadFile("/sys/class/net/ens4f0/device/ptp/ptp0/pins/SMA1", nil, os.ErrNotExist)
mockFS.ExpectWriteFile("/sys/class/net/ens4f0/device/ptp/ptp0/period", []byte("2 0 0 1 0"), os.FileMode(0o666), nil)

// Load a profile with multiple NICs (leading + other NICs)
profile, err := loadProfile("./testdata/profile-tbc.yaml")
assert.NoError(t, err)

mockClockIDsFromProfile(mockFS, profile)

// Initialize the clock chain with multiple NICs
err = OnPTPConfigChangeE810(nil, profile)
assert.NoError(t, err)
assert.Equal(t, 1, mockPinConfig.actualPinSetCount, "SDP22 sysfs channel assignment for 1PPS")
assert.Equal(t, 0, mockPinConfig.actualPinFrqCount)

// Verify we have the expected clock chain structure
ccData := clockChain.(*ClockChain)
assert.Equal(t, ClockTypeTBC, ccData.Type)
assert.Equal(t, "ens4f0", ccData.LeadingNIC.Name)
assert.Equal(t, 2, len(ccData.OtherNICs), "should have 2 other NICs (ens5f0, ens8f0)")

// Reset to only capture commands from the explicit SetPinDefaults call
mockPinSet.reset()
err = clockChain.SetPinDefaults()
assert.NoError(t, err)
assert.NotNil(t, mockPinSet.commands)

// SetPinDefaults configures 9 different pin types, and we have 3 NICs total
// Each pin type should have a command for each NIC that has that pin
assert.Equal(t, len(mockPinSet.commands), 27, "should have exactly 27 pin commands")

// Verify that commands include pins from multiple clock IDs
clockIDsSeen := make(map[uint64]bool)
pinLabelsSeen := make(map[string]bool)

mockPins := ccData.DpllPins.(*mockedDPLLPins)
for _, cmd := range mockPinSet.commands {
// Find which pin this command refers to by searching all pins
for _, pin := range mockPins.pins {
if pin.ID == cmd.ID {
clockIDsSeen[pin.ClockID] = true
pinLabelsSeen[pin.BoardLabel] = true
break
}
}
}

// We should see commands for multiple clock IDs (multiple NICs)
assert.GreaterOrEqual(t, len(clockIDsSeen), 2, "should have commands for at least 2 different clock IDs")

// We should see commands for the standard configurable pin types
expectedPins := []string{
"GNSS-1PPS", "SMA1", "SMA2/U.FL2", "CVL-SDP20", "CVL-SDP22",
"CVL-SDP21", "CVL-SDP23", "C827_0-RCLKA", "C827_0-RCLKB",
}
for _, expectedPin := range expectedPins {
assert.True(t, pinLabelsSeen[expectedPin], "should have command for pin %s", expectedPin)
}
}
121 changes: 121 additions & 0 deletions addons/intel/clockID.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package intel

import (
"encoding/binary"
"fmt"
"os/exec"
"path/filepath"
"strconv"
"strings"

"github.com/golang/glog"
dpll "github.com/k8snetworkplumbingwg/linuxptp-daemon/pkg/dpll-netlink"
)

const (
pciConfigSpaceSize = 256
pciExtendedCapabilityDsnID = 3
pciExtendedCapabilityNextOffset = 2
pciExtendedCapabilityOffsetShift = 4
pciExtendedCapabilityDataOffset = 4
)

// getClockID returns the DPLL clock ID for a network device.
func getClockID(device string) uint64 {
clockID := getPCIClockID(device)
if clockID != 0 {
return clockID
}
return getDevlinkClockID(device)
}

func getPCIClockID(device string) uint64 {
b, err := filesystem.ReadFile(fmt.Sprintf("/sys/class/net/%s/device/config", device))
if err != nil {
glog.Error(err)
return 0
}
var offset uint16 = pciConfigSpaceSize
var id uint16
for {
if len(b) < int(offset)+pciExtendedCapabilityDataOffset {
glog.Errorf("PCI config space too short (%d bytes) for device %s", len(b), device)
return 0
}
id = binary.LittleEndian.Uint16(b[offset:])
if id != pciExtendedCapabilityDsnID {
if id == 0 {
glog.Errorf("DSN capability not found for device %s", device)
return 0
}
offset = binary.LittleEndian.Uint16(b[offset+pciExtendedCapabilityNextOffset:]) >> pciExtendedCapabilityOffsetShift
continue
}
break
}
return binary.LittleEndian.Uint64(b[offset+pciExtendedCapabilityDataOffset:])
}

func getDevlinkClockID(device string) uint64 {
devicePath, err := filesystem.ReadLink(fmt.Sprintf("/sys/class/net/%s/device", device))
if err != nil {
glog.Errorf("failed to resolve PCI address for %s: %v", device, err)
return 0
}
pciAddr := filepath.Base(devicePath)

out, err := exec.Command("devlink", "dev", "info", "pci/"+pciAddr).Output()
if err != nil {
glog.Errorf("getDevlinkClockID: devlink failed for %s (pci/%s): %v", device, pciAddr, err)
return 0
}

for _, line := range strings.Split(string(out), "\n") {
fields := strings.Fields(strings.TrimSpace(line))
if len(fields) != 2 || fields[0] != "serial_number" {
continue
}
var clockID uint64
// "50-7c-6f-ff-ff-1f-b5-80" -> "507c6fffff1fb580" -> 0x507c6fffff1fb580
clockID, err = strconv.ParseUint(strings.ReplaceAll(fields[1], "-", ""), 16, 64)
if err != nil {
glog.Errorf("getDevlinkClockID: failed to parse serial '%s': %v", fields[1], err)
return 0
}
return clockID
}
glog.Errorf("getDevlinkClockID: serial_number not found in devlink output for %s (pci/%s)", device, pciAddr)
return 0
}

// Using a named anonymous function to allow mocking
var getAllDpllDevices = func() ([]*dpll.DoDeviceGetReply, error) {
conn, err := dpll.Dial(nil)
if err != nil {
return nil, err
}
//nolint:errcheck
defer conn.Close()
return conn.DumpDeviceGet()
}

// getClockIDByModule returns ClockID for a given DPLL module name, preferring PPS type if present
func getClockIDByModule(module string) (uint64, error) {
devices, err := getAllDpllDevices()
if err != nil {
return 0, err
}
var anyID uint64
for _, d := range devices {
if strings.EqualFold(d.ModuleName, module) {
if d.Type == 1 { // PPS
return d.ClockID, nil
}
anyID = d.ClockID
}
}
if anyID != 0 {
return anyID, nil
}
return 0, fmt.Errorf("module %s DPLL not found", module)
}
Loading