From ec7dab026c317c63196f413ff64aa2859a062f89 Mon Sep 17 00:00:00 2001 From: Aliyah Hoda Date: Mon, 22 Jun 2026 18:39:51 +0000 Subject: [PATCH] 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 | 938 ++++++++++++ translib/platform/platform_test.go | 474 ++++++ translib/transformer/xfmr_intf.go | 310 +++- translib/transformer/xfmr_intf_test.go | 1326 +++++++++++++++++ translib/transformer/xfmr_path_utils.go | 9 + 10 files changed, 3137 insertions(+), 34 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..fb44e543f --- /dev/null +++ b/translib/platform/platform.go @@ -0,0 +1,938 @@ +package platform + +import ( + "encoding/json" + "os" + "sort" + "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 + * SubIndex - first channel of an interface + * 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 + * + * Interface naming scheme is 1// + * Examples: Ethernet1/1/1, Ethernet1/32/8 + * The index corresponds to a port and may be zero-based or one-based depending + * on the platform. The subIndex is the first channel of the interface and may + * be omitted for non-breakable interfaces such as SFP+, e.g. Ethernet1/1. + */ +const ( + PLATFORM_JSON = "/usr/share/sonic/hwsku/platform.json" + PLATFORM_PLATFORM_JSON = "/usr/share/sonic/platform/platform.json" + HWSKU_JSON = "/usr/share/sonic/hwsku/hwsku.json" + GBPS_TO_MBPS = 1000 +) + +type platformIntf struct { + index int + subIndex int + isPrimary bool + name string + alias string + dfltMode string // Default breakout mode. + modes []string // List of supported breakout modes. + lanes []int // For a given interface group, each interface has a slice of the same array + speedsGbps []int // Possible speeds for the interface in Gbps + primary *platformIntf + intfGroup []*platformIntf // All interfaces in the group (including the primary) + channelOffset uint16 // (First Lane) % (lane set size); used to derive channel index from lane +} + +type InterfaceProperties struct { + Index int // Index of the interface, Ethernet1// + SpeedMbps int // Interface speed in Mbps + Lanes []int // Slice of lanes used by the interface from platformIntf + Name string + Alias string +} + +type platformConfig struct { + intfs map[string]*platformIntf + intfNameToPortName map[string]string + portNameToIntfName map[string]string +} + +type brkoutProperties struct { + /* True when all interfaces in a mixed-mode breakout use the same number of + * channels. 1x400G(4)+1x200G(4) would be true since both the 400G and + * 200G interface use 4 channels even though the channels run at different + * serdes speeds. 1x400G(4)+2x200G(4) would be false since the 400G interface + * uses 4 channels and the 200G interfaces use 2 channels each. */ + isSymmetric bool + /* The total number of interfaces in the breakout; sum of all the "x" + * prefixes in the breakout mode string. 1x400G(4)+4x100G(4) would be 5. */ + numIntfs int + /* An array of all speeds in the breakout, e.g. {"100G", "200G"}. This is + * unsorted and may contain duplicates. */ + speedsGbps []string +} + +var platformCfg platformConfig +var intfToDefaultMode = map[string]string{} // from hwsku.json + +/* Functions */ +func init() { + parseHwskuJson() + parsePlatformJson() +} + +func parseThisThenThat(a, b string) error { + if err := doParsePlatformJson(a); err != nil { + return doParsePlatformJson(b) + } + return nil +} + +var once = sync.OnceValue(func() error { return parseThisThenThat(PLATFORM_JSON, PLATFORM_PLATFORM_JSON) }) +var onceagain = sync.OnceValue(func() error { return doParseHwskuJson(HWSKU_JSON) }) + +func parsePlatformJson() error { + err := once() + if err != nil { + if log.V(3) { + log.Info(err) + } + } + return err +} + +func parseHwskuJson() error { + err := onceagain() + if err != nil { + if log.V(3) { + log.Info(err) + } + } + return err +} + +// Returns true if the legacy format was detected. +// The distinction is primarily derived from the `breakout_modes` type. +// In the standard sonic format,`breakout_modes` is a dictionary +// but a string in the legacy format. +// +// So "An interface entry where all fields are strings" is used to make this +// determination. +// +// The input is expected to be the platform.json `interfaces` dictionary +// unmarshalled into a map. Here's an example json before unmarshalling: +// +// "interfaces": { +// "Ethernet1/1/1": { +// "index": "1,1,1,1,1,1,1,1", +// "default_brkout_mode": "8x100G", +// "lanes": "9,10,11,12,13,14,15,16", +// "alias_at_lanes": "Eth1/1,Eth1/2,Eth1/3,Eth1/4,Eth1/5,Eth1/6,Eth1/7,Eth1/8", +// "breakout_modes": "1x1600G[800G], 2x800G[400G,200G,100G], 4x400G[200G], 8x200G[100G]," +// }, +// "Ethernet1/1/2": { "index": "1", "breakout_modes": "1x200G[100G]" }, +// ... +// } +func legacyInterfacesFormatFound(intfsJson map[string]any) (bool, error) { + for _, v := range intfsJson { + intfJson, ok := v.(map[string]any) + if !ok { + return false, tlerr.InvalidArgs("Failed type assertion, unexpected interfaces format") + } + // are all values strings? + for _, vv := range intfJson { + if _, ok := vv.(string); !ok { + return false, nil + } + } + break + } + return true, nil +} + +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 := parsedJson["interfaces"].(map[string]any) + log.Infof("Found %d interface entries in platform json", len(intfsJson)) + if legacyFound, err := legacyInterfacesFormatFound(intfsJson); err != nil { + return err + } else if !legacyFound { + return doParsePlatformJsonSonic(intfsJson) + } + for intfName, v := range intfsJson { + /* All values are strings */ + intfJson := v.(map[string]any) + intfStrMap := make(map[string]string) + for k, v := range intfJson { + intfStrMap[k] = v.(string) + } + indexJson := intfStrMap["index"] + modesJson := intfStrMap["breakout_modes"] + dfltModeJson := intfStrMap["default_brkout_mode"] + lanesJson := intfStrMap["lanes"] + + /* GPINS naming convention is 1// where index + * corresponds to the front panel labeling and subindex is a one-based + * offset into the group of lanes. The subindex may be ommitted for single + * lane interface such as SFP+ interfaces (hence the length may be 2 or 3). */ + nameArr := strings.Split(intfName, "/") + if len(nameArr) != 2 && len(nameArr) != 3 { + if log.V(3) { + log.Infof("Platform json parsing error %s:malformed name", intfName) + } + return tlerr.New("Platform json parsing error %s:malformed name", intfName) + } + nameIndex, err := strconv.Atoi(nameArr[1]) + if err != nil { + if log.V(3) { + log.Infof("Platform json parsing error %s:nameIndex, err %v", intfName, err) + } + return err + } + var subIndex int + if len(nameArr) > 2 { + subIndex, err = strconv.Atoi(nameArr[2]) + if err != nil { + if log.V(3) { + log.Infof("Platform json parsing error %s:subIndex, err %v", intfName, err) + } + return err + } + } + + /* 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 + } + if index != nameIndex { + if log.V(3) { + log.Infof("Platform json parsing error %s:parsed index %d doesn't match name", intfName, index) + } + return tlerr.New("Platform json parsing error %s:parsed index %d doesn't match name", intfName, index) + } + + /* 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. They should also be the first + * sub-index in the interface group, i.e. subindex one. Note that unbreakable + * interfaces have a subindex of zero. */ + isPrimary := len(lanesJson) > 0 && subIndex <= 1 + + /* The primary interface is required to have breakout modes. It is not required for other interfaces. */ + modes := parseBreakoutModes(modesJson) + if len(modes) == 0 && isPrimary { + if log.V(3) { + log.Infof("Platform json parsing error %s:breakout_modes=\"%s\"", intfName, modesJson) + } + return tlerr.New("Platform json parsing error %s:breakout_modes=\"%s\"", intfName, modesJson) + } + /* Sanitize and validate the breakout mode string and default breakout mode string. */ + if len(dfltModeJson) > 0 { + dfltMode, ok := sanitizeBreakoutMode(dfltModeJson) + if !ok { + return tlerr.New("Platform json parsing error %s:bad default_brkout_mode=\"%v\"", intfName, dfltMode) + } + dfltSpeed, err := brkoutModesToSpeeds([]string{dfltMode}) + if err != nil || len(dfltSpeed) != 1 { + return tlerr.New("Platform json parsing error %s:bad default_brkout_mode=\"%v\"", intfName, dfltMode) + } + } + for i, m := range modes { + mCleaned, ok := sanitizeBreakoutMode(m) + if !ok { + return tlerr.New("Platform json parsing error %s:bad mode=\"%v\"", intfName, modes) + } + modes[i] = mCleaned + } + speeds, err := brkoutModesToSpeeds(modes) + if err != nil { + if log.V(3) { + log.Infof("Platform json parsing error %s:breakout_modes=\"%s\" (%v)", intfName, modesJson, err) + } + return tlerr.New("Platform json parsing error %s:breakout_modes=\"%s\" (%v)", intfName, modesJson, err) + } + + platformCfg.intfs[intfName] = &platformIntf{ + name: intfName, + index: index, + subIndex: subIndex, + lanes: lanesInt, + speedsGbps: speeds, + modes: modes, + dfltMode: dfltModeJson, + 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 := mJsonAny.(map[string]any) + mJsonStrMap := make(map[string]string) + for k, v := range mJson { + mJsonStrMap[k] = v.(string) + } + + /* Non-breakable interfaces will have a subindex of zero while breakable interfaces + * have a one-based index. Check what case we are in and unify to a zero- + * based index. */ + subIndex := 0 + if intf.subIndex > 0 { + subIndex = intf.subIndex - 1 + } + + aliases := strings.Split(mJsonStrMap["alias_at_lanes"], ",") + 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) > subIndex { + /* Aliases are defined and our index is legal, save the alias. */ + intf.alias = strings.TrimSpace(aliases[subIndex]) + } else { + /* Aliases are defined, but this interface's index is out of range. */ + if log.V(3) { + log.Infof("Platform json parsing error %s:subIndex=%d, no alias=\"%s\"", intf.name, subIndex, aliases) + } + return tlerr.New("Platform json parsing error %s:subIndex=%d, no alias=\"%s\"", intf.name, subIndex, aliases) + } + + /* Tag this interface with the lane set composed of its own lane as well as all + * lanes of subindexes larger than itself. */ + intf.lanes = intf.primary.lanes[subIndex:] + } + log.Infof("Updated %d platform interfaces from platform json", len(platformCfg.intfs)) + + /* Loop over all interfaces to build the intfRel map capturing the primary intf to + * all interfaces in the group mapping. */ + intfRel := make(map[string][]string) + for _, pIntf := range platformCfg.intfs { + primaryIntf := pIntf.primary.name + relatedIntfs, ok := intfRel[primaryIntf] + if !ok { + relatedIntfs = []string{} + } + intfRel[primaryIntf] = append(relatedIntfs, pIntf.name) + } + for k, v := range intfRel { + sort.Strings(v) + intfRel[k] = v + } + for primaryIntfName, intfGroup := range intfRel { + priIntf := platformCfg.intfs[primaryIntfName] + priIntf.intfGroup = make([]*platformIntf, len(intfGroup)) + for i, intfName := range intfGroup { + pIntf := platformCfg.intfs[intfName] + priIntf.intfGroup[i] = pIntf + } + for _, pIntf := range priIntf.intfGroup { + pIntf.intfGroup = priIntf.intfGroup + } + if len(priIntf.intfGroup) > len(priIntf.lanes) { + if log.V(3) { + log.Infof("Platform json parsing error %s: %d lanes but %d intfs in group", primaryIntfName, len(priIntf.lanes), len(priIntf.intfGroup)) + } + return tlerr.New("Platform json parsing error %s: %d lanes but %d intfs in group", primaryIntfName, len(priIntf.lanes), len(priIntf.intfGroup)) + } + } + log.Infof("Updated %d platform intfs from platform json", len(platformCfg.intfs)) + + /* Loop over all interfaces to build the interface name (Ethernet1/1/1) to port + * name (1/1). */ + 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 doParseHwskuJson(filename string) error { + file, err := os.ReadFile(filename) + if err != nil { + if log.V(3) { + log.Infof("Error reading hwsku 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 hwsku json, file %s, err %v", filename, err) + } + return err + } + + // Pass over the json, map interface to default breakout mode + intfsJson, ok := parsedJson["interfaces"].(map[string]any) + if !ok { + return tlerr.InvalidArgs("Type assertion failed converting interfaces") + } + log.Infof("Found %d interface entries in hwsku json", len(intfsJson)) + for intfName, v := range intfsJson { + intfJson, ok := v.(map[string]any) + if !ok { + return tlerr.InvalidArgs("Type assertion failed converting interface") + } + intfStrMap := make(map[string]string) + for k, v := range intfJson { + intfStrMap[k], ok = v.(string) + if !ok { + return tlerr.InvalidArgs("Type assertion failed converting interface fields") + } + } + intfToDefaultMode[intfName] = intfStrMap["default_brkout_mode"] + } + log.Infof("interface to default map: %v", intfToDefaultMode) + return nil +} + +func doParsePlatformJsonSonic(intfsJson map[string]any) error { + for _, v := range intfsJson { + intfJson := v.(map[string]any) + indexJson := "" + modesJson := make(map[string]interface{}) + lanesJson := "" + for k, v := range intfJson { + switch k { + case "index": + indexJson = v.(string) + case "lanes": + lanesJson = v.(string) + case "breakout_modes": + modesJson = v.(map[string]interface{}) + } + } + + intfToMode, err := parseBreakoutModesDict(modesJson, lanesJson) + if err != nil { + if log.V(3) { + log.Infof("parseBreakoutModesDict returned=%v", err) + } + } + + // Iterate through the interfaces discovered by parsing intfName's breakout modes + for intf, modes := range intfToMode { + speeds, err := brkoutModesToSpeeds(modes) + if err != nil { + if log.V(3) { + log.Infof("brkoutModesToSpeeds returned=%v", err) + } + } + + /* GPINS naming convention is 1// where index + * corresponds to the front panel labeling and subindex is a one-based + * offset into the group of lanes. The subindex may be ommitted for single + * lane interface such as SFP+ interfaces (hence the length may be 2 or 3). */ + nameArr := strings.Split(intf, "/") + switch len(nameArr) { + case 2, 3: + break + default: + if log.V(3) { + log.Infof("Platform json parsing error %s:malformed name", intf) + } + return tlerr.New("Platform json parsing error %s:malformed name", intf) + } + nameIndex, err := strconv.Atoi(nameArr[1]) + if err != nil { + if log.V(3) { + log.Infof("Platform json parsing error %s:nameIndex, err %v", intf, err) + } + return err + } + var subIndex int + if len(nameArr) > 2 { + subIndex, err = strconv.Atoi(nameArr[2]) + if err != nil { + if log.V(3) { + log.Infof("Platform json parsing error %s:subIndex, err %v", intf, err) + } + return err + } + } + + /* 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)", intf, indexJson) + } + return tlerr.New("Platform json parsing error %s:index=\"%s\" (missing)", intf, 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", intf, indexJson, err) + } + return err + } + if index != nameIndex { + if log.V(3) { + log.Infof("Platform json parsing error %s:parsed index %d doesn't match name", intf, index) + } + return tlerr.New("Platform json parsing error %s:parsed index %d doesn't match name", intf, index) + } + + /* 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", intf, lanesJson, err) + } + return err + } + lanesInt[i] = lane + } + } + /* Primary interface must have the lane set. They should also be the first + * sub-index in the interface group, i.e. subindex one. Note that unbreakable + * interfaces have a subindex of zero. */ + isPrimary := len(lanesJson) > 0 && subIndex <= 1 + + dfltModeJson := "" + if dbm, ok := intfToDefaultMode[intf]; ok && isPrimary { + dfltModeJson = dbm + } + + platformCfg.intfs[intf] = &platformIntf{ + name: intf, + alias: intf, + index: index, + subIndex: subIndex, + lanes: lanesInt, + speedsGbps: speeds, + modes: modes, + dfltMode: dfltModeJson, + isPrimary: isPrimary, + } + } + } + + /* 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) + } + + /* Non-breakable interfaces will have a subindex of zero while breakable interfaces + * have a one-based index. Check what case we are in and unify to a zero- + * based index. */ + subIndex := 0 + if intf.subIndex > 0 { + subIndex = intf.subIndex - 1 + } + + /* Tag this interface with the lane set composed of its own lane as well as all + * lanes of subindexes larger than itself. */ + intf.lanes = intf.primary.lanes[subIndex:] + } + log.Infof("Updated %d platform interfaces from platform json", len(platformCfg.intfs)) + + /* Loop over all interfaces to build the intfRel map capturing the primary intf to + * all interfaces in the group mapping. */ + intfRel := make(map[string][]string) + for _, pIntf := range platformCfg.intfs { + primaryIntf := pIntf.primary.name + relatedIntfs, ok := intfRel[primaryIntf] + if !ok { + relatedIntfs = []string{} + } + intfRel[primaryIntf] = append(relatedIntfs, pIntf.name) + } + for k, v := range intfRel { + sort.Strings(v) + intfRel[k] = v + } + for primaryIntfName, intfGroup := range intfRel { + priIntf := platformCfg.intfs[primaryIntfName] + priIntf.intfGroup = make([]*platformIntf, len(intfGroup)) + for i, intfName := range intfGroup { + pIntf := platformCfg.intfs[intfName] + priIntf.intfGroup[i] = pIntf + } + for _, pIntf := range priIntf.intfGroup { + pIntf.intfGroup = priIntf.intfGroup + } + if len(priIntf.intfGroup) > len(priIntf.lanes) { + if log.V(3) { + log.Infof("Platform json parsing error %s: %d lanes but %d intfs in group", primaryIntfName, len(priIntf.lanes), len(priIntf.intfGroup)) + } + return tlerr.New("Platform json parsing error %s: %d lanes but %d intfs in group", primaryIntfName, len(priIntf.lanes), len(priIntf.intfGroup)) + } + } + log.Infof("Updated %d platform intfs from platform json", len(platformCfg.intfs)) + + /* Loop over all interfaces to build the interface name (Ethernet1/1/1) to port + * name (1/1). */ + 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 parseBreakoutModes(modes string) []string { + // Example breakout_modes entry: "1x400G, 2x200G[100G,40G], 1x200G(4)+2x100G(4)" + // Returns array length 3: {"1x400G", "2x200G[100G,40G]", "1x200G(4)+2x100G(4)"} + isSqParens := false + return strings.FieldsFunc(modes, func(c rune) bool { + if c == '[' { + isSqParens = true + } else if c == ']' { + isSqParens = false + } + if !isSqParens { + return c == ',' + } + return false + }) +} + +// Parses the breakout_modes and +// returns map of interfaces to modes (they are part of) +func parseBreakoutModesDict(modes map[string]interface{}, lanes string) (map[string][]string, error) { + // Example input dictionary: + // breakout_modes": { + // "1x1600G": ["Ethernet1/35/1"], + // "2x800G": ["Ethernet1/35/1","Ethernet1/35/5"] + // } + intfToMode := make(map[string][]string) + for mode, data := range modes { + intfs := data.([]interface{}) + m, ok := sanitizeBreakoutMode(mode) + if !ok { + return nil, tlerr.InvalidArgs("Unable to sanitize mode:" + mode) + } + for _, intf := range intfs { + i := intf.(string) + intfToMode[i] = append(intfToMode[i], m) + } + } + + return intfToMode, nil +} + +func sanitizeBreakoutMode(unsanitizedMode string) (string, bool) { + mode := strings.TrimSpace(unsanitizedMode) + if len(mode) == 0 || strings.ContainsAny(mode, " \t\n\r") { + if log.V(3) { + log.Infof("Breakout mode \"%s\" contains whitespace", mode) + } + return mode, false + } + numModes := strings.Count(mode, "+") + 1 + isMixed := numModes > 1 + mixedModes := strings.Split(mode, "+") + if len(mixedModes) != numModes { + if log.V(3) { + log.Infof("Breakout mixed-mode \"%s\" expected %d modes, got %d", mode, numModes, len(mixedModes)) + } + return mode, false + } + for _, mixedMode := range mixedModes { + /* Modes must be prefixed by the interface count, "x..." */ + prefixSep := strings.Index(mixedMode, "x") + if prefixSep == -1 { + if log.V(3) { + log.Infof("Breakout mode \"%s\" not prefixed by intf count", mode) + } + return mode, false + } + if _, err := strconv.Atoi(mixedMode[:prefixSep]); err != nil { + if log.V(3) { + log.Infof("Breakout mode \"%s\" prefixed by non-int", mode) + } + return mode, false + } + /* If this is a mixed mode, each mode must have a lane count suffix "...(= suffixEnd { + if log.V(3) { + log.Infof("Breakout mode \"%s\" missing lane count suffix for mixed mode", mode) + } + return mode, false + } + _, err := strconv.Atoi(mixedMode[suffixStart+1 : suffixEnd]) + if err != nil { + if log.V(3) { + log.Infof("Breakout mode \"%s\" has non-int lane count suffix for mixed mode", mode) + } + return mode, false + } + mixedMode = mixedMode[:suffixStart] + } + /* The speeds are listed after the prefix but before the optional lane count. */ + speeds := mixedMode[prefixSep+1:] + /* Speeds may be a single speed, "100G", or a set of speeds, "400G[200G,100G,50G]" */ + numOpen := strings.Count(speeds, "[") + numClose := strings.Count(speeds, "]") + if numOpen != numClose || numOpen > 1 { + if log.V(3) { + log.Infof("Breakout mode \"%s\" (%s) has malformed alternate speed list", mode, speeds) + } + return mode, false + } + if numOpen > 0 { + /* We have an alternate speed list, verify it. */ + start := strings.Index(speeds, "[") + end := strings.Index(speeds, "]") + if (start+1) >= end || end != len(speeds)-1 { + if log.V(3) { + log.Infof("Breakout mode \"%s\" (%s) has malformed alternate speed list \"%s\"; start %d end %d", mode, mixedMode, speeds, start, end) + } + return mode, false + } + for _, altSpeed := range strings.Split(speeds[start+1:end], ",") { + s, ok := strings.CutSuffix(altSpeed, "G") + if !ok { + if log.V(3) { + log.Infof("mode \"%s\", alt-speed \"%s\" is missing G suffix", mixedMode, altSpeed) + } + return mode, false + } + if _, err := strconv.Atoi(s); err != nil { + if log.V(3) { + log.Infof("mode \"%s\", alt-speed \"%s\" is not an int (\"%s\")", mixedMode, altSpeed, s) + } + return mode, false + } + } + speeds = speeds[:start] + } + s, ok := strings.CutSuffix(speeds, "G") + if !ok { + if log.V(3) { + log.Infof("mode \"%s\", speeds \"%s\" is missing G suffix", mixedMode, speeds) + } + return mode, false + } + if _, err := strconv.Atoi(s); err != nil { + if log.V(3) { + log.Infof("mode \"%s\", speeds \"%s\", is not an int (\"%s\")", mixedMode, speeds, s) + } + return mode, false + } + } + return mode, true +} + +func brkoutModesToSpeeds(brkoutModes []string) ([]int, error) { + speeds := map[int]int{} + for _, mode := range brkoutModes { + modeSpeeds, err := brkoutModeToSpeeds(mode) + if err != nil { + return nil, err + } + for _, speed := range modeSpeeds { + /* Use map to remove duplicates. */ + speeds[speed] = speed + } + } + speedList := make([]int, len(speeds)) + i := 0 + for speed := range speeds { + speedList[i] = speed + i++ + } + sort.Ints(speedList) + return speedList, nil +} +func brkoutModeToSpeeds(brkoutMode string) ([]int, error) { + speeds := map[int]int{} + modes := strings.FieldsFunc(brkoutMode, func(c rune) bool { + return c == ',' || c == '[' || c == ']' || c == '+' + }) + for _, mode := range modes { + // Mode may contain, or be, the mixed mode channel count, e.g "(4)", if so skip it + chanCountIndex := strings.Index(mode, "(") + if chanCountIndex != -1 { + mode = mode[:chanCountIndex] + mode = strings.TrimSpace(mode) + if len(mode) == 0 { + continue + } + } + // Mode may have a "x" prefix; ignore it. + before, after, found := strings.Cut(mode, "x") + if found { + mode = after + } else { + mode = before + } + // Mode may have a suffix of "()"; ignore it. + suffixStart := strings.Index(mode, "(") + if suffixStart != -1 { + mode = mode[:suffixStart] + } + // Mode must always end in "G" at this point. + if mode[len(mode)-1] != 'G' { + return nil, tlerr.InvalidArgs("Invalid breakout mode \"%s\" (%s)", brkoutMode, mode) + } + mode = mode[:len(mode)-1] + speed, err := strconv.Atoi(mode) + if err != nil { + return nil, tlerr.InvalidArgs("Invalid breakout mode \"%s\" (%s)", brkoutMode, mode) + } + speeds[speed] = speed + } + speedList := make([]int, len(speeds)) + i := 0 + for speed := range speeds { + speedList[i] = speed + i++ + } + sort.Ints(speedList) + return speedList, nil +} + +func calcChannelOffset() { + // Derive the channel offset for each primary interface + for _, intf := range platformCfg.intfs { + if !intf.isPrimary { + continue + } + intf.channelOffset = uint16(intf.lanes[0] % len(intf.lanes)) + + // Propogate the offset to the rest of the group + for _, i := range intf.intfGroup { + i.channelOffset = intf.channelOffset + } + } +} + +func ChannelOffset(intfName string) (uint16, error) { + 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..624c0db30 --- /dev/null +++ b/translib/platform/platform_test.go @@ -0,0 +1,474 @@ +package platform + +import ( + "fmt" + "os" + "testing" + + "github.com/google/go-cmp/cmp" +) + +var platformCfgBackup platformConfig +var intfToDefaultModeBackup map[string]string + +func savePlatformConfig() { + platformCfgBackup = platformCfg +} + +func restorePlatformConfig() { + platformCfg = platformCfgBackup +} + +func saveHwskuConfig() { + intfToDefaultModeBackup = intfToDefaultMode +} + +func restoreHwskuConfig() { + intfToDefaultMode = intfToDefaultModeBackup +} + +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) + + return doParsePlatformJson(file.Name()) +} + +func loadTestHwskuJson(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) + + return doParseHwskuJson(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 TestParseThisThenThatFallback(t *testing.T) { + savePlatformConfig() + defer restorePlatformConfig() + platJson := + `{ + "interfaces": { + "test1/12/1": { + "lanes": "80,81,82,83,84,85,86,87", + "index": "12,12,12,12,12,12,12,12", + "default_brkout_mode": "2x800G", + "breakout_modes": "2x800G, 4x400G, 8x200G[100G,50G], 1x800G(4)+2x400G(4), 1x800G(4)+4x200G[100G,50G](4), 4x200G[100G,50G](4)+2x400G(4)", + "alias_at_lanes": "Tst12/1, Tst12/2, Tst12/3, Tst12/4, Tst12/5, Tst12/6, Tst12/7, Tst12/8" + } + } + }` + 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(platJson) + + if err := parseThisThenThat("/path/to/nonexistent/file", file.Name()); err != nil { + t.Errorf("Unexpected error parsing platform json: %v", err) + } +} + +func TestMalformedHwskuJsonFile(t *testing.T) { + saveHwskuConfig() + defer restoreHwskuConfig() + // Missing comma creates an invalid json + if err := loadTestHwskuJson(t, `{"interfaces": {"eth1":{} "eth2":{} }}`); err == nil { + t.Errorf("No error generated for malformed platform.json file") + } + + // interfaces should be a dictionary + if err := loadTestHwskuJson(t, `{"interfaces": 1}`); err == nil { + t.Errorf("No error generated for malformed platform.json file") + } + + // interface should be a dictionary + if err := loadTestHwskuJson(t, `{"interfaces": { "eth1": 1 }}`); err == nil { + t.Errorf("No error generated for malformed platform.json file") + } + + // interface dictionary key should be a string + if err := loadTestHwskuJson(t, `{"interfaces": { "eth1": { 1: 2} }}`); err == nil { + t.Errorf("No error generated for malformed 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/7/1":{ "index":"" } }}`, + `{"interfaces": {"ethernet1/7/1":{ "xedni":"a" } }}`, + // Index should be an integer + `{"interfaces": {"ethernet1/7/1":{ "index":"NotAnInteger" } }}`, + // Index should match the name + `{"interfaces": {"ethernet1/7/1":{ "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 TestMalformedBrkoutMode(t *testing.T) { + savePlatformConfig() + defer restorePlatformConfig() + badJsons := []string{ + // Breakout mode should not be empty + `{"interfaces": {"ethernet1/7/1":{ "index":"7", "breakout_modes":"" } }}`, + `{"interfaces": {"ethernet1/7/1":{ "index":"7" } }}`, + // Default mode should be sane if it is present + `{"interfaces": {"ethernet1/7/1":{ "index":"7", "breakout_modes":"1x400G", "default_brkout_mode":"nonsense mode" } }}`, + // Default mode should be a single speed if it is present + `{"interfaces": {"ethernet1/7/1":{ "index":"7", "breakout_modes":"1x400G[200G,100G]", "default_brkout_mode":"1x400G[200G]" } }}`, + // Modes should be sane + `{"interfaces": {"ethernet1/7/1":{ "index":"7", "breakout_modes":"1x400G, 2x400G, 2xOneHundredG" } }}`, + } + + 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/7/1":{ "index":"7", "breakout_modes":"1x400G", "lanes":"a,b,c" } }}`, + // Lanes should be have empty entries + `{"interfaces": {"ethernet1/7/1":{ "index":"7", "breakout_modes":"1x400G", "lanes":"1,2,,3" } }}`, + `{"interfaces": {"ethernet1/7/1":{ "index":"7", "breakout_modes":"1x400G", "lanes":"1,2,3," } }}`, + `{"interfaces": {"ethernet1/7/1":{ "index":"7", "breakout_modes":"1x400G", "lanes":",1,2,3" } }}`, + // Should have enough lanes for all children + `{"interfaces": {"ethernet1/7/1":{ "index":"7,7,7", "breakout_modes":"1x400G", "lanes": "12,13", "alias_at_lanes": "foo,bar,baz"}, + "ethernet1/7/2":{ "index":"7", "breakout_modes":"1x400G"}, + "ethernet1/7/3":{ "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/7/2":{ "index":"7", "breakout_modes":"1x400G" } }}`, + // Primary intf isn't a primary + `{"interfaces": {"ethernet1/7/1":{ "index":"7", "breakout_modes":"1x400G" }, "ethernet1/7/2":{ "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{ + // Subindex references bad alias + `{"interfaces": {"ethernet1/7/1":{ "index":"7,7", "breakout_modes":"1x400G", "lanes": "12,13", "alias_at_lanes": "foo, bar" }, + "ethernet1/7/100":{ "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 TestSanitizeBreakoutMode(t *testing.T) { + good := []string{"1x400G", "2x800G[400G]", "8x200G[100G,50G]", "4x800G[100G,50G,40G]", "2x100G(2)+6x200G(6)", "2x200G[100G,50G](2)+6x200G(6)", "2x200G[100G,50G](2)+6x200G[100G](6)", "3x200G(6)+1x800G[400G](2)"} + bad_single := []string{"1X400G", "1x400g", "400G", "ax400G", "x400G", "1x4O0G", "1x[400G,100G]", "4x800G[100G, 50G, 40G]", "1x400G[]", "1x400G[", "1x400G]", "1x400G(100G,200G)", "1x400G[200G 100G]", "1x400G[200, 100]", "1x400G+1x200G"} + bad_mixed := []string{"1x400G+1x200G", "2x400G[200G]+4x100G", "2x400G+4x200G[100G]", "2x400G[200G,100G]+4x200G[100G]", "1x400G(a)+1x200G(4)", "1x400G(2)+1x200G)4("} + bad := append(bad_single, bad_mixed...) + + for _, mode := range good { + if _, ok := sanitizeBreakoutMode(mode); !ok { + t.Errorf("Error generated for valid mode: \"%s\"", mode) + } + } + for _, mode := range bad { + if _, ok := sanitizeBreakoutMode(mode); ok { + t.Errorf("No error generated for invalid mode: \"%s\"", mode) + } + } +} + +func TestBrkoutModeToSpeeds(t *testing.T) { + var cases = []struct { + mode string + speeds []int + }{ + {mode: "1x400G", speeds: []int{400}}, + {mode: "2x400G[200G,100G]", speeds: []int{100, 200, 400}}, + {mode: "4x200G[100G]", speeds: []int{100, 200}}, + {mode: "1x400G(4)+2x200G(4)", speeds: []int{200, 400}}, + {mode: "2x200G(4)+1x400G(4)", speeds: []int{200, 400}}, + {mode: "8x100G", speeds: []int{100}}, + {mode: "2x800G[400G,200G](4)+1x400G(4)", speeds: []int{200, 400, 800}}, + } + for _, c := range cases { + speeds, err := brkoutModeToSpeeds(c.mode) + if err != nil { + t.Errorf("Unexpected error %v for mode %s", err, c.mode) + } + if len(speeds) != len(c.speeds) { + t.Errorf("Unexpected speeds %v for mode %s", speeds, c.mode) + } + for i, _ := range speeds { + if speeds[i] != c.speeds[i] { + t.Errorf("Unexpected speeds %v for mode %s", speeds, c.mode) + } + } + } +} + +func TestPlatIntfAlias(t *testing.T) { + savePlatformConfig() + defer restorePlatformConfig() + + platJson := `{ + "interfaces": { + "Ethernet1/1/1": { + "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" + }, + "Ethernet1/1/2": { "index": "1" }, + "Ethernet1/1/3": { "index": "1" }, + "Ethernet1/1/4": { "index": "1" }, + "Ethernet1/1/5": { "index": "1" }, + "Ethernet1/1/6": { "index": "1" }, + "Ethernet1/1/7": { "index": "1" }, + "Ethernet1/1/8": { "index": "1" } + } + }` + + if err := loadTestJson(t, platJson); err != nil { + t.Fatalf("Error loading test json: %v", err) + } + + for i := 1; i <= 8; i++ { + intf := fmt.Sprintf("Ethernet1/1/%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 laneSliceEqual(a, b []int) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestDoParsePlatformJsonSonic_Success(t *testing.T) { + platformCfg.intfs = make(map[string]*platformIntf) + intfToDefaultMode = map[string]string{ + "Ethernet1/1/1": "8x100G", + } + + inputJson := map[string]interface{}{ + "Ethernet1/1/1": map[string]interface{}{ + "index": "1,1,1,1,1,1,1,1", + "lanes": "9,10,11,12,13,14,15,16", + "breakout_modes": map[string]interface{}{ + "8x100G": []interface{}{ + "Ethernet1/1/1", "Ethernet1/1/2", "Ethernet1/1/3", "Ethernet1/1/4", + "Ethernet1/1/5", "Ethernet1/1/6", "Ethernet1/1/7", "Ethernet1/1/8", + }, + }, + }, + } + + err := doParsePlatformJsonSonic(inputJson) + if err != nil { + t.Fatalf("Expected doParsePlatformJsonSonic to succeed, got error: %v", err) + } + + intf1, ok := platformCfg.intfs["Ethernet1/1/1"] + if !ok { + t.Fatalf("Expected interface 'Ethernet1/1/1' to be populated in platformCfg") + } + if !intf1.isPrimary { + t.Errorf("Expected Ethernet1/1/1 to be marked as primary") + } + if intf1.dfltMode != "8x100G" { + t.Errorf("Expected default mode to be '8x100G', got '%s'", intf1.dfltMode) + } + + intf2, ok := platformCfg.intfs["Ethernet1/1/2"] + if !ok { + t.Fatalf("Expected child interface 'Ethernet1/1/2' to be populated") + } + if intf2.isPrimary { + t.Errorf("Expected Ethernet1/1/2 to NOT be marked as primary") + } + if intf2.primary != intf1 { + t.Errorf("Expected child interface primary reference to link back to parent entry block") + } + + portName, ok := platformCfg.intfNameToPortName["Ethernet1/1/1"] + if !ok || portName != "1/1" { + t.Errorf("Expected map translation registration to match '1/1', got '%s'", portName) + } +} + +func TestDoParsePlatformJsonSonic_MalformedName(t *testing.T) { + platformCfg.intfs = make(map[string]*platformIntf) + inputJson := map[string]interface{}{ + "InvalidNameOnly": map[string]interface{}{ + "index": "1", + "lanes": "1", + "breakout_modes": map[string]interface{}{ + "1x100G": []interface{}{"InvalidNameOnly"}, + }, + }, + } + + err := doParsePlatformJsonSonic(inputJson) + if err == nil { + t.Errorf("Expected failure error on an interface string with an invalid token forward-slash layout pattern") + } +} + +func TestChannelOffset(t *testing.T) { + platformCfg.intfs = make(map[string]*platformIntf) + + targetIntf := "Ethernet1/1/1" + var expectedOffset uint16 = 3 + + platformCfg.intfs[targetIntf] = &platformIntf{ + name: targetIntf, + channelOffset: expectedOffset, + } + + t.Run("ValidInterface", func(t *testing.T) { + offset, err := ChannelOffset(targetIntf) + if err != nil { + t.Errorf("Unexpected error for %s: %v", targetIntf, err) + } + if offset != expectedOffset { + t.Errorf("Expected offset %d, got %d", expectedOffset, offset) + } + }) + + t.Run("InvalidInterface", func(t *testing.T) { + missingIntf := "Ethernet1/99/1" + _, err := ChannelOffset(missingIntf) + if err == nil { + t.Errorf("Expected an error for non-existent interface %s, but got nil", missingIntf) + } + }) +} + +func TestHwskuParsing(t *testing.T) { + saveHwskuConfig() + defer restoreHwskuConfig() + hwskuJson := + `{ + "interfaces": { + "Ethernet1/1/1": { + "default_brkout_mode": "1x800G" + }, + "Ethernet1/2/1": { + "default_brkout_mode": "2x800G" + }, + "Ethernet1/3/1": { + "default_brkout_mode": "3x800G" + }, + "Ethernet1/4/1": { + "default_brkout_mode": "4x800G" + } + } + }` + if err := loadTestHwskuJson(t, hwskuJson); err != nil { + t.Fatalf("Error %v loading hwsku json", err) + } + + expected := map[string]string{ + "Ethernet1/1/1": "1x800G", + "Ethernet1/2/1": "2x800G", + "Ethernet1/3/1": "3x800G", + "Ethernet1/4/1": "4x800G", + } + if diff := cmp.Diff(expected, intfToDefaultMode); diff != "" { + t.Errorf("unexpected diff (-want +got):\n%s", diff) + } +} diff --git a/translib/transformer/xfmr_intf.go b/translib/transformer/xfmr_intf.go index 560946911..585b9e6c4 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 @@ -1520,41 +1536,42 @@ var DbToYang_intf_get_counters_xfmr SubTreeXfmrDbToYang = func(inParams XfmrPara } var Subscribe_intf_get_counters_xfmr SubTreeXfmrSubscribe = func(inParams XfmrSubscInParams) (XfmrSubscOutParams, error) { - var err error - var result XfmrSubscOutParams - - if inParams.subscProc == TRANSLATE_SUBSCRIBE { - log.Info("Subscribe_intf_get_counters_xfmr: inParams.subscProc: ", inParams.subscProc) + log.Info("Entering Subscribe_intf_get_counters_xfmr") - pathInfo := NewPathInfo(inParams.uri) - targetUriPath := pathInfo.YangPath + result := XfmrSubscOutParams{ + isVirtualTbl: false, + needCache: true, + onChange: OnchangeDisable, + dbDataMap: make(RedisDbSubscribeMap), + nOpts: ¬ificationOpts{mInterval: 1, pType: Sample}, + } - log.Infof("Subscribe_intf_get_counters_xfmr:- URI:%s pathinfo:%s ", inParams.uri, pathInfo.Path) - log.Infof("Subscribe_intf_get_counters_xfmr:- Target URI path:%s", targetUriPath) + defer log.Info("Returning Subscribe_intf_get_counters_xfmr, result:", result) - // to handle the TRANSLATE_SUBSCRIBE - result.nOpts = new(notificationOpts) - result.nOpts.pType = Sample - result.nOpts.mInterval = 30 - result.isVirtualTbl = false - result.needCache = true - - ifName := pathInfo.Var("name") - log.Info("Subscribe_intf_get_counters_xfmr: ifName: ", ifName) + pathInfo := NewPathInfo(inParams.uri) + ifName := pathInfo.Var("name") - if ifName == "" || ifName == "*" { - if strings.HasPrefix(targetUriPath, "/openconfig-interfaces:interfaces/interface/openconfig-if-ethernet:ethernet/state/counters") { - ifName = "Eth" + "*" - } else { - ifName = "*" - } + if ifName != "*" { + 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: {}}}} + return result, nil + } - log.Info("Subscribe_intf_eth_port_config_xfmr: result ", result) + // wildcard key + 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: {}} } - return result, err + + return result, nil } var DbToYangPath_intf_get_counters_path_xfmr PathXfmrDbToYangFunc = func(params XfmrDbToYgPathParams) error { @@ -1580,7 +1597,24 @@ var DbToYangPath_intf_get_counters_path_xfmr PathXfmrDbToYangFunc = func(params } var Subscribe_intf_get_ether_counters_xfmr SubTreeXfmrSubscribe = func(inParams XfmrSubscInParams) (XfmrSubscOutParams, error) { - return Subscribe_intf_get_counters_xfmr(inParams) + log.Info("Entering Subscribe_intf_get_ether_counters_xfmr") + + result := XfmrSubscOutParams{ + isVirtualTbl: false, + needCache: true, + onChange: OnchangeDisable, + dbDataMap: make(RedisDbSubscribeMap), + nOpts: ¬ificationOpts{mInterval: 1, pType: Sample}, // Counters can only support Sample. + } + + ifName := NewPathInfo(inParams.uri).Var("name") + if ifName == "" { + ifName = "*" + } + result.dbDataMap = RedisDbSubscribeMap{db.ConfigDB: {"PORT": {ifName: {}}}} + log.Infof("Returning Subscribe_intf_get_ether_counters_xfmr, result.dbDataMap=%v", result.dbDataMap) + + return result, nil } var populatePortCounters PopulateIntfCounters = func(inParams XfmrParams, counter interface{}) error { @@ -3967,3 +4001,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 +} + +// sfp_type_to_max_lanes_map mapping pulled from third_party/sonic-platform-daemons/sonic-xcvrd/xcvrd/xcvrd.py +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 { + 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..af62b2039 --- /dev/null +++ b/translib/transformer/xfmr_intf_test.go @@ -0,0 +1,1326 @@ +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 TestSubscribe_intf_get_counters_xfmr(t *testing.T) { + tests := []struct { + name string + uri string + setupMock func() + expectError bool + errorMsg string + validateOut func(t *testing.T, res XfmrSubscOutParams) + }{ + { + name: "Success - Specific Interface Key Subscription Mapping", + uri: "/openconfig-interfaces:interfaces/interface[name=Ethernet202]/state/counters", + setupMock: func() {}, + expectError: false, + validateOut: func(t *testing.T, res XfmrSubscOutParams) { + if res.needCache != true || res.onChange != OnchangeDisable { + t.Errorf("Subscription parameter flags were incorrectly assigned") + } + + tblMap, exists := res.dbDataMap[db.ConfigDB] + if !exists { + t.Fatalf("ConfigDB key missing from output subscription map framework") + } + + if len(tblMap) == 0 { + t.Fatalf("Expected at least one table entry in the configuration subscription map") + } + + var foundIntf bool + for tblName, interfaceMap := range tblMap { + if _, ok := interfaceMap["Ethernet202"]; ok { + foundIntf = true + t.Logf("Successfully verified interface entry inside table: %s", tblName) + break + } + } + + if !foundIntf { + t.Errorf("Expected subscription path for interface key 'Ethernet202' not found across generated tables") + } + }, + }, + { + name: "Failure - Invalid Interface String Target", + uri: "/openconfig-interfaces:interfaces/interface[name=InvalidIntf99]/state/counters", + setupMock: func() {}, + expectError: true, + errorMsg: "Invalid interface:", + }, + { + name: "Success - Wildcard Interface Pattern Expansion Mapping", + uri: "/openconfig-interfaces:interfaces/interface[name=*]/state/counters", + setupMock: func() {}, + expectError: false, + validateOut: func(t *testing.T, res XfmrSubscOutParams) { + tblMap, exists := res.dbDataMap[db.ConfigDB] + if !exists { + t.Fatalf("ConfigDB subscription data framework initialization skipped") + } + if len(tblMap) == 0 { + t.Errorf("Wildcard loop expansion generated an empty subscription schema map") + } + for _, tbl := range tblMap { + if _, hasWildcard := tbl["*"]; !hasWildcard { + t.Errorf("Wildcard interface key marker '*' missing from generated sub-table layout") + } + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setupMock() + + inParams := XfmrSubscInParams{ + uri: tt.uri, + } + + res, err := Subscribe_intf_get_counters_xfmr(inParams) + + if tt.expectError { + if err == nil { + t.Fatalf("Expected subscription configuration fault but function returned success") + } + if !strings.Contains(err.Error(), tt.errorMsg) { + t.Errorf("Error string signature mismatch.\nExpected containing: %q\nGot: %q", tt.errorMsg, err.Error()) + } + return + } + + if err != nil { + t.Fatalf("Unexpected execution fault returned during parameters layout calculation: %v", err) + } + + if tt.validateOut != nil { + tt.validateOut(t, res) + } + }) + } +} + +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 TestSubscribe_intf_get_ether_counters_xfmr(t *testing.T) { + tests := []struct { + name string + uri string + expectError bool + validateOut func(t *testing.T, res XfmrSubscOutParams) + }{ + { + name: "Success - Specific Interface Extraction Path", + uri: "/openconfig-interfaces:interfaces/interface[name=Ethernet202]/ethernet/state/counters", + expectError: false, + validateOut: func(t *testing.T, res XfmrSubscOutParams) { + if res.needCache != true || res.onChange != OnchangeDisable { + t.Errorf("Subscription parameter attributes were incorrectly initialized") + } + + tblMap, exists := res.dbDataMap[db.ConfigDB] + if !exists { + t.Fatalf("ConfigDB key missing from generated subscription map") + } + + interfaceMap, hasPortTbl := tblMap["PORT"] + if !hasPortTbl { + t.Fatalf("Expected target database table 'PORT' was not generated") + } + + if _, ok := interfaceMap["Ethernet202"]; !ok { + t.Errorf("Subscription key entry for interface 'Ethernet202' was not found") + } + }, + }, + { + name: "Success - Fallback to Wildcard Expansion when Name is Empty", + uri: "/openconfig-interfaces:interfaces/interface/ethernet/state/counters", + expectError: false, + validateOut: func(t *testing.T, res XfmrSubscOutParams) { + tblMap, exists := res.dbDataMap[db.ConfigDB] + if !exists { + t.Fatalf("ConfigDB key missing from empty path subscription layout") + } + + interfaceMap, hasPortTbl := tblMap["PORT"] + if !hasPortTbl { + t.Fatalf("Expected target database table 'PORT' was missing from fallback setup") + } + + if _, ok := interfaceMap["*"]; !ok { + t.Errorf("Expected wildcard string modifier '*' fallback was not assigned when name segment was missing") + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + inParams := XfmrSubscInParams{ + uri: tt.uri, + } + + res, err := Subscribe_intf_get_ether_counters_xfmr(inParams) + + if tt.expectError && err == nil { + t.Fatalf("Expected subscription execution failure state but function completed successfully") + } + if !tt.expectError && err != nil { + t.Fatalf("Unexpected execution context error returned: %v", err) + } + + if tt.validateOut != nil { + tt.validateOut(t, res) + } + }) + } +} + +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", + }, + } + + 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) +}