From d78a1469d48ef1dabda61c7e311957bf08e63ce6 Mon Sep 17 00:00:00 2001 From: Aliyah Hoda Date: Mon, 22 Jun 2026 18:39:51 +0000 Subject: [PATCH 1/2] Support for UMF Interfaces Model - Front Panel Interfaces Signed-off-by: Aliyah Hoda --- config/transformer/models_list | 1 + .../openconfig-interfaces-annot.yang | 34 + .../openconfig-interfaces-deviation.yang | 4 - models/yang/openconfig-p4rt.yang | 69 + translib/Makefile | 6 +- translib/platform/platform.go | 285 ++++ translib/platform/platform_test.go | 388 ++++++ translib/transformer/xfmr_intf.go | 236 +++- translib/transformer/xfmr_intf_test.go | 1167 +++++++++++++++++ translib/transformer/xfmr_path_utils.go | 9 + 10 files changed, 2193 insertions(+), 6 deletions(-) create mode 100644 models/yang/openconfig-p4rt.yang create mode 100644 translib/platform/platform.go create mode 100644 translib/platform/platform_test.go create mode 100644 translib/transformer/xfmr_intf_test.go diff --git a/config/transformer/models_list b/config/transformer/models_list index 32f43e52e..3ff363b74 100644 --- a/config/transformer/models_list +++ b/config/transformer/models_list @@ -8,6 +8,7 @@ openconfig-interfaces-annot.yang openconfig-interfaces.yang openconfig-mclag-annot.yang openconfig-mclag.yang +openconfig-p4rt.yang openconfig-sampling-sflow-annot.yang openconfig-sampling-sflow.yang openconfig-system-annot.yang diff --git a/models/yang/annotations/openconfig-interfaces-annot.yang b/models/yang/annotations/openconfig-interfaces-annot.yang index 4eeee0a3b..14b0729c3 100644 --- a/models/yang/annotations/openconfig-interfaces-annot.yang +++ b/models/yang/annotations/openconfig-interfaces-annot.yang @@ -10,6 +10,9 @@ module openconfig-interfaces-annot { import openconfig-vlan {prefix oc-vlan; } import openconfig-if-ip {prefix oc-ip; } import openconfig-if-aggregate { prefix oc-lag; } + import openconfig-p4rt { prefix "p4rt-if"; } + import openconfig-platform-port { prefix "oc-plat-port"; } + import openconfig-platform-transceiver { prefix "oc-plat-xcvr"; } deviation /oc-intf:interfaces/oc-intf:interface { deviate add { @@ -277,4 +280,35 @@ module openconfig-interfaces-annot { } } + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:config/p4rt-if:id { + deviate add { + sonic-ext:field-transformer "pins_if_id_xfmr"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/p4rt-if:id { + deviate add { + sonic-ext:field-transformer "pins_if_id_xfmr"; + sonic-ext:table-name "P4RT_PORT_ID_TABLE"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-plat-port:hardware-port { + deviate add { + sonic-ext:field-transformer "intf_hardware_port_xfmr"; + sonic-ext:field-name "index"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-plat-xcvr:transceiver { + deviate add { + sonic-ext:field-transformer "intf_transceiver_xfmr"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-plat-xcvr:physical-channel { + deviate add { + sonic-ext:field-transformer "intf_physical_channel_xfmr"; + } + } } diff --git a/models/yang/extensions/openconfig-interfaces-deviation.yang b/models/yang/extensions/openconfig-interfaces-deviation.yang index 097100f66..be50c0f93 100644 --- a/models/yang/extensions/openconfig-interfaces-deviation.yang +++ b/models/yang/extensions/openconfig-interfaces-deviation.yang @@ -77,10 +77,6 @@ module openconfig-interfaces-deviation { deviate not-supported; } - deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-intf:counters/oc-intf:in-fcs-errors { - deviate not-supported; - } - deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-intf:counters/oc-intf:carrier-transitions { deviate not-supported; } diff --git a/models/yang/openconfig-p4rt.yang b/models/yang/openconfig-p4rt.yang new file mode 100644 index 000000000..2b8a4d7ab --- /dev/null +++ b/models/yang/openconfig-p4rt.yang @@ -0,0 +1,69 @@ +module openconfig-p4rt { + yang-version "1"; + + prefix "oc-p4rt"; + + namespace "http://openconfig.net/yang/p4rt"; + + import openconfig-extensions { prefix oc-ext; } + import openconfig-interfaces { prefix oc-if; } + + organization + "OpenConfig Working Group"; + + contact + "www.openconfig.net"; + + description + "This module defines a set of extensions that provide P4Runtime (P4RT) + specific extensions to the OpenConfig data models. Specifically, these + parameters for configuration and state provide extensions that control + the P4RT service, or allow it to be used alongside other OpenConfig + data models. + + The P4RT protocol specification is linked from https://p4.org/specs/ + under the P4Runtime heading."; + + oc-ext:openconfig-version "0.1.0"; + + revision 2021-04-06 { + description + "Initial revision."; + reference "0.1.0"; + } + + grouping p4rt-interface-config { + description + "Interface-specific configuration that is applicable to devices that + are running the P4RT service."; + + leaf id { + type uint32; + description + "The numeric identifier used by the controller to address the interface. + This ID is assigned by an external-to-the-device entity (e.g., an SDN + management system) to establish an externally deterministic numeric + reference for the interface. The programming entity must ensure that + the ID is unique within the required context. + + Note that this identifier is used only when a numeric reference to the + interface is required, it does not replace the unique name assigned to + the interface."; + } + } + + augment "/oc-if:interfaces/oc-if:interface/oc-if:config" { + description + "Add interface-specific intended configuration for P4RT."; + + uses p4rt-interface-config; + } + + augment "/oc-if:interfaces/oc-if:interface/oc-if:state" { + description + "Add interface-specific applied configuration for P4RT."; + + uses p4rt-interface-config; + } +} + diff --git a/translib/Makefile b/translib/Makefile index 7957c0c6c..c6cc5ae5c 100644 --- a/translib/Makefile +++ b/translib/Makefile @@ -25,6 +25,7 @@ TRANSL_DB_ALL_SRCS = $(filter ./db/%, $(SRCS) $(TESTS)) TRANSLIB_TEST_DIR = $(BUILD_DIR)/tests/translib TRANSLIB_TEST_BIN = $(TRANSLIB_TEST_DIR)/translib.test TRANSL_DB_TEST_BIN = $(TRANSLIB_TEST_DIR)/db.test +PLATFORM_TEST_BIN = $(TRANSLIB_TEST_DIR)/platform.test TRANSFORMER_TEST_BIN = $(TRANSLIB_TEST_DIR)/transformer.test TRANSFORMER_ALL_SRCS = $(filter ./transformer/%, $(SRCS) $(TESTS)) @@ -43,7 +44,7 @@ XFMR_TEST_MODELS = $(notdir $(wildcard transformer/test/*.yang)) DEFAULT_TARGETS = $(YGOT_BINDS) $(XFMR_MODELS_LIST) $(FORMAT_CHECK) ifeq ($(NO_TEST_BINS),) -DEFAULT_TARGETS += $(TRANSLIB_TEST_BIN) $(TRANSL_DB_TEST_BIN) $(TRANSFORMER_TEST_APP_BIN) +DEFAULT_TARGETS += $(TRANSLIB_TEST_BIN) $(TRANSL_DB_TEST_BIN) $(TRANSFORMER_TEST_APP_BIN) $(PLATFORM_TEST_BIN) ifdef INCLUDE_TEST_MODELS DEFAULT_TARGETS += $(TRANSFORMER_TEST_BIN) endif @@ -58,6 +59,9 @@ all: $(DEFAULT_TARGETS) $(TRANSLIB_TEST_BIN): $(TRANSLIB_MAIN_SRCS) $(TRANSLIB_TEST_SRCS) $(YGOT_BINDS) $(GO) test -mod=vendor -tags test -cover -coverpkg=../translib,../translib/tlerr -c ../translib -o $@ +$(PLATFORM_TEST_BIN): $(TRANSLIB_MAIN_SRCS) + $(GO) test -mod=vendor -tags test -cover -coverpkg=../translib/platform -c ../translib/platform -o $@ + $(TRANSL_DB_TEST_BIN) : $(TRANSL_DB_ALL_SRCS) $(GO) test -mod=vendor -cover -c ../translib/db -o $@ diff --git a/translib/platform/platform.go b/translib/platform/platform.go new file mode 100644 index 000000000..b8263a9e3 --- /dev/null +++ b/translib/platform/platform.go @@ -0,0 +1,285 @@ +package platform + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + + "github.com/Azure/sonic-mgmt-common/translib/tlerr" + log "github.com/golang/glog" +) + +/* Glossary - Terms like "port" and "interface" are often used interchangeably but + * sometimes to refer to different things. A few terms are defined here for their use + * in this package. + * Lane - single serdes lane with absolute numbering, e.g. 563 + * Lane set - Group of lanes which can be grouped in various ways to create an interface + * Channel - relative numbered serdes lane in a lane set, e.g. 1..8 + * Interface - A logical grouping of lanes with a speed + * Port - A.k.a Physical Port, a physical connector on the switch, e.g. OSFP cage w/ connector + */ +const ( + PLATFORM_JSON = "/usr/share/sonic/hwsku/" + PLATFORM_PLATFORM_JSON = "/usr/share/sonic/platform/" +) + +type platformIntf struct { + index int + isPrimary bool + name string + alias string + lanes []int // For a given interface group, each interface has a slice of the same array + primary *platformIntf + channelOffset uint16 // (First Lane) % (lane set size); used to derive channel index from lane +} + +type platformConfig struct { + intfs map[string]*platformIntf + intfNameToPortName map[string]string + portNameToIntfName map[string]string +} + +var ( + PLATFORM_JSON_PATH string + platformCfg platformConfig + initErr error + once sync.Once +) + +func init() { + searchBases := []string{PLATFORM_JSON, PLATFORM_PLATFORM_JSON} + + for _, baseDir := range searchBases { + asicFile := filepath.Join(baseDir, "platform_asic") + content, err := os.ReadFile(asicFile) + if err != nil { + log.Infof("Platform init: platform_asic not found in %s. Trying next..", baseDir) + continue + } + + var candidatePath string + platform := strings.TrimSpace(string(content)) + + if platform == "alpine_vs" || platform == "vs" { + candidatePath = filepath.Join(baseDir, platform, "platform.json") + + } else { + candidatePath = filepath.Join(baseDir, "platform.json") + } + + if _, err := os.Stat(candidatePath); err == nil { + PLATFORM_JSON_PATH = candidatePath + log.Infof("Platform init: Found platform.json at %s\n", PLATFORM_JSON_PATH) + return + } + } + + if PLATFORM_JSON_PATH == "" { + log.Infof("Platform init: platform.json not found.") + } +} + +/* Lazy Initialization */ +func ensureLoaded() error { + once.Do(func() { + if PLATFORM_JSON_PATH == "" { + initErr = fmt.Errorf("platform.json path not found during init") + return + } + initErr = doParsePlatformJson(PLATFORM_JSON_PATH) + }) + return initErr +} + +func doParsePlatformJson(filename string) error { + file, err := os.ReadFile(filename) + if err != nil { + if log.V(3) { + log.Infof("Error reading platform json, file %s, err %v", filename, err) + } + return err + } + + var parsedJson map[string]any + if err := json.Unmarshal(file, &parsedJson); err != nil { + if log.V(3) { + log.Infof("Error parsing platform json, file %s, err %v", filename, err) + } + return err + } + + /* Perform an initial pass over the json to create the platformIntfs and + * populate most fields. */ + platformCfg = platformConfig{} + platformCfg.intfs = map[string]*platformIntf{} + intfsJson, ok := parsedJson["interfaces"].(map[string]any) + if !ok { + return tlerr.InvalidArgs("Failed type assertion, 'interfaces' is missing or not a JSON object") + } + + log.Infof("Found %d interface entries in platform json", len(intfsJson)) + for intfName, v := range intfsJson { + intfJson, ok := v.(map[string]any) + if !ok { + return tlerr.InvalidArgs("Failed type assertion, interface entry '%s' is not a JSON object", intfName) + } + + indexRaw, exists := intfJson["index"] + if !exists { + return tlerr.InvalidArgs("Field 'index' missing in interface '%s'", intfName) + } + indexJson, ok := indexRaw.(string) + if !ok { + return tlerr.InvalidArgs("Field 'index' in interface '%s' must be a string", intfName) + } + + lanesJson, _ := intfJson["lanes"].(string) + + /* Primary interface has a comma seperated list of all indexes, otherwise a single index. */ + indexes := strings.Split(indexJson, ",") + if len(indexes) == 1 && indexes[0] == "" { + if log.V(3) { + log.Infof("Platform json parsing error %s:index=\"%s\" (missing)", intfName, indexJson) + } + return tlerr.New("Platform json parsing error %s:index=\"%s\" (missing)", intfName, indexJson) + } + index, err := strconv.Atoi(indexes[0]) + if err != nil { + if log.V(3) { + log.Infof("Platform json parsing error %s:index=\"%s\", err %v", intfName, indexJson, err) + } + return err + } + + /* Only the primary interface will have a comma seperated list of lanes. */ + var lanesInt []int = nil + if len(lanesJson) > 0 { + lanesArr := strings.Split(lanesJson, ",") + lanesInt = make([]int, len(lanesArr)) + for i, l := range lanesArr { + lane, err := strconv.Atoi(l) + if err != nil { + if log.V(3) { + log.Infof("Platform json parsing error %s:lanes=\"%s\", err %v", intfName, lanesJson, err) + } + return err + } + lanesInt[i] = lane + } + } + /* Primary interface must have the lane set. */ + isPrimary := len(lanesJson) > 0 + + platformCfg.intfs[intfName] = &platformIntf{ + name: intfName, + index: index, + lanes: lanesInt, + isPrimary: isPrimary, + } + log.Infof("Added %s platform interface", intfName) + } + log.Infof("Created %d platform interfaces from platform json", len(platformCfg.intfs)) + + /* Perform a second pass linking interfaces in the same group and adding aliases. */ + for _, intf := range platformCfg.intfs { + if intf.isPrimary { + intf.primary = intf + } else { + for _, pIntf := range platformCfg.intfs { + /* Skip non-primary interfaces. Skip primary interfaces for other indexes. */ + if !pIntf.isPrimary || intf.index != pIntf.index { + continue + } + intf.primary = pIntf + break + } + } + if intf.primary == nil { + if log.V(3) { + log.Infof("Platform json parsing error %s: no primary", intf.name) + } + return tlerr.New("Platform json parsing error %s: no primary", intf.name) + } + + mJsonAny, ok := intfsJson[intf.primary.name] + if !ok { + if log.V(3) { + log.Infof("Platform json parsing error %s:primary=%s, no json entry", intf.name, intf.primary.name) + } + return tlerr.New("Platform json parsing error %s:primary=%s, no json entry", intf.name, intf.primary.name) + } + mJson, ok := mJsonAny.(map[string]any) + if !ok { + return tlerr.InvalidArgs("Failed type assertion, data for primary interface '%s' is not a JSON object", intf.primary.name) + } + + aliasAtLanes, _ := mJson["alias_at_lanes"].(string) + aliases := strings.Split(aliasAtLanes, ",") + if len(aliases) == 1 && aliases[0] == "" { + /* No name aliases are defined, this is okay, use the interface name as the + * alias. */ + intf.alias = intf.name + } else if len(aliases) > 0 { + intf.alias = strings.TrimSpace(aliases[0]) + } + } + log.Infof("Updated %d platform interfaces from platform json", len(platformCfg.intfs)) + + platformCfg.intfNameToPortName = make(map[string]string) + platformCfg.portNameToIntfName = make(map[string]string) + for _, pIntf := range platformCfg.intfs { + if !pIntf.isPrimary { + continue + } + intfName := pIntf.name + portName := "1/" + strconv.Itoa(pIntf.index) + platformCfg.intfNameToPortName[intfName] = portName + platformCfg.portNameToIntfName[portName] = intfName + } + calcChannelOffset() + log.Infof("Built port name maps for %d (%d) entries", len(platformCfg.intfNameToPortName), len(platformCfg.portNameToIntfName)) + + return nil +} + +func platIntfByName(intfName string) (platformIntf, error) { + rv, ok := platformCfg.intfs[intfName] + if !ok { + return platformIntf{}, tlerr.InvalidArgs("platformIntf \"%s\" not found", intfName) + } + return *rv, nil +} + +func calcChannelOffset() { + // Derive the channel offset for each primary interface + for _, intf := range platformCfg.intfs { + if !intf.isPrimary { + continue + } + if len(intf.lanes) == 0 { + if log.V(3) { + log.Infof("Primary interface %s has 0 physical lanes configured, skipping offset calculation", intf.name) + } + continue + } + intf.channelOffset = uint16(intf.lanes[0] % len(intf.lanes)) + } +} + +func ChannelOffset(intfName string) (uint16, error) { + if err := ensureLoaded(); err != nil { + if log.V(3) { + log.Infof("Error in loading platform file. err %v", err) + } + return 0, err + } + pIntf, err := platIntfByName(intfName) + if err != nil { + return 0, err + } + return pIntf.channelOffset, nil +} diff --git a/translib/platform/platform_test.go b/translib/platform/platform_test.go new file mode 100644 index 000000000..c1ae50639 --- /dev/null +++ b/translib/platform/platform_test.go @@ -0,0 +1,388 @@ +package platform + +import ( + "fmt" + "os" + "path/filepath" + "sync" + "testing" +) + +var platformCfgBackup platformConfig + +func savePlatformConfig() { + platformCfgBackup = platformCfg +} + +func restorePlatformConfig() { + platformCfg = platformCfgBackup +} + +func resetState() { + once = sync.Once{} + initErr = nil + platformCfg = platformConfig{ + intfs: make(map[string]*platformIntf), + } +} + +func loadTestJson(t *testing.T, jsonStr string) error { + file, err := os.CreateTemp("", "platform_test_tmp-*.json") + if err != nil { + t.Fatalf("Failed to create temporary file: %v", err) + } + defer os.Remove(file.Name()) + _, err = file.WriteString(jsonStr) + if err != nil { + t.Fatalf("Failed to write to temporary file: %v", err) + } + + return doParsePlatformJson(file.Name()) +} + +func TestMissingJsonFile(t *testing.T) { + savePlatformConfig() + defer restorePlatformConfig() + err := doParsePlatformJson("/path/to/nonexistent/file") + if err == nil { + t.Errorf("No error generated for missing platform.json file") + } +} + +func TestMalformedJsonFile(t *testing.T) { + savePlatformConfig() + defer restorePlatformConfig() + // Missing comma creates an invalid json + if err := loadTestJson(t, `{"interfaces": {"eth1":{} "eth2":{} }}`); err == nil { + t.Errorf("No error generated for malformed platform.json file") + } + + // interfaces should be a dictionary + if err := loadTestJson(t, `{"interfaces": {1: 2}}`); err == nil { + t.Errorf("No error generated for malformed platform.json file") + } +} + +func TestMalformedIntfName(t *testing.T) { + savePlatformConfig() + defer restorePlatformConfig() + badJsons := []string{ + // Interface naming is required to be 1/ or 1// + `{"interfaces": {"eth1":{}, "eth2":{} }}`, + // Interface index must be an integer + `{"interfaces": {"ethernetA/B/C":{} }}`, + // Interface subindex must be an integer + `{"interfaces": {"ethernet1/2/C":{} }}`, + } + + for _, badJson := range badJsons { + if err := loadTestJson(t, badJson); err == nil { + t.Errorf("No error generated for malformed interface case: \"%s\"", badJson) + } + } +} + +func TestMalformedIndex(t *testing.T) { + savePlatformConfig() + defer restorePlatformConfig() + badJsons := []string{ + // Index should not be empty + `{"interfaces": {"ethernet1":{ "index":"" } }}`, + `{"interfaces": {"ethernet1":{ "xedni":"a" } }}`, + // Index should be an integer + `{"interfaces": {"ethernet1":{ "index":"NotAnInteger" } }}`, + // Index should match the name + `{"interfaces": {"ethernet1":{ "index":"12345" } }}`, + } + + for _, badJson := range badJsons { + if err := loadTestJson(t, badJson); err == nil { + t.Errorf("No error generated for malformed interface case: \"%s\"", badJson) + } + } +} + +func TestMalformedLanes(t *testing.T) { + savePlatformConfig() + defer restorePlatformConfig() + badJsons := []string{ + // Lanes should be integers + `{"interfaces": {"ethernet1":{ "index":"7", "breakout_modes":"1x400G", "lanes":"a,b,c" } }}`, + // Lanes should be have empty entries + `{"interfaces": {"ethernet1":{ "index":"7", "breakout_modes":"1x400G", "lanes":"1,2,,3" } }}`, + `{"interfaces": {"ethernet1":{ "index":"7", "breakout_modes":"1x400G", "lanes":"1,2,3," } }}`, + `{"interfaces": {"ethernet1":{ "index":"7", "breakout_modes":"1x400G", "lanes":",1,2,3" } }}`, + // Should have enough lanes for all children + `{"interfaces": {"ethernet1":{ "index":"7,7,7", "breakout_modes":"1x400G", "lanes": "12,13", "alias_at_lanes": "foo,bar,baz"}, + "ethernet1":{ "index":"7", "breakout_modes":"1x400G"}, + "ethernet1":{ "index":"7", "breakout_modes":"1x400G"} }}`, + } + + for _, badJson := range badJsons { + if err := loadTestJson(t, badJson); err == nil { + t.Errorf("No error generated for malformed interface case: \"%s\"", badJson) + } + } +} + +func TestMalformedPrimary(t *testing.T) { + savePlatformConfig() + defer restorePlatformConfig() + badJsons := []string{ + // Primary intf missing + `{"interfaces": {"ethernet1":{ "index":"7", "breakout_modes":"1x400G" } }}`, + // Primary intf isn't a primary + `{"interfaces": {"ethernet1":{ "index":"7", "breakout_modes":"1x400G" }, "ethernet1":{ "index":"7", "breakout_modes":"1x400G" }}}`, + } + + for _, badJson := range badJsons { + if err := loadTestJson(t, badJson); err == nil { + t.Errorf("No error generated for malformed interface case: \"%s\"", badJson) + } + } +} + +func TestMalformedAlias(t *testing.T) { + savePlatformConfig() + defer restorePlatformConfig() + badJsons := []string{ + `{"interfaces": {"ethernet100":{ "index":"7", "breakout_modes":"1x400G" } }}`, + } + + for _, badJson := range badJsons { + if err := loadTestJson(t, badJson); err == nil { + t.Errorf("No error generated for malformed interface case: \"%s\"", badJson) + } + } +} + +func TestPlatIntfAlias(t *testing.T) { + savePlatformConfig() + defer restorePlatformConfig() + + platJson := `{ + "interfaces": { + "Ethernet1": { + "index": "1,1,1,1,1,1,1,1", + "lanes": "9,10,11,12,13,14,15,16", + "breakout_modes": "8x100G", + "alias_at_lanes": "Eth1/1, Eth1/2, Eth1/3, Eth1/4, Eth1/5, Eth1/6, Eth1/7, Eth1/8" + } + } + }` + + if err := loadTestJson(t, platJson); err != nil { + t.Fatalf("Error loading test json: %v", err) + } + + for i := 1; i <= 1; i++ { + intf := fmt.Sprintf("Ethernet%d", i) + expectedAlias := fmt.Sprintf("Eth1/%d", i) + pIntf, err := platIntfByName(intf) + if err != nil { + t.Errorf("Unexpected error %v for interface %s", err, intf) + } + if pIntf.alias != expectedAlias { + t.Errorf("Unexpected alias for interface %s (%#v)", intf, pIntf) + } + } +} + +func TestCalcChannelOffset(t *testing.T) { + platformCfg.intfs = make(map[string]*platformIntf) + + intfName := "Ethernet3333" + platformCfg.intfs[intfName] = &platformIntf{ + name: intfName, + isPrimary: true, + lanes: []int{}, + channelOffset: 99, + } + + calcChannelOffset() + + actualOffset := platformCfg.intfs[intfName].channelOffset + if actualOffset != 99 { + t.Errorf("Expected channelOffset to remain 99 for zero-lane interface, but got %d", actualOffset) + } +} + +func resetGlobalState() { + once = sync.Once{} + initErr = nil + platformCfg = platformConfig{ + intfs: make(map[string]*platformIntf), + intfNameToPortName: make(map[string]string), + portNameToIntfName: make(map[string]string), + } +} + +func TestEnsureLoaded(t *testing.T) { + //Create a temporary platform.json file + tmpFile := filepath.Join(t.TempDir(), "platform.json") + + // Ethernet0: lanes 25,26,27,28,29,30,31,32. Offset = 25 % 4 = 1 + // Ethernet4: lanes 33,34,35,36,37,38,39,40. Offset = 29 % 4 = 1 + mockData := `{ + "interfaces": { + "Ethernet0": { + "index": "0,0,0,0,0,0,0,0", + "lanes": "25,26,27,28,29,30,31,32", + "alias_at_lanes": "Eth0" + }, + "Ethernet4": { + "index": "1,1,1,1,1,1,1,1", + "lanes": "33,34,35,36,37,38,39,40", + "alias_at_lanes": "Eth4" + } + } + }` + + if err := os.WriteFile(tmpFile, []byte(mockData), 0644); err != nil { + t.Fatalf("Failed to write mock file: %v", err) + } + + //Override the global path variable + oldPath := PLATFORM_JSON_PATH + PLATFORM_JSON_PATH = tmpFile + defer func() { PLATFORM_JSON_PATH = oldPath }() // Restore after test + + // --- Test Case: ensureLoaded --- + t.Run("TestEnsureLoaded", func(t *testing.T) { + resetGlobalState() // Crucial: forces a fresh execution of once.Do + + err := ensureLoaded() + if err != nil { + t.Fatalf("ensureLoaded returned unexpected error: %v", err) + } + + if len(platformCfg.intfs) != 2 { + t.Errorf("Expected 2 interfaces in map, got %d", len(platformCfg.intfs)) + } + }) + + // --- Test Case: ChannelOffset --- + t.Run("TestChannelOffset", func(t *testing.T) { + resetGlobalState() // Ensure clean state for this calculation + + // Test for Ethernet0 + // Math: lanes[0] (25) % len(lanes) (4) = 1 + expected := uint16(1) + val, err := ChannelOffset("Ethernet0") + + if err != nil { + t.Fatalf("ChannelOffset failed: %v", err) + } + if val != expected { + t.Errorf("Ethernet0 offset mismatch: expected %d, got %d", expected, val) + } + + // Test for Ethernet4 + // Math: lanes[0] (29) % len(lanes) (4) = 1 + val4, err := ChannelOffset("Ethernet4") + if err != nil { + t.Fatalf("ChannelOffset failed for Ethernet4: %v", err) + } + if val4 != expected { + t.Errorf("Ethernet4 offset mismatch: expected %d, got %d", expected, val4) + } + }) +} + +func TestAliasFallbackLogic(t *testing.T) { + savePlatformConfig() + defer restorePlatformConfig() + + // Scenario 1: Field is missing entirely + t.Run("MissingAliasField", func(t *testing.T) { + platJson := `{ + "interfaces": { + "Ethernet100": { + "index": "100", + "lanes": "1" + } + } + }` + if err := loadTestJson(t, platJson); err != nil { + t.Fatalf("Error loading test json: %v", err) + } + + pIntf, err := platIntfByName("Ethernet100") + if err != nil { + t.Fatalf("Interface not found: %v", err) + } + + // Validation: Alias should match the Name + if pIntf.alias != "Ethernet100" { + t.Errorf("Expected alias to fallback to name 'Ethernet100', but got '%s'", pIntf.alias) + } + }) + + // Scenario 2: Field exists but is an empty string + t.Run("EmptyAliasString", func(t *testing.T) { + platJson := `{ + "interfaces": { + "Ethernet200": { + "index": "200", + "lanes": "1", + "alias_at_lanes": "" + } + } + }` + if err := loadTestJson(t, platJson); err != nil { + t.Fatalf("Error loading test json: %v", err) + } + + pIntf, _ := platIntfByName("Ethernet200") + + // Validation: Alias should match the Name + if pIntf.alias != "Ethernet200" { + t.Errorf("Expected alias to fallback to name 'Ethernet200' for empty string, but got '%s'", pIntf.alias) + } + }) +} + +func TestPrimaryLinking(t *testing.T) { + savePlatformConfig() + defer restorePlatformConfig() + + // Ethernet0 is Primary (has lanes) + // Ethernet0_Child is Secondary (missing lanes, but same index "0") + platJson := `{ + "interfaces": { + "Ethernet0": { + "index": "0", + "lanes": "1,2,3,4" + }, + "Ethernet0_Child": { + "index": "0" + } + } + }` + + if err := loadTestJson(t, platJson); err != nil { + t.Fatalf("Error loading test json: %v", err) + } + + // 1. Verify Ethernet0_Child was parsed + child, err := platIntfByName("Ethernet0_Child") + if err != nil { + t.Fatalf("Secondary interface not found: %v", err) + } + + // 2. Verify it is NOT a primary + if child.isPrimary { + t.Error("Ethernet0_Child should not be a primary interface") + } + + // 3. Verify the linking logic (This covers the requested lines) + if child.primary == nil { + t.Fatal("Linking failed: Ethernet0_Child.primary is nil") + } + + if child.primary.name != "Ethernet0" { + t.Errorf("Linking mismatch: Expected primary to be 'Ethernet0', got '%s'", child.primary.name) + } + + t.Logf("SUCCESS: Ethernet0_Child correctly linked to primary %s", child.primary.name) +} diff --git a/translib/transformer/xfmr_intf.go b/translib/transformer/xfmr_intf.go index 560946911..5355b7266 100644 --- a/translib/transformer/xfmr_intf.go +++ b/translib/transformer/xfmr_intf.go @@ -32,6 +32,7 @@ import ( "github.com/Azure/sonic-mgmt-common/translib/db" "github.com/Azure/sonic-mgmt-common/translib/ocbinds" + "github.com/Azure/sonic-mgmt-common/translib/platform" "github.com/Azure/sonic-mgmt-common/translib/tlerr" log "github.com/golang/glog" "github.com/openconfig/ygot/ygot" @@ -55,6 +56,12 @@ func init() { XlateFuncBind("DbToYangPath_intf_eth_port_config_path_xfmr", DbToYangPath_intf_eth_port_config_path_xfmr) XlateFuncBind("DbToYang_intf_eth_auto_neg_xfmr", DbToYang_intf_eth_auto_neg_xfmr) XlateFuncBind("DbToYang_intf_eth_port_speed_xfmr", DbToYang_intf_eth_port_speed_xfmr) + XlateFuncBind("DbToYang_intf_hardware_port_xfmr", DbToYang_intf_hardware_port_xfmr) + XlateFuncBind("DbToYang_intf_transceiver_xfmr", DbToYang_intf_transceiver_xfmr) + XlateFuncBind("DbToYang_intf_physical_channel_xfmr", DbToYang_intf_physical_channel_xfmr) + XlateFuncBind("DbToYangPath_intf_path_xfmr", DbToYangPath_intf_path_xfmr) + XlateFuncBind("YangToDb_pins_if_id_xfmr", YangToDb_pins_if_id_xfmr) + XlateFuncBind("DbToYang_pins_if_id_xfmr", DbToYang_pins_if_id_xfmr) XlateFuncBind("DbToYang_intf_get_counters_xfmr", DbToYang_intf_get_counters_xfmr) XlateFuncBind("DbToYang_intf_get_ether_counters_xfmr", DbToYang_intf_get_ether_counters_xfmr) @@ -114,6 +121,11 @@ const ( VLAN = "Vlan" ) +const ( + HARDWARE_PORT = "hardware-port" + PORT_INDEX = "index" +) + type TblData struct { portTN string memberTN string @@ -1314,7 +1326,7 @@ func getCounters(entry *db.Value, attr string, counter_val **uint64) error { } var portCntList []string = []string{"in-octets", "in-unicast-pkts", "in-broadcast-pkts", "in-multicast-pkts", - "in-errors", "in-discards", "in-pkts", "out-octets", "out-unicast-pkts", + "in-errors", "in-discards", "in-pkts", "in-fcs-errors", "out-octets", "out-unicast-pkts", "out-broadcast-pkts", "out-multicast-pkts", "out-errors", "out-discards", "out-pkts"} var etherCntList []string = []string{"in-oversize-frames", "in-undersize-frames", "in-jabber-frames", "in-fragment-frames", "in-distribution/in-frames-128-255-octets"} @@ -1358,6 +1370,10 @@ func getSpecificCounterAttr(targetUriPath string, entry *db.Value, counter inter e = getCounters(entry, "SAI_PORT_STAT_IF_IN_DISCARDS", &counter_val.InDiscards) return true, e + case "/openconfig-interfaces:interfaces/interface/state/counters/in-fcs-errors": + e = getCounters(entry, "SAI_PORT_STAT_ETHER_STATS_CRC_ALIGN_ERRORS", &counter_val.InFcsErrors) + return true, e + case "/openconfig-interfaces:interfaces/interface/state/counters/in-pkts": var inNonUCastPkt, inUCastPkt *uint64 var in_pkts uint64 @@ -3967,3 +3983,221 @@ var DbToYang_routed_vlan_ip_addr_xfmr SubTreeXfmrDbToYang = func(inParams XfmrPa return err } + +func getDBValues(inParams XfmrParams, tblName string) (db.Value, error) { + if tblName == "" { + return db.Value{Field: map[string]string{}}, errors.New("Invalid inParams or invalid tableName") + } + ifName := keyFromInParamsOrUri(inParams, "name") + prtInst, dbErr := inParams.dbs[inParams.curDb].GetEntry(&db.TableSpec{Name: tblName}, db.Key{Comp: []string{ifName}}) + if dbErr != nil { + return db.Value{Field: map[string]string{}}, dbErr + } + return prtInst, nil +} + +func getPortIndex(inParams XfmrParams, funcName string) (string, error) { + ifName := keyFromInParamsOrUri(inParams, "name") + intfType, _, ierr := getIntfTypeByName(ifName) + if intfType == IntfTypeUnset || ierr != nil { + return "", tlerr.InvalidArgsError{Format: "Invalid interface: " + ifName} + } + if intfType != IntfTypeEthernet { + return "", errors.New("interface type is not IntfTypeEthernet") + } + intTbl, ok := IntfTypeTblMap[intfType] + if !ok { + log.V(3).Infof("%s type not found : %v", funcName, intfType) + return "", errors.New("interface type not found.") + } + tblName, err := getPortTableNameByDBId(intTbl, inParams.curDb) + if err != nil { + log.V(3).Infof("%s table name not found", funcName) + return "", errors.New("table name not found. Err: " + err.Error()) + } + prtInst, dbErr := getDBValues(inParams, tblName) + if dbErr != nil { + return "", dbErr + } + index, ok := prtInst.Field[PORT_INDEX] + if !ok { + return "", errors.New(funcName + " index not found in DB") + } + return index, nil +} + +var sfpTypeToMaxLanesMap = map[string]int{ + "SFP/SFP+/SFP28": 1, + "QSFP": 4, + "QSFP+ or later": 4, + "QSFP28 or later": 4, + "OSFP 8X Pluggable Transceiver": 8, + "QSFP-DD Double Density 8X Pluggable Transceiver": 8, +} + +var DbToYang_intf_hardware_port_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (map[string]interface{}, error) { + result := make(map[string]interface{}) + index, err := getPortIndex(inParams, "DbToYang_intf_hardware_port_xfmr") + if err != nil { + return nil, err + } + result[HARDWARE_PORT] = "1/" + index + return result, nil +} + +var DbToYang_intf_transceiver_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (map[string]interface{}, error) { + index, err := getPortIndex(inParams, "DbToYang_intf_transceiver_xfmr") + if err != nil { + return nil, err + } + return map[string]interface{}{"transceiver": "Ethernet" + index}, nil +} + +var DbToYang_intf_physical_channel_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (map[string]interface{}, error) { + ifName := keyFromInParamsOrUri(inParams, "name") + intfType, _, ierr := getIntfTypeByName(ifName) + if intfType == IntfTypeUnset || ierr != nil { + return nil, tlerr.InvalidArgsError{Format: "Invalid interface: " + ifName} + } + if intfType != IntfTypeEthernet { + return nil, errors.New("interface type is not IntfTypeEthernet") + } + intTbl, ok := IntfTypeTblMap[intfType] + if !ok { + return nil, errors.New("interface type not found.") + } + tblName, err := getPortTableNameByDBId(intTbl, inParams.curDb) + if err != nil { + return nil, errors.New("table name not found. Err: " + err.Error()) + } + prtInst, err := getDBValues(inParams, tblName) + if err != nil { + return nil, err + } + lanes, ok := prtInst.Field["lanes"] + if !ok { + return nil, errors.New("DbToYang_intf_physical_channel_xfmr: lanes not found in DB") + } + + index, err := getPortIndex(inParams, "DbToYang_intf_physical_channel_xfmr") + if err != nil { + return nil, err + } + xcvrName := "Ethernet" + index + stateDB := inParams.dbs[db.StateDB] + xcvrEntry, err := stateDB.GetEntry(&db.TableSpec{Name: "TRANSCEIVER_INFO"}, db.Key{Comp: []string{xcvrName}}) + if err != nil { + return nil, err + } + xcvrType := xcvrEntry.Get("type") + if xcvrType == "" { + return nil, errors.New("DbToYang_intf_physical_channel_xfmr: empty transceiver type for physical-channel") + } + maxLanes, ok := sfpTypeToMaxLanesMap[xcvrType] + if !ok { + return nil, errors.New("DbToYang_intf_physical_channel_xfmr: could not find the max number of lanes for transceiver type " + xcvrType) + } + offset, err := platform.ChannelOffset(ifName) + if err != nil { + log.Infof("DbToYang_intf_physical_channel_xfmr: Error: %v", err) + return nil, errors.New("DbToYang_intf_physical_channel_xfmr: could not find the channel offset for " + ifName) + } + + lanesSplit := strings.Split(lanes, ",") + channels := make([]uint16, 0, len(lanesSplit)) + for _, str := range lanesSplit { + val, err := strconv.ParseUint(str, 10, 16) + if err != nil { + return nil, errors.New("DbToYang_intf_physical_channel_xfmr: err in strconv") + } + channels = append(channels, (uint16(val)-offset)%uint16(maxLanes)) + } + return map[string]interface{}{"physical-channel": channels}, nil +} + +var DbToYangPath_intf_path_xfmr PathXfmrDbToYangFunc = func(inParams XfmrDbToYgPathParams) error { + rootPath := "/openconfig-interfaces:interfaces/interface" + + log.Info("DbToYangPath_intf_path_xfmr: inParams: ", inParams) + + switch len(inParams.tblKeyComp) { + case 1: + inParams.ygPathKeys[rootPath+"/name"] = inParams.tblKeyComp[0] + default: + return fmt.Errorf("Invalid tblKeyCom for intf path xmfr:%v", inParams.tblKeyComp) + } + + log.Info("DbToYangPath_intf_path_xfmr:- params.ygPathKeys: ", inParams.ygPathKeys) + + return nil +} + +var YangToDb_pins_if_id_xfmr FieldXfmrYangToDb = func(inParams XfmrParams) (map[string]string, error) { + pathInfo := NewPathInfo(inParams.uri) + ifName := pathInfo.Var("name") + if ifName == "" { + return nil, errors.New("YangToDb_pins_if_id_xfmr: Interface KEY not present") + } + + intfType, _, ierr := getIntfTypeByName(ifName) + if intfType == IntfTypeUnset || ierr != nil { + return nil, tlerr.InvalidArgsError{Format: "Invalid interface: " + ifName} + } + + if intfType != IntfTypeEthernet { + return nil, errors.New("YangToDb_pins_if_id_xfmr: interface type " + strconv.Itoa(int(intfType)) + " not supported for Config Id.") + } + + idVal, ok := inParams.param.(*uint32) + if !ok { + return nil, tlerr.InvalidArgsError{Format: "YangToDb_pins_if_id_xfmr: Config Id doesn't exist"} + } + log.Info("YangToDb_pins_if_id_xfmr : URI:", inParams.uri, " Id: ", idVal) + resMap := make(map[string]string) + + resMap["id"] = strconv.FormatUint(uint64(*idVal), 10) + return resMap, nil +} + +var DbToYang_pins_if_id_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (map[string]interface{}, error) { + ifName := keyFromInParamsOrUri(inParams, "name") + intfType, _, ierr := getIntfTypeByName(ifName) + if intfType == IntfTypeUnset || ierr != nil { + return nil, tlerr.InvalidArgsError{Format: "Invalid interface: " + ifName} + } + if intfType != IntfTypeEthernet { + return nil, errors.New("DbToYang_pins_if_id_xfmr: interface type " + strconv.Itoa(int(intfType)) + " not supported for Config Id.") + } + + intTbl, ok := IntfTypeTblMap[intfType] + if !ok { + return nil, errors.New("DbToYang_pins_if_id_xfmr: interface type not found " + strconv.Itoa(int(intfType))) + } + + // By default we assume P4RT_PORT_ID_TABLE which is used when reading out state + // for Ethernet and PortChannels. + tblName := "P4RT_PORT_ID_TABLE" + var err error + if inParams.curDb != db.ApplDB { + tblName, err = getPortTableNameByDBId(intTbl, inParams.curDb) + if err != nil { + return nil, errors.New("DbToYang_pins_if_id_xfmr: Port table name not found.") + } + } + + prtInst, dbErr := getDBValues(inParams, tblName) + if dbErr != nil { + return nil, dbErr + } + + resMap := make(map[string]interface{}) + if idStr, ok := prtInst.Field["id"]; ok && idStr != "" { + if idVal, err := strconv.ParseUint(idStr, 10, 32); err == nil { + resMap["id"] = uint32(idVal) + return resMap, nil + } + return nil, err + } + log.Info("DbToYang_pins_if_id_xfmr: Config Id field not found in DB.") + return nil, tlerr.NotFound("config id field not found in DB.") +} diff --git a/translib/transformer/xfmr_intf_test.go b/translib/transformer/xfmr_intf_test.go new file mode 100644 index 000000000..c995aecdc --- /dev/null +++ b/translib/transformer/xfmr_intf_test.go @@ -0,0 +1,1167 @@ +package transformer + +import ( + "github.com/Azure/sonic-mgmt-common/translib/db" + "github.com/Azure/sonic-mgmt-common/translib/ocbinds" + "github.com/openconfig/ygot/ygot" + "reflect" + "strings" + "testing" +) + +func TestInvalidInterfaceType_FieldXfmrDbtoYang(t *testing.T) { + name := "bogusinterfacename" + dummyDbDataMap := make(map[db.DBNum]map[string]map[string]db.Value) + inParams := XfmrParams{ + key: name, + uri: "/interfaces/interface[name=" + name + "]/", + dbDataMap: &dummyDbDataMap, + curDb: 0, + } + tests := []struct { + f FieldXfmrDbtoYang + name string + }{ + {DbToYang_intf_hardware_port_xfmr, "DbToYang_intf_hardware_port_xfmr"}, + {DbToYang_intf_transceiver_xfmr, "DbToYang_intf_transceiver_xfmr"}, + {DbToYang_intf_physical_channel_xfmr, "DbToYang_intf_physical_channel_xfmr"}, + {DbToYang_pins_if_id_xfmr, "DbToYang_pins_if_id_xfmr"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := tt.f(inParams); err == nil { + t.Fatalf("Expected an error when passing invalid interface name") + } + }) + } + + t.Run("DbToYangPath_intf_get_counters_path_xfmr", func(t *testing.T) { + pathParams := XfmrDbToYgPathParams{ + tblName: "COUNTERS_PORT_NAME_MAP", + tblKeyComp: []string{name}, + ygPathKeys: make(map[string]string), + } + + if err := DbToYangPath_intf_get_counters_path_xfmr(pathParams); err != nil { + t.Fatalf("Path transformer failed with error: %v", err) + } + }) +} + +func TestInvalidInterfaceType_SubTreeXfmrDbToYang(t *testing.T) { + name := "bogusinterfacename" + var rootObj ygot.GoStruct = &ocbinds.Device{} + inParams := XfmrParams{ + key: name, + uri: "/openconfig-interfaces:interfaces/interface[name=" + name + "]/state/counters", + ygRoot: &rootObj, + } + tests := []struct { + f SubTreeXfmrDbToYang + name string + }{ + {DbToYang_intf_get_counters_xfmr, "DbToYang_intf_get_counters_xfmr"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.f(inParams); err == nil { + t.Fatalf("Expected an error when passing invalid interface name") + } + }) + } +} + +func TestInvalidInterfaceType_YangToDb(t *testing.T) { + name := "bogusinterfacename" + inParams := XfmrParams{ + key: name, + uri: "/interfaces/interface[name=" + name + "]/", + param: "not nothing", + } + tests := []struct { + f FieldXfmrYangToDb + name string + }{ + {YangToDb_pins_if_id_xfmr, "YangToDb_pins_if_id_xfmr"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := tt.f(inParams); err == nil { + t.Fatalf("Expected an error when passing invalid interface name") + } + }) + } +} + +func TestDbToYang_intf_hardware_port_xfmr(t *testing.T) { + configDb, _ := db.NewDB(db.Options{ + DBNo: db.ConfigDB, + InitIndicator: "CONFIG_DB_INITIALIZED", + TableNameSeparator: "|", + KeySeparator: "|", + }) + defer configDb.DeleteDB() + + configDb.SetEntry(&db.TableSpec{Name: "PORT"}, + db.Key{Comp: []string{"Ethernet55"}}, + db.Value{Field: map[string]string{"index": "55"}}) + + dbs := [db.MaxDB]*db.DB{ + db.ConfigDB: configDb, + } + + tests := []struct { + name string + dbArray [db.MaxDB]*db.DB + expectError bool + expectedValue string + }{ + { + name: "Success_Path", + dbArray: dbs, + expectError: false, + expectedValue: "1/55", + }, + { + name: "Error_Path_Trigger_Missing_DB", + dbArray: [db.MaxDB]*db.DB{}, + expectError: true, + expectedValue: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + inParams := XfmrParams{ + key: "Ethernet55", + curDb: db.ConfigDB, + dbs: tt.dbArray, + uri: "/interfaces/interface[name=Ethernet55]/state/hardware-port", + } + + result, err := DbToYang_intf_hardware_port_xfmr(inParams) + + if tt.expectError { + if err == nil { + t.Errorf("Expected an error but got nil") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if result["hardware-port"] != tt.expectedValue { + t.Errorf("Expected %s, but got %v", tt.expectedValue, result["hardware-port"]) + } + }) + } +} + +func TestDbToYang_intf_transceiver_xfmr(t *testing.T) { + configDb, _ := db.NewDB(db.Options{ + DBNo: db.ConfigDB, + InitIndicator: "CONFIG_DB_INITIALIZED", + TableNameSeparator: "|", + KeySeparator: "|", + }) + defer configDb.DeleteDB() + + configDb.SetEntry(&db.TableSpec{Name: "PORT"}, + db.Key{Comp: []string{"Ethernet1/0/1"}}, + db.Value{Field: map[string]string{"index": "5"}}) + + dbs := [db.MaxDB]*db.DB{ + db.ConfigDB: configDb, + } + + tests := []struct { + name string + dbArray [db.MaxDB]*db.DB + expectError bool + expectedValue string + }{ + { + name: "Success_Path", + dbArray: dbs, + expectError: false, + expectedValue: "Ethernet5", + }, + { + name: "Error_Path_Trigger_Missing_DB", + dbArray: [db.MaxDB]*db.DB{}, + expectError: true, + expectedValue: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + inParams := XfmrParams{ + key: "Ethernet1/0/1", + curDb: db.ConfigDB, + dbs: tt.dbArray, + uri: "/interfaces/interface[name=Ethernet1/0/1]/state/transceiver", + } + + result, err := DbToYang_intf_transceiver_xfmr(inParams) + + if tt.expectError { + if err == nil { + t.Errorf("Expected an error but got nil") + } + if result != nil { + t.Errorf("Expected result map to be nil on failure, but got: %v", result) + } + return + } + + if err != nil { + t.Errorf("Unexpected Error: %v", err) + } + if result["transceiver"] != tt.expectedValue { + t.Errorf("Expected %s, but got %v", tt.expectedValue, result["transceiver"]) + } + }) + } +} + +func TestDbToYangPath_intf_path_xfmr(t *testing.T) { + rootPath := "/openconfig-interfaces:interfaces/interface" + + tests := []struct { + name string + tblKeyComp []string + expectError bool + errorMsg string + expectKeys map[string]string + }{ + { + name: "Success - Valid Single Component Key (Ethernet202)", + tblKeyComp: []string{"Ethernet202"}, + expectError: false, + expectKeys: map[string]string{ + rootPath + "/name": "Ethernet202", + }, + }, + { + name: "Success - Valid Single Component Key (PortChannel10)", + tblKeyComp: []string{"PortChannel10"}, + expectError: false, + expectKeys: map[string]string{ + rootPath + "/name": "PortChannel10", + }, + }, + { + name: "Failure - Empty Table Key Components List", + tblKeyComp: []string{}, + expectError: true, + errorMsg: "Invalid tblKeyCom for intf path xmfr:", + }, + { + name: "Failure - Multi Component Composite Key", + tblKeyComp: []string{"Ethernet202", "Subinterface1"}, + expectError: true, + errorMsg: "Invalid tblKeyCom for intf path xmfr:", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ygPathKeysMap := make(map[string]string) + + inParams := XfmrDbToYgPathParams{ + tblKeyComp: tt.tblKeyComp, + ygPathKeys: ygPathKeysMap, + } + + err := DbToYangPath_intf_path_xfmr(inParams) + + if tt.expectError { + if err == nil { + t.Fatalf("Expected an error from the path transformer but got success") + } + if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Error message verification failed.\nExpected containing: %q\nGot actual error: %q", tt.errorMsg, err.Error()) + } + return + } + + if err != nil { + t.Fatalf("Unexpected error returned from path transformer: %v", err) + } + + if len(inParams.ygPathKeys) != len(tt.expectKeys) { + t.Fatalf("Output path map size mismatch. Expected %d entries, got %d", len(tt.expectKeys), len(inParams.ygPathKeys)) + } + + for expectedKey, expectedVal := range tt.expectKeys { + actualVal, exists := inParams.ygPathKeys[expectedKey] + if !exists { + t.Errorf("Expected path key %q was not found in the output ygPathKeys map", expectedKey) + continue + } + if actualVal != expectedVal { + t.Errorf("Value mismatch for path key %q.\nExpected mapped value: %q\nGot actual value: %q", expectedKey, expectedVal, actualVal) + } + } + }) + } +} + +func TestDbToYang_pins_if_id_xfmr(t *testing.T) { + configDb, _ := db.NewDB(db.Options{ + DBNo: db.ConfigDB, + InitIndicator: "CONFIG_DB_INITIALIZED", + TableNameSeparator: "|", + KeySeparator: "|", + }) + defer configDb.DeleteDB() + + dbs := [db.MaxDB]*db.DB{ + db.ConfigDB: configDb, + } + + applDb, _ := db.NewDB(db.Options{ + DBNo: db.ApplDB, + InitIndicator: "CONFIG_DB_INITIALIZED", + TableNameSeparator: ":", + KeySeparator: ":", + }) + defer applDb.DeleteDB() + + dbs2 := [db.MaxDB]*db.DB{ + db.ApplDB: applDb, + } + + stateDb, _ := db.NewDB(db.Options{ + DBNo: db.StateDB, + InitIndicator: "CONFIG_DB_INITIALIZED", + TableNameSeparator: ":", + KeySeparator: ":", + }) + defer stateDb.DeleteDB() + + dbs3 := [db.MaxDB]*db.DB{ + db.StateDB: stateDb, + } + + tests := []struct { + name string + setupMock func() + inParams XfmrParams + expectError bool + errorMsg string + expectMap map[string]interface{} + }{ + { + name: "Failure - Invalid Interface Identifier", + setupMock: func() {}, + inParams: XfmrParams{ + uri: "/interfaces/interface[name=InvalidIntf0]/config/id", + curDb: db.ConfigDB, + dbs: dbs, + }, + expectError: true, + errorMsg: "Invalid interface:", + }, + { + name: "Failure - Invalid ID Value String in DB", + setupMock: func() { + configDb.SetEntry(&db.TableSpec{Name: "P4RT_PORT_ID_TABLE"}, + db.Key{Comp: []string{"Vlan100"}}, + db.Value{Field: map[string]string{"id": "200"}}) + }, + inParams: XfmrParams{ + uri: "/interfaces/interface[name=Vlan100]/config/id", + curDb: db.ConfigDB, + dbs: dbs, + }, + expectError: true, + errorMsg: "not supported for Config Id", + }, + { + name: "Failure - ID Field Missing from DB Entry", + setupMock: func() { + configDb.SetEntry(&db.TableSpec{Name: "PORT"}, + db.Key{Comp: []string{"Ethernet405"}}, + db.Value{Field: map[string]string{"id": ""}}) + }, + inParams: XfmrParams{ + uri: "/interfaces/interface[name=Ethernet405]/config/id", + curDb: db.ConfigDB, + dbs: dbs, + }, + expectError: true, + errorMsg: "config id field not found in DB.", + }, + { + name: "Failure - DB Connection Error", + setupMock: func() {}, + inParams: XfmrParams{ + uri: "/interfaces/interface[name=Ethernet4]/config/id", + curDb: db.ErrorDB, + dbs: dbs, + }, + expectError: true, + errorMsg: "connection closed", + }, + { + name: "Success - Valid ID in ConfigDB", + setupMock: func() { + configDb.SetEntry(&db.TableSpec{Name: "PORT"}, + db.Key{Comp: []string{"Ethernet201"}}, + db.Value{Field: map[string]string{"id": "201"}}) + }, + inParams: XfmrParams{ + uri: "/interfaces/interface[name=Ethernet201]/config/id", + curDb: db.ConfigDB, + dbs: dbs, + }, + expectError: false, + expectMap: map[string]interface{}{"id": uint32(201)}, + }, + { + name: "Success - Valid ID in ApplDB", + setupMock: func() { + applDb.SetEntry(&db.TableSpec{Name: "P4RT_PORT_ID_TABLE"}, + db.Key{Comp: []string{"Ethernet202"}}, + db.Value{Field: map[string]string{"id": "202"}}) + }, + inParams: XfmrParams{ + uri: "/interfaces/interface[name=Ethernet202]/config/id", + curDb: db.ApplDB, + dbs: dbs2, + }, + expectError: false, + expectMap: map[string]interface{}{"id": uint32(202)}, + }, + { + name: "Success - Valid ID in StateDB", + setupMock: func() { + stateDb.SetEntry(&db.TableSpec{Name: "PORT_TABLE"}, + db.Key{Comp: []string{"Ethernet203"}}, + db.Value{Field: map[string]string{"id": "203"}}) + }, + inParams: XfmrParams{ + uri: "/interfaces/interface[name=Ethernet203]/state/id", + curDb: db.StateDB, + dbs: dbs3, + }, + expectError: false, + expectMap: map[string]interface{}{"id": uint32(203)}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setupMock() + + resMap, err := DbToYang_pins_if_id_xfmr(tt.inParams) + + if tt.expectError { + if err == nil { + t.Fatalf("Expected an error but function returned success") + } + if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Error string mismatch.\nExpected containing: %q\nGot: %q", tt.errorMsg, err.Error()) + } + return + } + + if err != nil { + t.Fatalf("Unexpected error returned: %v", err) + } + + if len(resMap) != len(tt.expectMap) { + t.Fatalf("Result map size mismatch. Expected %d keys, got %d", len(tt.expectMap), len(resMap)) + } + + for k, expectedVal := range tt.expectMap { + actualVal, exists := resMap[k] + if !exists { + t.Errorf("Expected key %q missing from output map", k) + continue + } + if actualVal != expectedVal { + t.Errorf("Value mismatch for key %q.\nExpected (%T): %v\nGot (%T): %v", k, expectedVal, expectedVal, actualVal, actualVal) + } + } + }) + } + + configDb.DeleteEntry(&db.TableSpec{Name: "P4RT_PORT_ID_TABLE"}, db.Key{Comp: []string{"Ethernet0"}}) + configDb.DeleteEntry(&db.TableSpec{Name: "P4RT_PORT_ID_TABLE"}, db.Key{Comp: []string{"Vlan100"}}) + configDb.DeleteEntry(&db.TableSpec{Name: "PORT"}, db.Key{Comp: []string{"Ethernet405"}}) + configDb.DeleteEntry(&db.TableSpec{Name: "PORT"}, db.Key{Comp: []string{"Ethernet201"}}) + applDb.DeleteEntry(&db.TableSpec{Name: "P4RT_PORT_ID_TABLE"}, db.Key{Comp: []string{"Ethernet202"}}) + stateDb.DeleteEntry(&db.TableSpec{Name: "PORT_TABLE"}, db.Key{Comp: []string{"Ethernet203"}}) +} + +func TestYangToDb_pins_if_id_xfmr(t *testing.T) { + validID := uint32(100) + zeroID := uint32(0) + invalidTypeParam := "not-a-uint32-pointer" + + tests := []struct { + name string + uri string + param interface{} + setupMock func() + expectError bool + errorMsg string + expectMap map[string]string + }{ + { + name: "Success - Valid Ethernet ID Parsed to String", + uri: "/interfaces/interface[name=Ethernet202]/config/id", + param: &validID, + setupMock: func() {}, + expectError: false, + expectMap: map[string]string{"id": "100"}, + }, + { + name: "Success - Edge Case Zero ID Parsed to String", + uri: "/interfaces/interface[name=Ethernet4]/config/id", + param: &zeroID, + setupMock: func() {}, + expectError: false, + expectMap: map[string]string{"id": "0"}, + }, + { + name: "Failure - Interface KEY Not Present in URI", + uri: "/interfaces/interface/config/id", + param: &validID, + setupMock: func() {}, + expectError: true, + errorMsg: "Interface KEY not present", + }, + { + name: "Failure - Invalid Interface Identifier Syntax", + uri: "/interfaces/interface[name=InvalidIntfName0]/config/id", + param: &validID, + setupMock: func() {}, + expectError: true, + errorMsg: "Invalid interface:", + }, + { + name: "Failure - Unsupported Interface Type (Vlan)", + uri: "/interfaces/interface[name=Vlan100]/config/id", + param: &validID, + setupMock: func() {}, + expectError: true, + errorMsg: "not supported for Config Id", + }, + { + name: "Failure - Param is Nil Pointer", + uri: "/interfaces/interface[name=Ethernet202]/config/id", + param: nil, + setupMock: func() {}, + expectError: true, + errorMsg: "Config Id doesn't exist", + }, + { + name: "Failure - Param Type Type-Assertion Mismatch", + uri: "/interfaces/interface[name=Ethernet202]/config/id", + param: &invalidTypeParam, + setupMock: func() {}, + expectError: true, + errorMsg: "Config Id doesn't exist", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setupMock() + + inParams := XfmrParams{ + uri: tt.uri, + param: tt.param, + } + + resMap, err := YangToDb_pins_if_id_xfmr(inParams) + + if tt.expectError { + if err == nil { + t.Fatalf("Expected an error but function returned execution success") + } + if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Error message text mismatch.\nExpected containing: %q\nGot actual: %q", tt.errorMsg, err.Error()) + } + return + } + + if err != nil { + t.Fatalf("Unexpected error returned: %v", err) + } + + if len(resMap) != len(tt.expectMap) { + t.Fatalf("Result map size mismatch. Expected %d keys, got %d", len(tt.expectMap), len(resMap)) + } + + for k, expectedVal := range tt.expectMap { + actualVal, exists := resMap[k] + if !exists { + t.Errorf("Expected database map key %q missing from transformer output", k) + continue + } + if actualVal != expectedVal { + t.Errorf("Value mismatch for key %q.\nExpected: %q\nGot: %q", k, expectedVal, actualVal) + } + } + }) + } +} + +func TestGetPortIndex(t *testing.T) { + configDb, _ := db.NewDB(db.Options{ + DBNo: db.ConfigDB, + InitIndicator: "CONFIG_DB_INITIALIZED", + TableNameSeparator: "|", + KeySeparator: "|", + }) + defer configDb.DeleteDB() + + dbs := [db.MaxDB]*db.DB{ + db.ConfigDB: configDb, + } + errDb, _ := db.NewDB(db.Options{ + DBNo: db.ErrorDB, + InitIndicator: "ERROR_DB_INITIALIZED", + TableNameSeparator: "|", + KeySeparator: "|", + }) + defer errDb.DeleteDB() + + dbs1 := [db.MaxDB]*db.DB{ + db.ErrorDB: errDb, + } + funcName := "DbToYang_intf_hardware_port_xfmr" + + t.Run("Success - Valid Path", func(t *testing.T) { + configDb.SetEntry(&db.TableSpec{Name: "PORT"}, + db.Key{Comp: []string{"Ethernet202"}}, + db.Value{Field: map[string]string{"index": "0"}}) + + params := XfmrParams{ + uri: "/interfaces/interface[name=Ethernet202]", + curDb: db.ConfigDB, + dbs: dbs, + } + + index, err := getPortIndex(params, funcName) + + if err != nil { + t.Fatalf("Expected success, got error: %v", err) + } + if index != "0" { + t.Errorf("Expected index '0', got '%s'", index) + } + }) + + t.Run("Error 1 - Invalid Interface Name Syntax", func(t *testing.T) { + params := XfmrParams{ + uri: "/interfaces/interface[name=InvalidName123]", + curDb: db.ConfigDB, + dbs: dbs, + } + + _, err := getPortIndex(params, funcName) + if err == nil || !strings.Contains(err.Error(), "Invalid interface:") { + t.Errorf("Expected 'Invalid interface' error, got: %v", err) + } + }) + + t.Run("Error 2 - Not IntfTypeEthernet", func(t *testing.T) { + params := XfmrParams{uri: "/interfaces/interface[name=PortChannel1]", curDb: db.ConfigDB, dbs: dbs} + _, err := getPortIndex(params, funcName) + + if err == nil || !strings.Contains(err.Error(), "interface type is not IntfTypeEthernet") { + t.Errorf("Expected type mismatch error, got: %v", err) + } + }) + + t.Run("Error 3 - Entry does not Exist", func(t *testing.T) { + params := XfmrParams{uri: "/interfaces/interface[name=Ethernet101]", curDb: db.ErrorDB, dbs: dbs1} + _, err := getPortIndex(params, funcName) + + if err == nil || !strings.Contains(err.Error(), "Entry does not exist") { + t.Errorf("Expected type map missing error, got: %v", err) + } + }) + + t.Run("Error 4 - DB Read Error or Entry Missing", func(t *testing.T) { + + params := XfmrParams{uri: "/interfaces/interface[name=Ethernet888]", curDb: db.ConfigDB, dbs: dbs} + _, err := getPortIndex(params, funcName) + + if err == nil { + t.Error("Expected DB read error, got nil") + } + }) + + t.Run("Error 5 - Index Field Missing in DB Entry", func(t *testing.T) { + configDb.SetEntry(&db.TableSpec{Name: "PORT"}, + db.Key{Comp: []string{"Ethernet66"}}, + db.Value{Field: map[string]string{"lanes": "16"}}) + + params := XfmrParams{uri: "/interfaces/interface[name=Ethernet66]", curDb: db.ConfigDB, dbs: dbs} + _, err := getPortIndex(params, funcName) + + expectedMsg := funcName + " index not found in DB" + if err == nil || !strings.Contains(err.Error(), expectedMsg) { + t.Errorf("Expected %q error, got: %v", expectedMsg, err) + } + }) +} + +func TestDbToYang_intf_get_counters_xfmr(t *testing.T) { + configDb, _ := db.NewDB(db.Options{DBNo: db.ConfigDB, TableNameSeparator: "|", KeySeparator: "|"}) + defer configDb.DeleteDB() + dbs := [db.MaxDB]*db.DB{db.ConfigDB: configDb} + + tests := []struct { + name string + uri string + getDevice func() *ocbinds.Device + setupMock func() + expectError bool + errorMsg string + }{ + { + name: "Success - Populate Counters Core Callback Execution", + uri: "/openconfig-interfaces:interfaces/interface[name=Ethernet202]/state/counters", + getDevice: func() *ocbinds.Device { + d := &ocbinds.Device{} + ygot.BuildEmptyTree(d) + return d + }, + setupMock: func() { + entry := IntfTypeTblMap[IntfTypeEthernet] + targetField := reflect.ValueOf(&entry.CountersHdl).Elem().FieldByName("PopulateCounters") + if targetField.IsValid() { + mockFunc := reflect.MakeFunc(targetField.Type(), func(args []reflect.Value) []reflect.Value { + return []reflect.Value{reflect.Zero(reflect.TypeOf((*error)(nil)).Elem())} + }) + targetField.Set(mockFunc) + } + IntfTypeTblMap[IntfTypeEthernet] = entry + }, + expectError: false, + }, + { + name: "Success - Pre-existing Interfaces Subtree Match Branch", + uri: "/openconfig-interfaces:interfaces/interface[name=Ethernet202]/state/counters", + getDevice: func() *ocbinds.Device { + d := &ocbinds.Device{} + ygot.BuildEmptyTree(d) + d.Interfaces.NewInterface("Ethernet202") + return d + }, + setupMock: func() { + entry := IntfTypeTblMap[IntfTypeEthernet] + targetField := reflect.ValueOf(&entry.CountersHdl).Elem().FieldByName("PopulateCounters") + if targetField.IsValid() { + mockFunc := reflect.MakeFunc(targetField.Type(), func(args []reflect.Value) []reflect.Value { + return []reflect.Value{reflect.Zero(reflect.TypeOf((*error)(nil)).Elem())} + }) + targetField.Set(mockFunc) + } + IntfTypeTblMap[IntfTypeEthernet] = entry + }, + expectError: false, + }, + { + name: "Success - Redundant Target URI Path Branch Coverage", + uri: "/openconfig-interfaces:interfaces/interface[name=Ethernet202]/state/hardware-port", + getDevice: func() *ocbinds.Device { + d := &ocbinds.Device{} + ygot.BuildEmptyTree(d) + return d + }, + setupMock: func() {}, + expectError: false, + }, + { + name: "Failure - Invalid Interface Type Branch Coverage", + uri: "/openconfig-interfaces:interfaces/interface[name=InvalidIntf99]/state/counters", + getDevice: func() *ocbinds.Device { + d := &ocbinds.Device{} + ygot.BuildEmptyTree(d) + return d + }, + setupMock: func() {}, + expectError: true, + errorMsg: "Invalid interface type IntfTypeUnset", + }, + { + name: "Success - Counters Callback Not Supported Branch Coverage", + uri: "/openconfig-interfaces:interfaces/interface[name=Ethernet202]/state/counters", + getDevice: func() *ocbinds.Device { + d := &ocbinds.Device{} + ygot.BuildEmptyTree(d) + return d + }, + setupMock: func() { + entry := IntfTypeTblMap[IntfTypeEthernet] + entry.CountersHdl.PopulateCounters = nil + IntfTypeTblMap[IntfTypeEthernet] = entry + }, + expectError: false, + }, + { + name: "Success - Interface Not Found In Existing Map Coverage", + uri: "/openconfig-interfaces:interfaces/interface[name=Ethernet202]/state/counters", + getDevice: func() *ocbinds.Device { + d := &ocbinds.Device{} + ygot.BuildEmptyTree(d) + d.Interfaces.NewInterface("Ethernet4") + return d + }, + setupMock: func() { + entry := IntfTypeTblMap[IntfTypeEthernet] + targetField := reflect.ValueOf(&entry.CountersHdl).Elem().FieldByName("PopulateCounters") + if targetField.IsValid() { + mockFunc := reflect.MakeFunc(targetField.Type(), func(args []reflect.Value) []reflect.Value { + return []reflect.Value{reflect.Zero(reflect.TypeOf((*error)(nil)).Elem())} + }) + targetField.Set(mockFunc) + } + IntfTypeTblMap[IntfTypeEthernet] = entry + }, + expectError: false, + }, + { + name: "Success - Nil State Component Verification Branch Coverage", + uri: "/openconfig-interfaces:interfaces/interface[name=Ethernet202]/state/counters", + getDevice: func() *ocbinds.Device { + d := &ocbinds.Device{} + + d.Interfaces = &ocbinds.OpenconfigInterfaces_Interfaces{} + d.Interfaces.Interface = make(map[string]*ocbinds.OpenconfigInterfaces_Interfaces_Interface) + + intfObj := &ocbinds.OpenconfigInterfaces_Interfaces_Interface{ + Name: ygot.String("Ethernet202"), + State: &ocbinds.OpenconfigInterfaces_Interfaces_Interface_State{}, + } + + d.Interfaces.Interface["Ethernet202"] = intfObj + return d + }, + setupMock: func() { + entry := IntfTypeTblMap[IntfTypeEthernet] + targetField := reflect.ValueOf(&entry.CountersHdl).Elem().FieldByName("PopulateCounters") + if targetField.IsValid() { + mockFunc := reflect.MakeFunc(targetField.Type(), func(args []reflect.Value) []reflect.Value { + return []reflect.Value{reflect.Zero(reflect.TypeOf((*error)(nil)).Elem())} + }) + targetField.Set(mockFunc) + } + IntfTypeTblMap[IntfTypeEthernet] = entry + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ethernetOrigVal, ethExists := IntfTypeTblMap[IntfTypeEthernet] + + tt.setupMock() + + t.Cleanup(func() { + if ethExists { + IntfTypeTblMap[IntfTypeEthernet] = ethernetOrigVal + } + }) + + devRoot := tt.getDevice() + var goStructInterface ygot.GoStruct = devRoot + + var inParams XfmrParams + inParams.uri = tt.uri + inParams.curDb = db.ConfigDB + inParams.dbs = dbs + inParams.ygRoot = &goStructInterface + + err := DbToYang_intf_get_counters_xfmr(inParams) + + if tt.expectError { + if err == nil { + t.Fatalf("Expected an error but function returned success") + } + if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Error string mismatch.\nExpected containing: %q\nGot: %q", tt.errorMsg, err.Error()) + } + return + } + + if err != nil { + t.Fatalf("Unexpected error returned on positive execution path: %v", err) + } + + if devRoot == nil || devRoot.Interfaces == nil { + t.Errorf("Expected populated Openconfig output tree layout, but tree reference components are unassigned") + } + }) + } +} + +func TestGetSpecificCounterAttr_InFcsErrors(t *testing.T) { + tests := []struct { + name string + targetUriPath string + entry *db.Value + getCounter func() interface{} + expectHandled bool + expectError bool + }{ + { + name: "Success - Hit InFcsErrors Target Case Branch", + targetUriPath: "/openconfig-interfaces:interfaces/interface/state/counters/in-fcs-errors", + entry: &db.Value{ + Field: map[string]string{ + "SAI_PORT_STAT_ETHER_STATS_CRC_ALIGN_ERRORS": "42", + }, + }, + getCounter: func() interface{} { + c := &ocbinds.OpenconfigInterfaces_Interfaces_Interface_State_Counters{} + return c + }, + expectHandled: true, + expectError: false, + }, + { + name: "Success - Fallthrough to Default Path Unhandled", + targetUriPath: "/openconfig-interfaces:interfaces/interface/state/counters/unsupported-attribute", + entry: &db.Value{}, + getCounter: func() interface{} { + return &ocbinds.OpenconfigInterfaces_Interfaces_Interface_State_Counters{} + }, + expectHandled: false, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + counterContainer := tt.getCounter() + + handled, err := getSpecificCounterAttr(tt.targetUriPath, tt.entry, counterContainer) + + if tt.expectError && err == nil { + t.Fatalf("Expected execution error but function returned success status") + } + if !tt.expectError && err != nil { + t.Fatalf("Unexpected processing error encountered: %v", err) + } + + if handled != tt.expectHandled { + t.Errorf("Handled boolean indicator status mismatch.\nExpected: %t\nGot: %t", tt.expectHandled, handled) + } + + if tt.targetUriPath == "/openconfig-interfaces:interfaces/interface/state/counters/in-fcs-errors" && err == nil { + typedCounter := counterContainer.(*ocbinds.OpenconfigInterfaces_Interfaces_Interface_State_Counters) + if typedCounter.InFcsErrors == nil { + t.Errorf("Target field InFcsErrors pointer remained nil; value was not parsed into object") + } + } + }) + } +} + +func TestDbToYang_intf_physical_channel_xfmr(t *testing.T) { + configDb, _ := db.NewDB(db.Options{ + DBNo: db.ConfigDB, + InitIndicator: "CONFIG_DB_INITIALIZED", + TableNameSeparator: "|", + KeySeparator: "|", + }) + defer configDb.DeleteDB() + + stateDb, _ := db.NewDB(db.Options{ + DBNo: db.StateDB, + InitIndicator: "STATE_DB_INITIALIZED", + TableNameSeparator: "|", + KeySeparator: "|", + }) + defer stateDb.DeleteDB() + + dbs := [db.MaxDB]*db.DB{ + db.ConfigDB: configDb, + db.StateDB: stateDb, + } + + originalSfpMap := sfpTypeToMaxLanesMap + t.Cleanup(func() { + sfpTypeToMaxLanesMap = originalSfpMap + }) + sfpTypeToMaxLanesMap = map[string]int{ + "QSFP28": 4, + } + + tests := []struct { + name string + uri string + curDb db.DBNum + setupMock func() + expectError bool + errorMsg string + expectMap map[string]interface{} + }{ + { + name: "Failure - Invalid Interface Name Syntax", + uri: "/interfaces/interface[name=InvalidIntf0]/state/physical-channel", + curDb: db.ConfigDB, + setupMock: func() {}, + expectError: true, + errorMsg: "Invalid interface:", + }, + { + name: "Failure - Interface Type is Not Ethernet (PortChannel)", + uri: "/interfaces/interface[name=PortChannel1]/state/physical-channel", + curDb: db.ConfigDB, + setupMock: func() {}, + expectError: true, + errorMsg: "interface type is not IntfTypeEthernet", + }, + { + name: "Failure - Lanes Field Missing from DB Entry", + uri: "/interfaces/interface[name=Ethernet4]/state/physical-channel", + curDb: db.ConfigDB, + setupMock: func() { + configDb.SetEntry(&db.TableSpec{Name: "PORT"}, + db.Key{Comp: []string{"Ethernet4"}}, + db.Value{Field: map[string]string{"index": "4"}}) + }, + expectError: true, + errorMsg: "lanes not found in DB", + }, + { + name: "Failure - Transceiver Type Field Missing in StateDB", + uri: "/interfaces/interface[name=Ethernet8]/state/physical-channel", + curDb: db.ConfigDB, + setupMock: func() { + configDb.SetEntry(&db.TableSpec{Name: "PORT"}, + db.Key{Comp: []string{"Ethernet8"}}, + db.Value{Field: map[string]string{"lanes": "8", "index": "8"}}) + + stateDb.SetEntry(&db.TableSpec{Name: "TRANSCEIVER_INFO"}, + db.Key{Comp: []string{"Ethernet8"}}, + db.Value{Field: map[string]string{"type": ""}}) + }, + expectError: true, + errorMsg: "empty transceiver type for physical-channel", + }, + { + name: "Failure - Unknown Transceiver Type Map Lookup Match", + uri: "/interfaces/interface[name=Ethernet12]/state/physical-channel", + curDb: db.ConfigDB, + setupMock: func() { + configDb.SetEntry(&db.TableSpec{Name: "PORT"}, + db.Key{Comp: []string{"Ethernet12"}}, + db.Value{Field: map[string]string{"lanes": "12", "index": "12"}}) + + stateDb.SetEntry(&db.TableSpec{Name: "TRANSCEIVER_INFO"}, + db.Key{Comp: []string{"Ethernet12"}}, + db.Value{Field: map[string]string{"type": "UNKNOWN_SFP"}}) + }, + expectError: true, + errorMsg: "could not find the max number of lanes", + }, + { + name: "Failure - DB Connection Error", + uri: "/interfaces/interface[name=Ethernet16]/state/physical-channel", + curDb: db.ErrorDB, + setupMock: func() {}, + expectError: true, + errorMsg: "DB error", + }, + { + name: "Failure - Port Index Field Missing from DB", + uri: "/interfaces/interface[name=Ethernet100]/state/physical-channel", + curDb: db.ConfigDB, + setupMock: func() { + configDb.SetEntry(&db.TableSpec{Name: "PORT"}, + db.Key{Comp: []string{"Ethernet100"}}, + db.Value{Field: map[string]string{"lanes": "100"}}) + }, + expectError: true, + errorMsg: "index not found in DB", + }, + { + name: "Failure - Transceiver Entry Missing from StateDB", + uri: "/interfaces/interface[name=Ethernet20]/state/physical-channel", + curDb: db.ConfigDB, + setupMock: func() { + configDb.SetEntry(&db.TableSpec{Name: "PORT"}, + db.Key{Comp: []string{"Ethernet20"}}, + db.Value{Field: map[string]string{"lanes": "20", "index": "20"}}) + }, + expectError: true, + errorMsg: "Entry does not exist", + }, + { + name: "Failure - Platform ChannelOffset Error", + uri: "/interfaces/interface[name=Ethernet999]/state/physical-channel", + curDb: db.ConfigDB, + setupMock: func() { + + configDb.SetEntry(&db.TableSpec{Name: "PORT"}, + db.Key{Comp: []string{"Ethernet999"}}, + db.Value{Field: map[string]string{"lanes": "999", "index": "999"}}) + + stateDb.SetEntry(&db.TableSpec{Name: "TRANSCEIVER_INFO"}, + db.Key{Comp: []string{"Ethernet999"}}, + db.Value{Field: map[string]string{"type": "QSFP28"}}) + }, + expectError: true, + errorMsg: "could not find the channel offset for Ethernet999", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setupMock() + + inParams := XfmrParams{ + uri: tt.uri, + curDb: tt.curDb, + dbs: dbs, + } + + resMap, err := DbToYang_intf_physical_channel_xfmr(inParams) + + if tt.expectError { + if err == nil { + t.Fatalf("Expected function execution to fail but got success") + } + if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Error string signature mismatch.\nExpected containing: %q\nGot actual: %q", tt.errorMsg, err.Error()) + } + return + } + + if err != nil { + t.Fatalf("Unexpected error returned during logic execution: %v", err) + } + + actualChannels, ok := resMap["physical-channel"].([]uint16) + if !ok { + t.Fatalf("Returned key 'physical-channel' type mismatch. Expected []uint16, got: %T", resMap["physical-channel"]) + } + + expectedChannels := tt.expectMap["physical-channel"].([]uint16) + if len(actualChannels) != len(expectedChannels) { + t.Fatalf("Channel array size mismatch. Expected length %d, got %d", len(expectedChannels), len(actualChannels)) + } + + for idx, targetVal := range expectedChannels { + if actualChannels[idx] != targetVal { + t.Errorf("Channel assignment value mismatch at position index %d. Expected %d, got %d", idx, targetVal, actualChannels[idx]) + } + } + }) + } +} diff --git a/translib/transformer/xfmr_path_utils.go b/translib/transformer/xfmr_path_utils.go index 1660cd1ff..04fad80fb 100644 --- a/translib/transformer/xfmr_path_utils.go +++ b/translib/transformer/xfmr_path_utils.go @@ -138,3 +138,12 @@ func SplitPath(path string) []string { parts = append(parts, path[start:]) return parts } + +// Returns the key from InParams, else parses kname from the uri +// If the uri contains multiple keys, inParams.key will be the last +func keyFromInParamsOrUri(inParams XfmrParams, kname string) string { + if inParams.key != "" { + return inParams.key + } + return NewPathInfo(inParams.uri).Var(kname) +} From 57d5370ade92ba626e51bc53636b5f5464784b85 Mon Sep 17 00:00:00 2001 From: Aliyah Hoda Date: Wed, 29 Jul 2026 15:29:15 +0000 Subject: [PATCH 2/2] Support for UMF Interfaces Model - Wildcard Subscription Signed-off-by: Aliyah Hoda --- .../openconfig-interfaces-annot.yang | 1 + translib/transformer/xfmr_intf.go | 27 +++++- translib/transformer/xfmr_intf_test.go | 84 +++++++++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/models/yang/annotations/openconfig-interfaces-annot.yang b/models/yang/annotations/openconfig-interfaces-annot.yang index 14b0729c3..dcefee9d4 100644 --- a/models/yang/annotations/openconfig-interfaces-annot.yang +++ b/models/yang/annotations/openconfig-interfaces-annot.yang @@ -18,6 +18,7 @@ module openconfig-interfaces-annot { deviate add { sonic-ext:key-transformer "intf_tbl_key_xfmr"; sonic-ext:table-transformer "intf_table_xfmr"; + sonic-ext:path-transformer "intf_path_xfmr"; } } diff --git a/translib/transformer/xfmr_intf.go b/translib/transformer/xfmr_intf.go index 5355b7266..495f5d471 100644 --- a/translib/transformer/xfmr_intf.go +++ b/translib/transformer/xfmr_intf.go @@ -1554,6 +1554,8 @@ var Subscribe_intf_get_counters_xfmr SubTreeXfmrSubscribe = func(inParams XfmrSu result.nOpts.mInterval = 30 result.isVirtualTbl = false result.needCache = true + result.onChange = OnchangeDisable + result.dbDataMap = make(RedisDbSubscribeMap) ifName := pathInfo.Var("name") log.Info("Subscribe_intf_get_counters_xfmr: ifName: ", ifName) @@ -1561,14 +1563,33 @@ var Subscribe_intf_get_counters_xfmr SubTreeXfmrSubscribe = func(inParams XfmrSu if ifName == "" || ifName == "*" { if strings.HasPrefix(targetUriPath, "/openconfig-interfaces:interfaces/interface/openconfig-if-ethernet:ethernet/state/counters") { ifName = "Eth" + "*" + tblName, err := getPortTableNameByDBId(IntfTypeTblMap[IntfTypeEthernet], db.ConfigDB) + if err != nil { + return result, errors.New("Subscribe_intf_get_counters_xfmr table name not found. Err: " + err.Error()) + } + + result.dbDataMap = RedisDbSubscribeMap{db.ConfigDB: {tblName: {ifName: {}}}} } else { ifName = "*" + result.dbDataMap[db.ConfigDB] = make(map[string]map[string]map[string]string) + for _, tblName := range dbIdToTblMap[db.ConfigDB] { + result.dbDataMap[db.ConfigDB][tblName] = map[string]map[string]string{ifName: {}} + } + } + } else { + intfType, _, ierr := getIntfTypeByName(ifName) + if intfType == IntfTypeUnset || ierr != nil { + return result, tlerr.InvalidArgsError{Format: "Invalid interface: " + ifName} + } + tblName, err := getPortTableNameByDBId(IntfTypeTblMap[intfType], db.ConfigDB) + if err != nil { + return result, errors.New("Subscribe_intf_get_counters_xfmr table name not found. Err: " + err.Error()) } - } - result.dbDataMap = RedisDbSubscribeMap{db.CountersDB: {"COUNTERS_PORT_NAME_MAP": {"": {FIELD_CURSOR: ifName}}}} + result.dbDataMap = RedisDbSubscribeMap{db.ConfigDB: {tblName: {ifName: {}}}} + } - log.Info("Subscribe_intf_eth_port_config_xfmr: result ", result) + log.Info("Subscribe_intf_get_counters_xfmr: result ", result) } return result, err } diff --git a/translib/transformer/xfmr_intf_test.go b/translib/transformer/xfmr_intf_test.go index c995aecdc..b63159a9c 100644 --- a/translib/transformer/xfmr_intf_test.go +++ b/translib/transformer/xfmr_intf_test.go @@ -1165,3 +1165,87 @@ func TestDbToYang_intf_physical_channel_xfmr(t *testing.T) { }) } } + +func TestSubscribe_intf_get_counters_xfmr(t *testing.T) { + tests := []struct { + name string + uri string + subscProc SubscProcType + expectError bool + errorMsg string + expectedIf string + }{ + { + name: "Success - Ethernet Wildcard Pattern", + uri: "/openconfig-interfaces:interfaces/interface[name=*]/openconfig-if-ethernet:ethernet/state/counters", + subscProc: TRANSLATE_SUBSCRIBE, + expectError: false, + expectedIf: "Eth*", + }, + { + name: "Success - Generic Wildcard Pattern", + uri: "/openconfig-interfaces:interfaces/interface[name=*]/state/counters", + subscProc: TRANSLATE_SUBSCRIBE, + expectError: false, + expectedIf: "*", + }, + { + name: "Success - Specific Interface", + uri: "/openconfig-interfaces:interfaces/interface[name=Ethernet0]/state/counters", + subscProc: TRANSLATE_SUBSCRIBE, + expectError: false, + expectedIf: "Ethernet0", + }, + { + name: "Failure - Invalid Interface Name", + uri: "/openconfig-interfaces:interfaces/interface[name=Invalid99]/state/counters", + subscProc: TRANSLATE_SUBSCRIBE, + expectError: true, + errorMsg: "Invalid interface: Invalid99", + expectedIf: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + inParams := XfmrSubscInParams{ + uri: tt.uri, + subscProc: tt.subscProc, + } + + res, err := Subscribe_intf_get_counters_xfmr(inParams) + + if tt.expectError { + if err == nil { + t.Fatalf("Expected error but function returned success") + } + if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Error mismatch. Expected: %s, Got: %v", tt.errorMsg, err) + } + return + } + + if err != nil { + t.Fatalf("Unexpected error state: %v", err) + } + + tblMap, exists := res.dbDataMap[db.ConfigDB] + if !exists { + t.Fatalf("ConfigDB subscription data missing") + } + + foundMatch := false + for tblName, keyMap := range tblMap { + if _, ok := keyMap[tt.expectedIf]; ok { + foundMatch = true + t.Logf("Found expected interface key %s in table %s", tt.expectedIf, tblName) + break + } + } + + if !foundMatch { + t.Errorf("Expected key %s not found in any ConfigDB table. Map: %v", tt.expectedIf, tblMap) + } + }) + } +}