diff --git a/config/transformer/models_list b/config/transformer/models_list index 32f43e52e..ab1eea166 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 @@ -16,3 +17,4 @@ gnsi-authz.yang gnsi-pathz.yang gnsi-certz.yang gnsi-credentialz.yang +sonic-port.yang diff --git a/models/yang/annotations/openconfig-interfaces-annot.yang b/models/yang/annotations/openconfig-interfaces-annot.yang index 4eeee0a3b..61a940de8 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,137 @@ 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"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-intf:oper-status { + deviate add { + sonic-ext:field-transformer "intf_oper_status_xfmr"; + sonic-ext:composite-field-names "PORT_TABLE:oper_status,PORT_TABLE:presence,PORT_TABLE:loopback_mode,PORT_TABLE:under_test,PORT_TABLE:unusable,MGMT_PORT_TABLE:oper_status,LAG_TABLE:oper_status,LAG_TABLE:under_test,BRIDGE_TABLE:oper_status,INTF_TABLE:oper_status"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-eth:ethernet/oc-eth:state/oc-eth:port-speed { + deviate add { + sonic-ext:field-transformer "intf_eth_port_speed_xfmr"; + sonic-ext:field-name "speed"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:config/oc-intf:enabled { + deviate add { + sonic-ext:field-transformer "intf_enabled_xfmr"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-intf:enabled { + deviate add { + sonic-ext:field-transformer "intf_enabled_xfmr"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-intf:loopback-mode { + deviate add { + sonic-ext:field-transformer "intf_loopback_mode_xfmr"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:config/oc-intf:loopback-mode { + deviate add { + sonic-ext:field-transformer "intf_loopback_mode_xfmr"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:config/oc-intf:mtu { + deviate add { + sonic-ext:field-transformer "intf_mtu_xfmr"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-intf:mtu { + deviate add { + sonic-ext:field-transformer "intf_mtu_xfmr"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-intf:management { + deviate add { + sonic-ext:field-transformer "intf_mgmt_xfmr"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-intf:cpu { + deviate add { + sonic-ext:field-transformer "intf_cpu_xfmr"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-intf:description { + deviate add { + sonic-ext:field-transformer "intf_description_xfmr"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-eth:ethernet/oc-eth:state/oc-eth:mac-address { + deviate add { + sonic-ext:field-transformer "intf_eth_mac_address_xfmr"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-intf:counters { + deviate add { + sonic-ext:subtree-transformer "intf_get_counters_xfmr"; + sonic-ext:db-name "COUNTERS_DB"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-eth:ethernet/oc-eth:state/oc-eth:counters { + deviate add { + sonic-ext:db-name "COUNTERS_DB"; + sonic-ext:subtree-transformer "intf_get_ether_counters_xfmr"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:subinterfaces/oc-intf:subinterface/oc-ip:ipv4/oc-ip:state/oc-ip:counters { + deviate add { + sonic-ext:db-name "COUNTERS_DB"; + sonic-ext:subtree-transformer "intf_ipv4_counters_xfmr"; + } + } + + deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:subinterfaces/oc-intf:subinterface/oc-ip:ipv6/oc-ip:state/oc-ip:counters { + deviate add { + sonic-ext:db-name "COUNTERS_DB"; + sonic-ext:subtree-transformer "intf_ipv6_counters_xfmr"; + } + } } diff --git a/models/yang/extensions/openconfig-interfaces-deviation.yang b/models/yang/extensions/openconfig-interfaces-deviation.yang index 097100f66..35c7584b2 100644 --- a/models/yang/extensions/openconfig-interfaces-deviation.yang +++ b/models/yang/extensions/openconfig-interfaces-deviation.yang @@ -33,10 +33,6 @@ module openconfig-interfaces-deviation { reference "0.1.0"; } - deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:config/oc-intf:loopback-mode { - deviate not-supported; - } - deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:config/oc-vlan:tpid { deviate not-supported; } @@ -45,14 +41,6 @@ module openconfig-interfaces-deviation { deviate not-supported; } - deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-intf:loopback-mode { - deviate not-supported; - } - - deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-intf:oper-status { - deviate not-supported; - } - deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:state/oc-intf:last-change { deviate not-supported; } @@ -77,10 +65,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; } @@ -145,14 +129,6 @@ module openconfig-interfaces-deviation { deviate not-supported; } - deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:subinterfaces/oc-intf:subinterface/oc-ip:ipv4/oc-ip:config { - deviate not-supported; - } - - deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:subinterfaces/oc-intf:subinterface/oc-ip:ipv4/oc-ip:state { - deviate not-supported; - } - deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:subinterfaces/oc-intf:subinterface/oc-ip:ipv6/oc-ip:addresses/oc-ip:address/oc-ip:state/oc-ip:origin { deviate not-supported; } @@ -207,10 +183,6 @@ module openconfig-interfaces-deviation { deviate not-supported; } - deviation /oc-intf:interfaces/oc-intf:interface/oc-intf:subinterfaces/oc-intf:subinterface/oc-ip:ipv6/oc-ip:state/oc-ip:counters { - deviate not-supported; - } - deviation /oc-intf:interfaces/oc-intf:interface/oc-eth:ethernet/oc-eth:config/oc-eth:mac-address { deviate not-supported; } diff --git a/models/yang/openconfig-interfaces.yang b/models/yang/openconfig-interfaces.yang index f3e0feeac..914ba680f 100644 --- a/models/yang/openconfig-interfaces.yang +++ b/models/yang/openconfig-interfaces.yang @@ -12,6 +12,7 @@ module openconfig-interfaces { import openconfig-yang-types { prefix oc-yang; } import openconfig-types { prefix oc-types; } import openconfig-extensions { prefix oc-ext; } + import openconfig-transport-types { prefix oc-opt-types; } // meta organization "OpenConfig working group"; @@ -371,8 +372,7 @@ module openconfig-interfaces { } leaf loopback-mode { - type boolean; - default false; + type oc-opt-types:loopback-mode-type; description "When set to true, the interface is logically looped back, such that packets that are forwarded via the interface 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/models/yang/sonic/sonic-port.yang b/models/yang/sonic/sonic-port.yang index 4050bd370..720899a12 100644 --- a/models/yang/sonic/sonic-port.yang +++ b/models/yang/sonic/sonic-port.yang @@ -6,6 +6,14 @@ module sonic-port { prefix cmn; } + import ietf-yang-types { + prefix yang; + } + + import sonic-common { + prefix scommon; + } + organization "SONiC"; @@ -20,6 +28,15 @@ module sonic-port { "Initial revision."; } + typedef loopback-mode-type { + type enumeration { + enum none; + enum mac_local; + enum mac_remote; + enum phy_local; + enum phy_remote; + } + } container sonic-port { @@ -78,6 +95,14 @@ module sonic-port { default 9100; } + leaf oper_status { + type scommon:oper-status; + } + + leaf id { + type uint32; + } + leaf lanes { type string; mandatory true; @@ -87,6 +112,16 @@ module sonic-port { type cmn:admin-status; default "down"; } + + leaf loopback-mode { + type loopback-mode-type; + default none; + } + + leaf mac-address { + type yang:mac-address; + } + } } } 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/db/db.go b/translib/db/db.go index 87b241014..05a2d333f 100644 --- a/translib/db/db.go +++ b/translib/db/db.go @@ -143,6 +143,7 @@ const ( SnmpDB // 7 ErrorDB // 8 EventDB // 9 + ApplStateDB DBNum = 14 // 14 // All DBs added above this line, please ---- MaxDB // The Number of DBs ) @@ -365,6 +366,8 @@ func getDBInstName(dbNo DBNum) string { return "ERROR_DB" case EventDB: return "EVENT_DB" + case ApplStateDB: + return "APPL_STATE_DB" } return "" } @@ -390,6 +393,8 @@ func GetdbNameToIndex(dbName string) DBNum { dbIndex = ErrorDB case "EVENT_DB": dbIndex = EventDB + case "APPL_STATE_DB": + dbIndex = ApplStateDB } return dbIndex } 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..dc8b4b1e4 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) @@ -89,6 +96,17 @@ func init() { XlateFuncBind("YangToDb_intf_routed_vlan_name_xfmr", YangToDb_intf_routed_vlan_name_xfmr) XlateFuncBind("YangToDb_routed_vlan_ip_addr_xfmr", YangToDb_routed_vlan_ip_addr_xfmr) XlateFuncBind("DbToYang_routed_vlan_ip_addr_xfmr", DbToYang_routed_vlan_ip_addr_xfmr) + + XlateFuncBind("DbToYang_intf_cpu_xfmr", DbToYang_intf_cpu_xfmr) + XlateFuncBind("DbToYang_intf_mgmt_xfmr", DbToYang_intf_mgmt_xfmr) + XlateFuncBind("DbToYang_intf_oper_status_xfmr", DbToYang_intf_oper_status_xfmr) + XlateFuncBind("DbToYang_intf_description_xfmr", DbToYang_intf_description_xfmr) + XlateFuncBind("DbToYang_pins_ifindex_xfmr", DbToYang_pins_ifindex_xfmr) + XlateFuncBind("DbToYang_intf_ipv4_counters_xfmr", DbToYang_intf_ipv4_counters_xfmr) + XlateFuncBind("DbToYang_intf_ipv6_counters_xfmr", DbToYang_intf_ipv6_counters_xfmr) + XlateFuncBind("YangToDb_intf_loopback_mode_xfmr", YangToDb_intf_loopback_mode_xfmr) + XlateFuncBind("DbToYang_intf_loopback_mode_xfmr", DbToYang_intf_loopback_mode_xfmr) + XlateFuncBind("DbToYang_intf_eth_mac_address_xfmr", DbToYang_intf_eth_mac_address_xfmr) } const ( @@ -112,6 +130,20 @@ const ( ETHERNET = "Eth" PORTCHANNEL = "PortChannel" VLAN = "Vlan" + CPU = "CPU" +) + +const ( + HARDWARE_PORT = "hardware-port" + PORT_INDEX = "index" + PORT_UNDER_TEST = "under_test" + PORT_OPER_STATUS = "oper_status" + LAG_MEMBER_TABLE_TN = "LAG_MEMBER_TABLE" + PORT_LOOPBACK_MODE = "loopback-mode" + LOOPBACK_TN = "LOOPBACK" + LOOPBACK_INTERFACE_TN = "LOOPBACK_INTERFACE" + PORT_PRESENCE = "presence" + PORT_MAC_ADDR = "mac-address" ) type TblData struct { @@ -121,7 +153,7 @@ type TblData struct { keySep string } -type PopulateIntfCounters func(inParams XfmrParams, counters interface{}) error +type PopulateIntfCounters func(inParams XfmrParams, itfName string, counters interface{}) error type CounterData struct { OIDTN string @@ -132,6 +164,7 @@ type CounterData struct { type IntfTblData struct { cfgDb TblData appDb TblData + appStateDb TblData stateDb TblData CountersHdl CounterData } @@ -152,10 +185,17 @@ var IntfTypeTblMap = map[E_InterfaceType]IntfTblData{ cfgDb: TblData{portTN: "VLAN", memberTN: "VLAN_MEMBER", intfTN: "VLAN_INTERFACE", keySep: PIPE}, appDb: TblData{portTN: "VLAN_TABLE", memberTN: "VLAN_MEMBER_TABLE", intfTN: "INTF_TABLE", keySep: COLON}, }, + IntfTypeCpu: IntfTblData{ + cfgDb: TblData{portTN: "CPU_PORT", keySep: PIPE}, + appDb: TblData{portTN: "PORT_TABLE", keySep: COLON}, + appStateDb: TblData{portTN: "PORT_TABLE", keySep: COLON}, + stateDb: TblData{portTN: "PORT_TABLE", keySep: PIPE}, + CountersHdl: CounterData{OIDTN: "COUNTERS_PORT_NAME_MAP", CountersTN: "COUNTERS", PopulateCounters: populatePortCounters}, + }, } var dbIdToTblMap = map[db.DBNum][]string{ - db.ConfigDB: {"PORT", "PORTCHANNEL", "VLAN"}, + db.ConfigDB: {"PORT", "PORTCHANNEL", "VLAN", "CPU_PORT"}, db.ApplDB: {"PORT_TABLE", "LAG_TABLE"}, db.StateDB: {"PORT_TABLE", "LAG_TABLE"}, } @@ -183,6 +223,7 @@ const ( IntfTypeEthernet E_InterfaceType = 1 IntfTypePortChannel E_InterfaceType = 2 IntfTypeVlan E_InterfaceType = 3 + IntfTypeCpu E_InterfaceType = 4 ) type E_InterfaceSubType int64 @@ -200,6 +241,8 @@ func getIntfTypeByName(name string) (E_InterfaceType, E_InterfaceSubType, error) return IntfTypePortChannel, IntfSubTypeUnset, err } else if strings.HasPrefix(name, VLAN) { return IntfTypeVlan, IntfSubTypeUnset, err + } else if strings.HasPrefix(name, CPU) { + return IntfTypeCpu, IntfSubTypeUnset, err } else { err = errors.New("Interface name prefix not matched with supported types") return IntfTypeUnset, IntfSubTypeUnset, err @@ -696,6 +739,9 @@ var DbToYang_intf_enabled_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (ma return result, errors.New("Invalid interface type IntfTypeUnset") } + if intfType == IntfTypeCpu { + return nil, errors.New("DbToYang_intf_enabled_xfmr: Invalid Interface Type") + } intTbl := IntfTypeTblMap[intfType] tblName, _ := getPortTableNameByDBId(intTbl, inParams.curDb) @@ -764,6 +810,25 @@ var DbToYang_intf_name_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (map[s return res_map, nil } +func updateDefaultLoopbackMode(inParams *XfmrParams, ifName *string, ifType E_InterfaceType, resMap map[string]string) error { + subOpMap := make(map[db.DBNum]map[string]map[string]db.Value) + intfMap := make(map[string]map[string]db.Value) + + intTbl, ok := IntfTypeTblMap[ifType] + if !ok { + log.Info("updateDefaultLoopbackMode interface type not found : ", ifType) + return errors.New("interface type not found.") + } + resMap[PORT_LOOPBACK_MODE] = "none" + + intfMap[intTbl.cfgDb.portTN] = make(map[string]db.Value) + intfMap[intTbl.cfgDb.portTN][*ifName] = db.Value{Field: resMap} + + subOpMap[db.ConfigDB] = intfMap + inParams.subOpDataMap[UPDATE] = &subOpMap + return nil +} + var YangToDb_intf_mtu_xfmr FieldXfmrYangToDb = func(inParams XfmrParams) (map[string]string, error) { res_map := make(map[string]string) var ifName string @@ -1314,7 +1379,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 +1423,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 @@ -1511,7 +1580,7 @@ var DbToYang_intf_get_counters_xfmr SubTreeXfmrDbToYang = func(inParams XfmrPara state_counters = intfObj.State.Counters } - err = intTbl.CountersHdl.PopulateCounters(inParams, state_counters) + err = intTbl.CountersHdl.PopulateCounters(inParams, "", state_counters) if log.V(3) { log.Info("DbToYang_intf_get_counters_xfmr - ", state_counters) } @@ -1520,41 +1589,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 + log.Info("Entering Subscribe_intf_get_counters_xfmr") - if inParams.subscProc == TRANSLATE_SUBSCRIBE { - log.Info("Subscribe_intf_get_counters_xfmr: inParams.subscProc: ", inParams.subscProc) - - 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,13 +1650,33 @@ 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 { +var populatePortCounters PopulateIntfCounters = func(inParams XfmrParams, ifName string, counter interface{}) error { var err error pathInfo := NewPathInfo(inParams.uri) - ifName := pathInfo.Var("name") + if ifName == "" { + ifName = pathInfo.Var("name") + } + //ifName := pathInfo.Var("name") targetUriPath := pathInfo.YangPath @@ -1671,6 +1761,10 @@ var DbToYang_intf_get_ether_counters_xfmr SubTreeXfmrDbToYang = func(inParams Xf log.Info("DbToYang_intf_get_ether_counters_xfmr - Invalid interface type IntfTypeUnset") return errors.New("Invalid interface type IntfTypeUnset") } + if intfType == IntfTypeCpu { + log.Infof("DbToYang_intf_get_ether_counters_xfmr - Ether Stats not supported for intfType %v", intfType) + return errors.New("Ethernet counters not supported.") + } if !strings.Contains(targetUriPath, "/openconfig-interfaces:interfaces/interface/ethernet/state/counters") && !strings.Contains(targetUriPath, "/openconfig-interfaces:interfaces/interface/openconfig-if-ethernet:ethernet/state/counters") { @@ -1698,7 +1792,7 @@ var DbToYang_intf_get_ether_counters_xfmr SubTreeXfmrDbToYang = func(inParams Xf ygot.BuildEmptyTree(intfObj.Ethernet.State.Counters) eth_counters = intfObj.Ethernet.State.Counters - return populatePortCounters(inParams, eth_counters) + return populatePortCounters(inParams, "", eth_counters) } var intf_post_xfmr PostXfmrFunc = func(inParams XfmrParams) error { @@ -3967,3 +4061,801 @@ 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 && intfType != IntfTypeCpu { + 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 && intfType != IntfTypeCpu { + 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.") +} + +var DbToYang_intf_cpu_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (map[string]interface{}, error) { + pathInfo := NewPathInfo(inParams.uri) + name := pathInfo.Var("name") + if name == "" { + return nil, errors.New("DbToYang_intf_cpu_xfmr : Interface KEY not present") + } + return map[string]interface{}{"cpu": name == CPU}, nil +} + +var DbToYang_intf_mgmt_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (map[string]interface{}, error) { + res_map := make(map[string]interface{}) + pathInfo := NewPathInfo(inParams.uri) + name := pathInfo.Var("name") + if name == "" { + errStr := "DbToYang_intf_mgmt_xfmr : Interface KEY not present" + return res_map, errors.New(errStr) + } + if name == CPU { + res_map["management"] = false + return res_map, nil + } + return res_map, nil +} + +var DbToYang_intf_oper_status_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (map[string]interface{}, error) { + ifName := keyFromInParamsOrUri(inParams, "name") + intfType, _, _ := getIntfTypeByName(ifName) + + // Helper to convert enum to string using ygot's ΛMap + operStatusToString := func(s ocbinds.E_OpenconfigInterfaces_Interfaces_Interface_State_OperStatus) string { + return ocbinds.E_OpenconfigInterfaces_Interfaces_Interface_State_OperStatus.ΛMap(s)["E_OpenconfigInterfaces_Interfaces_Interface_State_OperStatus"][int64(s)].Name + } + + switch intfType { + case IntfTypeUnset: + return nil, fmt.Errorf("Invalid interface - %s", ifName) + case IntfTypeCpu: + return nil, errors.New("DbToYang_intf_oper_status_xfmr: Unsupported interface type " + strconv.Itoa(int(intfType))) + } + + result := make(map[string]interface{}) + intTbl := IntfTypeTblMap[intfType] + dbName := db.ApplStateDB + tblName := intTbl.appStateDb.portTN + + entry, dbErr := inParams.dbs[dbName].GetEntry(&db.TableSpec{Name: tblName}, db.Key{Comp: []string{ifName}}) + if dbErr != nil { + return nil, dbErr + } + + // under_test attribute will be set to true when the link is under qualification + // in that case, gNMI will export the oper_status as testing regardless of the + // oper_status value + if test, ok := entry.Field[PORT_UNDER_TEST]; ok && test == "1" { + result["oper-status"] = operStatusToString(ocbinds.OpenconfigInterfaces_Interfaces_Interface_State_OperStatus_TESTING) + return result, nil + } + operStatus, ok := entry.Field[PORT_OPER_STATUS] + if !ok { + log.Info("Oper status field not found in DB for interface " + ifName) + return nil, errors.New("Oper status field not found in DB for interface " + ifName) + } + if operStatus == "up" { + if entry.Has("unusable") && entry.Get("unusable") == "true" { + result["oper-status"] = operStatusToString(ocbinds.OpenconfigInterfaces_Interfaces_Interface_State_OperStatus_DOWN) + return result, nil + } + result["oper-status"] = operStatusToString(ocbinds.OpenconfigInterfaces_Interfaces_Interface_State_OperStatus_UP) + return result, nil + } + + // If PORT_OPER_STATUS != up; use PORT_LOOPBACK_MODE and PORT_PRESENCE to derive if module is NOT_PRESENT + if intfType == IntfTypeEthernet { + loopback_mode, ok := entry.Field[PORT_LOOPBACK_MODE] + if !ok { + loopback_mode = "none" + } + presence, ok := entry.Field[PORT_PRESENCE] + if !ok || presence != "1" { + // ports configured with a "local" loopback-mode should ignore PORT_PRESENCE + if !strings.Contains(loopback_mode, "local") { + result["oper-status"] = operStatusToString(ocbinds.OpenconfigInterfaces_Interfaces_Interface_State_OperStatus_NOT_PRESENT) + return result, nil + } + } + } + var status ocbinds.E_OpenconfigInterfaces_Interfaces_Interface_State_OperStatus + switch operStatus { + case "down": + status = ocbinds.OpenconfigInterfaces_Interfaces_Interface_State_OperStatus_DOWN + case "testing": + status = ocbinds.OpenconfigInterfaces_Interfaces_Interface_State_OperStatus_TESTING + case "dormant": + status = ocbinds.OpenconfigInterfaces_Interfaces_Interface_State_OperStatus_DORMANT + case "lower_layer_down": + status = ocbinds.OpenconfigInterfaces_Interfaces_Interface_State_OperStatus_LOWER_LAYER_DOWN + default: + status = ocbinds.OpenconfigInterfaces_Interfaces_Interface_State_OperStatus_UNKNOWN + } + result["oper-status"] = operStatusToString(status) + return result, nil +} + +var DbToYang_intf_description_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} + } + intTbl, ok := IntfTypeTblMap[intfType] + if !ok { + return nil, errors.New("DbToYang_intf_description_xfmr: interface type not found " + strconv.Itoa(int(intfType))) + } + tblName, err := getPortTableNameByDBId(intTbl, inParams.curDb) + if err != nil { + return nil, errors.New("DbToYang_intf_description_xfmr table name not found : " + tblName) + } + prtInst, dbErr := getDBValues(inParams, tblName) + if dbErr != nil { + return nil, dbErr + } + result := make(map[string]interface{}) + if result["description"], ok = prtInst.Field["description"]; ok { + return result, nil + } + return nil, errors.New("description field not found in DB.") +} + +var DbToYang_pins_ifindex_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 == IntfTypeUnset { + return nil, errors.New("DbToYang_pins_ifindex_xfmr - interface type not supported for Config Id " + strconv.Itoa(int(intfType))) + } + + intTbl, ok := IntfTypeTblMap[intfType] + if !ok { + return nil, errors.New("DbToYang_pins_ifindex_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 intfType != IntfTypeEthernet && intfType != IntfTypePortChannel && intfType != IntfTypeCpu { + tblName, err = getPortTableNameByDBId(intTbl, inParams.curDb) + if err != nil { + return nil, errors.New("DbToYang_pins_ifindex_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["ifindex"] = uint32(idVal) + return resMap, nil + } + return nil, err + } + log.Info("DbToYang_pins_ifindex_xfmr: State Id field not found in DB.") + return nil, tlerr.NotFound("state ifindex field not found in DB.") +} + +func sumV4Counters(parent, member *ocbinds.OpenconfigInterfaces_Interfaces_Interface_Subinterfaces_Subinterface_Ipv4_State_Counters) { + if member.InDiscardedPkts != nil { + InDiscardedPkts := *member.InDiscardedPkts + if parent.InDiscardedPkts != nil { + InDiscardedPkts += *parent.InDiscardedPkts + } + parent.InDiscardedPkts = &InDiscardedPkts + } + if member.InErrorPkts != nil { + InErrorPkts := *member.InErrorPkts + if parent.InErrorPkts != nil { + InErrorPkts += *parent.InErrorPkts + } + parent.InErrorPkts = &InErrorPkts + } + if member.InForwardedOctets != nil { + InForwardedOctets := *member.InForwardedOctets + if parent.InForwardedOctets != nil { + InForwardedOctets += *parent.InForwardedOctets + } + parent.InForwardedOctets = &InForwardedOctets + } + if member.InForwardedPkts != nil { + InForwardedPkts := *member.InForwardedPkts + if parent.InForwardedPkts != nil { + InForwardedPkts += *parent.InForwardedPkts + } + parent.InForwardedPkts = &InForwardedPkts + } + if member.InOctets != nil { + InOctets := *member.InOctets + if parent.InOctets != nil { + InOctets += *parent.InOctets + } + parent.InOctets = &InOctets + } + if member.InPkts != nil { + InPkts := *member.InPkts + if parent.InPkts != nil { + InPkts += *parent.InPkts + } + parent.InPkts = &InPkts + } + if member.OutDiscardedPkts != nil { + OutDiscardedPkts := *member.OutDiscardedPkts + if parent.OutDiscardedPkts != nil { + OutDiscardedPkts += *parent.OutDiscardedPkts + } + parent.OutDiscardedPkts = &OutDiscardedPkts + } + if member.OutErrorPkts != nil { + OutErrorPkts := *member.OutErrorPkts + if parent.OutErrorPkts != nil { + OutErrorPkts += *parent.OutErrorPkts + } + parent.OutErrorPkts = &OutErrorPkts + } + if member.OutForwardedOctets != nil { + OutForwardedOctets := *member.OutForwardedOctets + if parent.OutForwardedOctets != nil { + OutForwardedOctets += *parent.OutForwardedOctets + } + parent.OutForwardedOctets = &OutForwardedOctets + } + if member.OutForwardedPkts != nil { + OutForwardedPkts := *member.OutForwardedPkts + if parent.OutForwardedPkts != nil { + OutForwardedPkts += *parent.OutForwardedPkts + } + parent.OutForwardedPkts = &OutForwardedPkts + } + if member.OutOctets != nil { + OutOctets := *member.OutOctets + if parent.OutOctets != nil { + OutOctets += *parent.OutOctets + } + parent.OutOctets = &OutOctets + } + if member.OutPkts != nil { + OutPkts := *member.OutPkts + if parent.OutPkts != nil { + OutPkts += *parent.OutPkts + } + parent.OutPkts = &OutPkts + } +} + +var DbToYang_intf_ipv4_counters_xfmr SubTreeXfmrDbToYang = func(inParams XfmrParams) error { + intfsObj := getIntfsRoot(inParams.ygRoot) + pathInfo := NewPathInfo(inParams.uri) + ifName := pathInfo.Var("name") + + intfType, _, ierr := getIntfTypeByName(ifName) + if intfType == IntfTypeUnset || ierr != nil { + return tlerr.InvalidArgsError{Format: "Invalid interface: " + ifName} + } + var intfObj *ocbinds.OpenconfigInterfaces_Interfaces_Interface + + if intfsObj != nil && intfsObj.Interface != nil && len(intfsObj.Interface) > 0 { + ok := false + if intfObj, ok = intfsObj.Interface[ifName]; !ok { + intfObj, _ = intfsObj.NewInterface(ifName) + } + ygot.BuildEmptyTree(intfObj) + } else { + ygot.BuildEmptyTree(intfsObj) + intfObj, _ = intfsObj.NewInterface(ifName) + ygot.BuildEmptyTree(intfObj) + } + + if _, ok := intfObj.Subinterfaces.Subinterface[uint32(0)]; !ok { + _, err := intfObj.Subinterfaces.NewSubinterface(uint32(0)) + if err != nil { + log.Info("DbToYang_intf_ipv4_counters_xfmr: Creation of subinterface subtree failed!") + return err + } + } + subIntf := intfObj.Subinterfaces.Subinterface[uint32(0)] + ygot.BuildEmptyTree(subIntf) + v4_counters := subIntf.Ipv4.State.Counters + + members, err := getMembers(inParams.dbs[db.StateDB], ifName) + if err != nil { + return fmt.Errorf("%w; getMembers() for %s failed", err, ifName) + } + + for _, member := range members { + var mcounters ocbinds.OpenconfigInterfaces_Interfaces_Interface_Subinterfaces_Subinterface_Ipv4_State_Counters + populatePortCounters(inParams, member, &mcounters) + sumV4Counters(v4_counters, &mcounters) + } + + return nil +} + +// Returns the members of intfName, or a slice of just intfName if it's a singleton +func getMembers(stateDb *db.DB, intfName string) ([]string, error) { + if !strings.HasPrefix(intfName, PORTCHANNEL) { + return []string{intfName}, nil + } + memKeys, err := stateDb.GetKeys(&db.TableSpec{Name: LAG_MEMBER_TABLE_TN + stateDb.Opts.KeySeparator + intfName}) + if err != nil { + return []string{intfName}, fmt.Errorf("%w; Unable to retrieve member keys for %s", err, intfName) + } + var members []string + for i := range memKeys { + members = append(members, memKeys[i].Get(1)) + } + return members, nil +} + +func sumV6Counters(parent, member *ocbinds.OpenconfigInterfaces_Interfaces_Interface_Subinterfaces_Subinterface_Ipv6_State_Counters) { + if member.InDiscardedPkts != nil { + InDiscardedPkts := *member.InDiscardedPkts + if parent.InDiscardedPkts != nil { + InDiscardedPkts += *parent.InDiscardedPkts + } + parent.InDiscardedPkts = &InDiscardedPkts + } + if member.InErrorPkts != nil { + InErrorPkts := *member.InErrorPkts + if parent.InErrorPkts != nil { + InErrorPkts += *parent.InErrorPkts + } + parent.InErrorPkts = &InErrorPkts + } + if member.InForwardedOctets != nil { + InForwardedOctets := *member.InForwardedOctets + if parent.InForwardedOctets != nil { + InForwardedOctets += *parent.InForwardedOctets + } + parent.InForwardedOctets = &InForwardedOctets + } + if member.InForwardedPkts != nil { + InForwardedPkts := *member.InForwardedPkts + if parent.InForwardedPkts != nil { + InForwardedPkts += *parent.InForwardedPkts + } + parent.InForwardedPkts = &InForwardedPkts + } + if member.InOctets != nil { + InOctets := *member.InOctets + if parent.InOctets != nil { + InOctets += *parent.InOctets + } + parent.InOctets = &InOctets + } + if member.InPkts != nil { + InPkts := *member.InPkts + if parent.InPkts != nil { + InPkts += *parent.InPkts + } + parent.InPkts = &InPkts + } + if member.OutDiscardedPkts != nil { + OutDiscardedPkts := *member.OutDiscardedPkts + if parent.OutDiscardedPkts != nil { + OutDiscardedPkts += *parent.OutDiscardedPkts + } + parent.OutDiscardedPkts = &OutDiscardedPkts + } + if member.OutErrorPkts != nil { + OutErrorPkts := *member.OutErrorPkts + if parent.OutErrorPkts != nil { + OutErrorPkts += *parent.OutErrorPkts + } + parent.OutErrorPkts = &OutErrorPkts + } + + if member.OutForwardedOctets != nil { + OutForwardedOctets := *member.OutForwardedOctets + if parent.OutForwardedOctets != nil { + OutForwardedOctets += *parent.OutForwardedOctets + } + parent.OutForwardedOctets = &OutForwardedOctets + } + if member.OutForwardedPkts != nil { + OutForwardedPkts := *member.OutForwardedPkts + if parent.OutForwardedPkts != nil { + OutForwardedPkts += *parent.OutForwardedPkts + } + parent.OutForwardedPkts = &OutForwardedPkts + } + if member.OutOctets != nil { + OutOctets := *member.OutOctets + if parent.OutOctets != nil { + OutOctets += *parent.OutOctets + } + parent.OutOctets = &OutOctets + } + if member.OutPkts != nil { + OutPkts := *member.OutPkts + if parent.OutPkts != nil { + OutPkts += *parent.OutPkts + } + parent.OutPkts = &OutPkts + } +} + +var DbToYang_intf_ipv6_counters_xfmr SubTreeXfmrDbToYang = func(inParams XfmrParams) error { + intfsObj := getIntfsRoot(inParams.ygRoot) + pathInfo := NewPathInfo(inParams.uri) + ifName := pathInfo.Var("name") + + intfType, _, ierr := getIntfTypeByName(ifName) + if intfType == IntfTypeUnset || ierr != nil { + return tlerr.InvalidArgsError{Format: "Invalid interface: " + ifName} + } + var intfObj *ocbinds.OpenconfigInterfaces_Interfaces_Interface + + if intfsObj != nil && intfsObj.Interface != nil && len(intfsObj.Interface) > 0 { + ok := false + if intfObj, ok = intfsObj.Interface[ifName]; !ok { + intfObj, _ = intfsObj.NewInterface(ifName) + } + ygot.BuildEmptyTree(intfObj) + } else { + ygot.BuildEmptyTree(intfsObj) + intfObj, _ = intfsObj.NewInterface(ifName) + ygot.BuildEmptyTree(intfObj) + } + + if _, ok := intfObj.Subinterfaces.Subinterface[uint32(0)]; !ok { + _, err := intfObj.Subinterfaces.NewSubinterface(uint32(0)) + if err != nil { + log.Info("DbToYang_intf_ipv6_counters_xfmr: Creation of subinterface subtree failed!") + return err + } + } + subIntf := intfObj.Subinterfaces.Subinterface[uint32(0)] + ygot.BuildEmptyTree(subIntf) + v6_counters := subIntf.Ipv6.State.Counters + + members, err := getMembers(inParams.dbs[db.StateDB], ifName) + if err != nil { + return fmt.Errorf("%w; getMembers() for %s failed", err, ifName) + } + + for _, member := range members { + var mcounters ocbinds.OpenconfigInterfaces_Interfaces_Interface_Subinterfaces_Subinterface_Ipv6_State_Counters + populatePortCounters(inParams, member, &mcounters) + sumV6Counters(v6_counters, &mcounters) + } + + return nil +} + +var YangToDb_intf_loopback_mode_xfmr FieldXfmrYangToDb = func(inParams XfmrParams) (map[string]string, error) { + ifName := keyFromInParamsOrUri(inParams, "name") + + intfType, _, ierr := getIntfTypeByName(ifName) + if intfType == IntfTypeUnset || ierr != nil { + return nil, tlerr.InvalidArgsError{Format: "Invalid interface: " + ifName} + } + resMap := make(map[string]string) + if inParams.oper == DELETE { + log.Infof("Updating the Interface: %s with default loopback-mode", ifName) + err := updateDefaultLoopbackMode(&inParams, &ifName, intfType, resMap) + if err != nil { + log.Infof("Updating Default loopback-mode for Interface: %s failed", ifName) + } + return resMap, err + } + mode, ok := inParams.param.(ocbinds.E_OpenconfigInterfaces_LoopbackModeType) + if !ok { + return nil, errors.New("YangToDb_intf_loopback_mode_xfmr, Error: Invalid parameter") + } + var enStr string + switch mode { + case ocbinds.OpenconfigInterfaces_LoopbackModeType_ASIC_MAC_LOCAL: + enStr = "mac_local" + case ocbinds.OpenconfigInterfaces_LoopbackModeType_ASIC_MAC_REMOTE: + enStr = "mac_remote" + case ocbinds.OpenconfigInterfaces_LoopbackModeType_ASIC_PHY_LOCAL: + enStr = "phy_local" + case ocbinds.OpenconfigInterfaces_LoopbackModeType_ASIC_PHY_REMOTE: + enStr = "phy_remote" + case ocbinds.OpenconfigInterfaces_LoopbackModeType_NONE: + enStr = "none" + default: + return nil, tlerr.InvalidArgs("Loopback mode value not supported") + } + resMap[PORT_LOOPBACK_MODE] = enStr + return resMap, nil +} + +var DbToYang_intf_loopback_mode_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (map[string]interface{}, error) { + ifName := NewPathInfo(inParams.uri).Var("name") + intfType, _, ierr := getIntfTypeByName(ifName) + if intfType == IntfTypeUnset || ierr != nil { + return nil, tlerr.InvalidArgsError{Format: "Invalid interface: " + ifName} + } + intTbl, ok := IntfTypeTblMap[intfType] + if !ok { + return nil, errors.New("DbToYang_intf_loopback_mode_xfmr: interface type not found : " + strconv.Itoa(int(intfType))) + } + tblName, err := getPortTableNameByDBId(intTbl, inParams.curDb) + if err != nil { + return nil, errors.New("DbToYang_intf_loopback_mode_xfmr: table name not found.") + } + prtInst, dbErr := getDBValues(inParams, tblName) + if dbErr != nil { + return nil, dbErr + } + if loopback, ok := prtInst.Field[PORT_LOOPBACK_MODE]; ok { + var mode ocbinds.E_OpenconfigInterfaces_LoopbackModeType + result := make(map[string]interface{}) + switch loopback { + case "none": + mode = ocbinds.OpenconfigInterfaces_LoopbackModeType_NONE + case "mac_local": + mode = ocbinds.OpenconfigInterfaces_LoopbackModeType_ASIC_MAC_LOCAL + case "mac_remote": + mode = ocbinds.OpenconfigInterfaces_LoopbackModeType_ASIC_MAC_REMOTE + case "phy_local": + mode = ocbinds.OpenconfigInterfaces_LoopbackModeType_ASIC_PHY_LOCAL + case "phy_remote": + mode = ocbinds.OpenconfigInterfaces_LoopbackModeType_ASIC_PHY_REMOTE + default: + return nil, errors.New("Invalid loopback_mode value") + } + result[PORT_LOOPBACK_MODE] = ocbinds.E_OpenconfigInterfaces_LoopbackModeType.ΛMap(mode)["E_OpenconfigInterfaces_LoopbackModeType"][int64(mode)].Name + return result, nil + } + return nil, errors.New("loopback_mode field not found in table.") +} + +var DbToYang_intf_eth_mac_address_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (map[string]interface{}, error) { + ifName := keyFromInParamsOrUri(inParams, "name") + intfType, _, _ := getIntfTypeByName(ifName) + if intfType == IntfTypeUnset { + return nil, tlerr.InvalidArgsError{Format: "Invalid interface: " + ifName} + } + intTbl, ok := IntfTypeTblMap[intfType] + if !ok { + return nil, errors.New("DbToYang_intf_eth_mac_address_xfmr: interface type not found.") + } + tblName, err := getPortTableNameByDBId(intTbl, inParams.curDb) + if err != nil { + return nil, errors.New("DbToYang_intf_eth_mac_address_xfmr: table name not found.") + } + prtInst, err := getDBValues(inParams, tblName) + if err != nil { + return nil, tlerr.New(err.Error()) + } + + resMap := make(map[string]interface{}) + if macAddr, ok := prtInst.Field[PORT_MAC_ADDR]; ok { + resMap[PORT_MAC_ADDR] = macAddr + return resMap, nil + } + + /* According to the yang specification, if mac address is not specified + for an interface, the corresponding operational state leaf is expected to + show the system-assigned MAC address. This is returned here if the backend + has not set this default value for the state path. */ + entry, err := inParams.dbs[db.ConfigDB].GetEntry(&db.TableSpec{Name: "DEVICE_METADATA"}, db.Key{Comp: []string{"localhost"}}) + if err != nil { + return nil, fmt.Errorf("Unable to fetch DEVICE_METADATA|localhost entry from ConfigDB. Error: %w", err) + } + if entry.IsPopulated() && entry.Has("mac") { + resMap[PORT_MAC_ADDR] = entry.Field["mac"] + return resMap, nil + } + log.Info("mac-address field not found in DB") + return nil, errors.New("mac-address 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) +}