Skip to content

Commit

Permalink
Add v2r mapping for Fabric Counters in VoQ Chassis (#365)
Browse files Browse the repository at this point in the history
<!--
 Please make sure you've read and understood our contributing guidelines:
 https://github.com/Azure/SONiC/blob/gh-pages/CONTRIBUTING.md

 failure_prs.log skip_prs.log Make sure all your commits include a signature generated with `git commit -s` **

 If this is a bug fix, make sure your description includes "fixes #xxxx", or
 "closes #xxxx" or "resolves #xxxx"

 Please provide the following information:
-->

#### Why I did it
1. In order to retrieve Fabric Counters using Streaming telemetry, the COUNTERS_FABRIC_PORT_NAME_MAP table in COUNTERS_DB has to be queried first to get PORT name to OID mapping, then COUNTERS:oid:<> table in COUNTERS_DB has to be queried to get the Fabric Counters.
To make the retrieval easy, changes are made in this PR to add v2r mapping for Fabric Counters in Packet Chassis similar to Ethernet interface counters. Reference sonic-net/sonic-telemetry#77
2 The Fabric counters Port name is unique within a given asic namespace and is not unique across a linecard. For example, there will be a port named PORT0 in asic0 and PORT0 in asic1 namespace as well. In order to make this unique across a multi-asic linecard, this PR appends asic namespace in the interface name. For example, PORT0 in asic0 will be called "PORT0-asic0".

#### How I did it
1. Add v2r mapping support to read all ports from COUNTERS_FABRIC_PORT_NAME_MAP so that Streaming telemetry can query using Fabric port name and COUNTERS_DB.
2. Modify the Fabric Port name by appending Asic namespace.
3. Add unit-test
4. Modify clean up and set up multi namespace to reset countersFabricPortNameMap so that the port map is re-initialized when changing from single to multi- namespace in unit-test

#### How to verify it
Verified on a multi-asic linecard:
```
gnmi_get -target_addr localhost:50051 -xpath COUNTERS/PORT0-asic0 -xpath_target COUNTERS_DB -insecure
== getRequest:
prefix: <
 target: "COUNTERS_DB"
>
path: <
 elem: <
 name: "COUNTERS"
 >
 elem: <
 name: "PORT0-asic0"
 >
>
encoding: JSON_IETF

== getResponse:
notification: <
 timestamp: 1737484675266818454
 prefix: <
 target: "COUNTERS_DB"
 >
 update: <
 path: <
 elem: <
 name: "COUNTERS"
 >
 elem: <
 name: "PORT0-asic0"
 >
 >
 val: <
 json_ietf_val: "{\"SAI_PORT_STAT_IF_IN_ERRORS\":\"0\",\"SAI_PORT_STAT_IF_IN_FABRIC_DATA_UNITS\":\"0\",\"SAI_PORT_STAT_IF_IN_FEC_CORRECTABLE_FRAMES\":\"0\",\"SAI_PORT_STAT_IF_IN_FEC_NOT_CORRECTABLE_FRAMES\":\"0\",\"SAI_PORT_STAT_IF_IN_FEC_SYMBOL_ERRORS\":\"0\",\"SAI_PORT_STAT_IF_IN_OCTETS\":\"0\",\"SAI_PORT_STAT_IF_OUT_FABRIC_DATA_UNITS\":\"0\",\"SAI_PORT_STAT_IF_OUT_OCTETS\":\"0\"}"
 >
 >
>
```

Verified on single asic Linecard:
```
gnmi_get -target_addr localhost:50051 -xpath COUNTERS/PORT0 -xpath_target COUNTERS_DB -insecure
== getRequest:
prefix: <
 target: "COUNTERS_DB"
>
path: <
 elem: <
 name: "COUNTERS"
 >
 elem: <
 name: "PORT0"
 >
>
encoding: JSON_IETF

== getResponse:
notification: <
 timestamp: 1737567049704926166
 prefix: <
 target: "COUNTERS_DB"
 >
 update: <
 path: <
 elem: <
 name: "COUNTERS"
 >
 elem: <
 name: "PORT0"
 >
 >
 val: <
 json_ietf_val: "{\"SAI_PORT_STAT_IF_IN_ERRORS\":\"0\",\"SAI_PORT_STAT_IF_IN_FABRIC_DATA_UNITS\":\"181562945\",\"SAI_PORT_STAT_IF_IN_FEC_CORRECTABLE_FRAMES\":\"0\",\"SAI_PORT_STAT_IF_IN_FEC_NOT_CORRECTABLE_FRAMES\":\"0\",\"SAI_PORT_STAT_IF_IN_FEC_SYMBOL_ERRORS\":\"0\",\"SAI_PORT_STAT_IF_IN_OCTETS\":\"40499614075\",\"SAI_PORT_STAT_IF_OUT_FABRIC_DATA_UNITS\":\"11152\",\"SAI_PORT_STAT_IF_OUT_OCTETS\":\"2588370\"}"
 >
 >
>

gnmi_get -target_addr localhost:50051 -xpath COUNTERS/PORT* -xpath_target COUNTERS_DB -insecure
```
Verified on Pizza box to ensure no error log is seen.
#### Which release branch to backport (provide reason below if selected)

<!--
- Note we only backport fixes to a release branch, *not* features!
- Please also provide a reason for the backporting below.
- e.g.
- [x] 202006
-->

- [ ] 201811
- [ ] 201911
- [ ] 202006
- [ ] 202012
- [ ] 202106
- [ ] 202111

#### Description for the changelog
<!--
Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog:
-->

#### Link to config_db schema for YANG module changes
<!--
Provide a link to config_db schema for the table for which YANG model
is defined
Link should point to correct section on https://github.com/Azure/SONiC/wiki/Configuration.
-->

#### A picture of a cute animal (not mandatory but encouraged)
  • Loading branch information
mssonicbld authored Feb 19, 2025
1 parent 2c4b9c8 commit bcfc802
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 0 deletions.
99 changes: 99 additions & 0 deletions gnmi_server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,32 @@ func initFullCountersDb(t *testing.T, namespace string) {
}
mpi_counter = loadConfig(t, "COUNTERS:oid:0x1500000000091f", countersEeth68_4Byte)
loadDB(t, rclient, mpi_counter)

fileName = "../testdata/COUNTERS_FABRIC_PORT_NAME_MAP.txt"
countersFabricPortNameMapByte, err := ioutil.ReadFile(fileName)
if err != nil {
t.Fatalf("read file %v err: %v", fileName, err)
}
mpi_fab_name_map := loadConfig(t, "COUNTERS_FABRIC_PORT_NAME_MAP", countersFabricPortNameMapByte)
loadDB(t, rclient, mpi_fab_name_map)

// "PORT0": "oid:0x1000000000081" : Fabric port counter, for COUNTERS/PORT0 vpath test
fileName = "../testdata/COUNTERS:oid:0x1000000000081.txt"
countersPort0_Byte, err := ioutil.ReadFile(fileName)
if err != nil {
t.Fatalf("read file %v err: %v", fileName, err)
}
mpi_fab_counter_0 := loadConfig(t, "COUNTERS:oid:0x1000000000081", countersPort0_Byte)
loadDB(t, rclient, mpi_fab_counter_0)

// "PORT1": "oid:0x1000000000082" : Fabric port counter, for COUNTERS/PORT1 vpath test
fileName = "../testdata/COUNTERS:oid:0x1000000000082.txt"
countersPort1_Byte, err := ioutil.ReadFile(fileName)
if err != nil {
t.Fatalf("read file %v err: %v", fileName, err)
}
mpi_fab_counter_1 := loadConfig(t, "COUNTERS:oid:0x1000000000082", countersPort1_Byte)
loadDB(t, rclient, mpi_fab_counter_1)
}

func prepareConfigDb(t *testing.T, namespace string) {
Expand Down Expand Up @@ -640,6 +666,14 @@ func prepareDb(t *testing.T, namespace string) {
mpi_qname_map := loadConfig(t, "COUNTERS_QUEUE_NAME_MAP", countersQueueNameMapByte)
loadDB(t, rclient, mpi_qname_map)

fileName = "../testdata/COUNTERS_FABRIC_PORT_NAME_MAP.txt"
countersFabricPortNameMapByte, err := ioutil.ReadFile(fileName)
if err != nil {
t.Fatalf("read file %v err: %v", fileName, err)
}
mpi_fab_name_map := loadConfig(t, "COUNTERS_FABRIC_PORT_NAME_MAP", countersFabricPortNameMapByte)
loadDB(t, rclient, mpi_fab_name_map)

fileName = "../testdata/COUNTERS:Ethernet68.txt"
countersEthernet68Byte, err := ioutil.ReadFile(fileName)
if err != nil {
Expand Down Expand Up @@ -694,6 +728,25 @@ func prepareDb(t *testing.T, namespace string) {
mpi_counter = loadConfig(t, "COUNTERS:oid:0x1500000000091f", countersEeth68_4Byte)
loadDB(t, rclient, mpi_counter)

// "PORT0": "oid:0x1000000000081" : Fabric port counters, for COUNTERS/PORT0 vpath test
fileName = "../testdata/COUNTERS:oid:0x1000000000081.txt"
fileName = "../testdata/COUNTERS:oid:0x1000000000081.txt"
countersPort0, err := ioutil.ReadFile(fileName)
if err != nil {
t.Fatalf("read file %v err: %v", fileName, err)
}
mpi_counter = loadConfig(t, "COUNTERS:oid:0x1000000000081", countersPort0)
loadDB(t, rclient, mpi_counter)

// "PORT1": "oid:0x1000000000082" : Fabric port counter, for COUNTERS/PORT1 vpath test
fileName = "../testdata/COUNTERS:oid:0x1000000000082.txt"
countersPort1_Byte, err := ioutil.ReadFile(fileName)
if err != nil {
t.Fatalf("read file %v err: %v", fileName, err)
}
mpi_counter = loadConfig(t, "COUNTERS:oid:0x1000000000082", countersPort1_Byte)
loadDB(t, rclient, mpi_counter)

// Load CONFIG_DB for alias translation
prepareConfigDb(t, namespace)

Expand Down Expand Up @@ -1232,11 +1285,27 @@ func runGnmiTestGet(t *testing.T, namespace string) {
t.Fatalf("read file %v err: %v", fileName, err)
}

fileName = "../testdata/COUNTERS:PORT0.txt"
countersFabricPort0Byte, err := ioutil.ReadFile(fileName)
if err != nil {
t.Fatalf("read file %v err: %v", fileName, err)
}

fileName = "../testdata/COUNTERS:PORT_wildcard" + namespace + ".txt"
countersFabricPortWildcardByte, err := ioutil.ReadFile(fileName)
if err != nil {
t.Fatalf("read file %v err: %v", fileName, err)
}

stateDBPath := "STATE_DB"

ns, _ := sdcfg.GetDbDefaultNamespace()
validFabricPortName := "PORT0"
invalidFabricPortName := "PORT0-" + namespace
if namespace != ns {
stateDBPath = "STATE_DB" + "/" + namespace
validFabricPortName = "PORT0-" + namespace
invalidFabricPortName = "PORT0"
}

type testCase struct {
Expand Down Expand Up @@ -1417,6 +1486,35 @@ func runGnmiTestGet(t *testing.T, namespace string) {
valTest: true,
wantRetCode: codes.OK,
wantRespVal: []byte(`{"test_field": "test_value"}`),
}, {
desc: "get COUNTERS:" + validFabricPortName,
pathTarget: "COUNTERS_DB",
textPbPath: `
elem: <name: "COUNTERS" >
elem: <name: "` + validFabricPortName + `">
`,
wantRetCode: codes.OK,
wantRespVal: countersFabricPort0Byte,
valTest: true,
}, {
desc: "get COUNTERS:PORT*",
pathTarget: "COUNTERS_DB",
textPbPath: `
elem: <name: "COUNTERS" >
elem: <name: "PORT*" >
`,
wantRetCode: codes.OK,
wantRespVal: countersFabricPortWildcardByte,
valTest: true,
}, {
desc: "Invalid fabric port key get" + invalidFabricPortName,
pathTarget: "COUNTERS_DB",
textPbPath: `
elem: <name: "COUNTERS" >
elem: <name: "` + invalidFabricPortName + `">
`,
wantRetCode: codes.NotFound,
valTest: true,
}, {
desc: "Invalid DBKey of length 1",
pathTarget: stateDBPath,
Expand Down Expand Up @@ -4650,6 +4748,7 @@ func init() {

// Inform gNMI server to use redis tcp localhost connection
sdc.UseRedisLocalTcpPort = true
os.Setenv("UNIT_TEST", "1")
}

func TestMain(m *testing.M) {
Expand Down
4 changes: 4 additions & 0 deletions sonic_data_client/db_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,10 @@ func populateDbtablePath(prefix, path *gnmipb.Path, pathG2S *map[*gnmipb.Path][]
if err != nil {
return err
}
err = initCountersFabricPortNameMap()
if err != nil {
log.Errorf("Could not create CountersFabricPortNameMap: %v", err)
}
}


Expand Down
105 changes: 105 additions & 0 deletions sonic_data_client/virtual_db.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
log "github.com/golang/glog"
"strings"
"os"
)

// virtual db is to Handle
Expand Down Expand Up @@ -44,6 +45,9 @@ var (
// SONiC interface name to their PFC-WD enabled queues, then to oid map
countersPfcwdNameMap = make(map[string]map[string]string)

// SONiC interface name to their Fabric port name map, then to oid map
countersFabricPortNameMap = make(map[string]string)

// path2TFuncTbl is used to populate trie tree which is reponsible
// for virtual path to real data path translation
pathTransFuncTbl = []pathTransFunc{
Expand All @@ -59,6 +63,9 @@ var (
}, { // PFC WD stats for one or all Ethernet ports
path: []string{"COUNTERS_DB", "COUNTERS", "Ethernet*", "Pfcwd"},
transFunc: v2rTranslate(v2rEthPortPfcwdStats),
}, { // stats for one or all Fabric ports
path: []string{"COUNTERS_DB", "COUNTERS", "PORT*"},
transFunc: v2rTranslate(v2rFabricPortStats),
},
}
)
Expand Down Expand Up @@ -107,6 +114,7 @@ func initAliasMap() error {
}
return nil
}

func initCountersPfcwdNameMap() error {
var err error
if len(countersPfcwdNameMap) == 0 {
Expand All @@ -118,6 +126,20 @@ func initCountersPfcwdNameMap() error {
return nil
}

func initCountersFabricPortNameMap() error {
var err error
// Reset map for Unit test to ensure that counters db is updated
// after changing from single to multi-asic config
value := os.Getenv("UNIT_TEST")
if len(countersFabricPortNameMap) == 0 || value == "1" {
countersFabricPortNameMap, err = getFabricCountersMap("COUNTERS_FABRIC_PORT_NAME_MAP")
if err != nil {
return err
}
}
return nil
}

// Get the mapping between sonic interface name and oids of their PFC-WD enabled queues in COUNTERS_DB
func getPfcwdMap() (map[string]map[string]string, error) {
var pfcwdName_map = make(map[string]map[string]string)
Expand Down Expand Up @@ -284,6 +306,89 @@ func getCountersMap(tableName string) (map[string]string, error) {
return counter_map, nil
}


// Get the mapping between objects in counters DB, Ex. port name to oid in "COUNTERS_FABRIC_PORT_NAME_MAP" table.
// Aussuming static port name to oid map in COUNTERS table
func getFabricCountersMap(tableName string) (map[string]string, error) {
counter_map := make(map[string]string)
dbName := "COUNTERS_DB"
redis_client_map, err := GetRedisClientsForDb(dbName)
if err != nil {
return nil, err
}
for namespace, redisDb := range redis_client_map {
fv, err := redisDb.HGetAll(tableName).Result()
if err != nil {
log.V(2).Infof("redis HGetAll failed for COUNTERS_DB in namespace %v, tableName: %s", namespace, tableName)
return nil, err
}
namespaceFv := make(map[string]string)
for k, v := range fv {
// Fabric port names are not unique across asic namespace
// To make them unique, add asic namesapce to the port name
// For example, PORT0 in asic0 will be PORT0-asic0
var namespace_str = ""
if len(namespace) != 0 {
namespace_str = string('-') + namespace
}
namespaceFv[k + namespace_str] = v
}
addmap(counter_map, namespaceFv)
log.V(6).Infof("tableName: %s in namespace %v, map %v", tableName, namespace, namespaceFv)
}
return counter_map, nil
}

// Populate real data paths from paths like
// [COUNTER_DB COUNTERS PORT*] or [COUNTER_DB COUNTERS PORT0]
func v2rFabricPortStats(paths []string) ([]tablePath, error) {
var tblPaths []tablePath
if strings.HasSuffix(paths[KeyIdx], "*") { // All Ethernet ports
for port, oid := range countersFabricPortNameMap {
var namespace string
// Extract namespace from port name
// multi-asic Linecard ex: PORT0-asic0
if strings.Contains(port, "-"){
namespace = strings.Split(port, "-")[1]
} else {
namespace = ""
}
separator, _ := GetTableKeySeparator(paths[DbIdx], namespace)
tblPath := tablePath{
dbNamespace: namespace,
dbName: paths[DbIdx],
tableName: paths[TblIdx],
tableKey: oid,
delimitor: separator,
jsonTableKey: port,
}
tblPaths = append(tblPaths, tblPath)
}
} else { //single port
var port, namespace string
port = paths[KeyIdx]
oid, ok := countersFabricPortNameMap[port]
if !ok {
return nil, fmt.Errorf("%v not a valid sonic fabric interface.", port)
}
if strings.Contains(port, "-"){
namespace = strings.Split(port, "-")[1]
} else {
namespace = ""
}
separator, _ := GetTableKeySeparator(paths[DbIdx], namespace)
tblPaths = []tablePath{{
dbNamespace: namespace,
dbName: paths[DbIdx],
tableName: paths[TblIdx],
tableKey: oid,
delimitor: separator,
}}
}
log.V(6).Infof("v2rFabricPortStats: %v", tblPaths)
return tblPaths, nil
}

// Populate real data paths from paths like
// [COUNTER_DB COUNTERS Ethernet*] or [COUNTER_DB COUNTERS Ethernet68]
func v2rEthPortStats(paths []string) ([]tablePath, error) {
Expand Down
10 changes: 10 additions & 0 deletions testdata/COUNTERS:PORT0.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"SAI_PORT_STAT_IF_OUT_FABRIC_DATA_UNITS": "6428",
"SAI_PORT_STAT_IF_IN_FEC_SYMBOL_ERRORS": "0",
"SAI_PORT_STAT_IF_IN_FEC_NOT_CORRECTABLE_FRAMES": "0",
"SAI_PORT_STAT_IF_IN_FEC_CORRECTABLE_FRAMES": "0",
"SAI_PORT_STAT_IF_OUT_OCTETS": "1007545",
"SAI_PORT_STAT_IF_IN_FABRIC_DATA_UNITS": "16807108",
"SAI_PORT_STAT_IF_IN_ERRORS": "0",
"SAI_PORT_STAT_IF_IN_OCTETS": "3747867283"
}
22 changes: 22 additions & 0 deletions testdata/COUNTERS:PORT_wildcard.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"PORT0": {
"SAI_PORT_STAT_IF_OUT_FABRIC_DATA_UNITS": "6428",
"SAI_PORT_STAT_IF_IN_FEC_SYMBOL_ERRORS": "0",
"SAI_PORT_STAT_IF_IN_FEC_NOT_CORRECTABLE_FRAMES": "0",
"SAI_PORT_STAT_IF_IN_FEC_CORRECTABLE_FRAMES": "0",
"SAI_PORT_STAT_IF_OUT_OCTETS": "1007545",
"SAI_PORT_STAT_IF_IN_FABRIC_DATA_UNITS": "16807108",
"SAI_PORT_STAT_IF_IN_ERRORS": "0",
"SAI_PORT_STAT_IF_IN_OCTETS": "3747867283"
},
"PORT1": {
"SAI_PORT_STAT_IF_OUT_FABRIC_DATA_UNITS": "0",
"SAI_PORT_STAT_IF_IN_FEC_SYMBOL_ERRORS": "0",
"SAI_PORT_STAT_IF_IN_FEC_NOT_CORRECTABLE_FRAMES": "0",
"SAI_PORT_STAT_IF_IN_FEC_CORRECTABLE_FRAMES": "0",
"SAI_PORT_STAT_IF_OUT_OCTETS": "0",
"SAI_PORT_STAT_IF_IN_FABRIC_DATA_UNITS": "0",
"SAI_PORT_STAT_IF_IN_ERRORS": "0",
"SAI_PORT_STAT_IF_IN_OCTETS": "0"
}
}
22 changes: 22 additions & 0 deletions testdata/COUNTERS:PORT_wildcardasic0.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"PORT0-asic0": {
"SAI_PORT_STAT_IF_OUT_FABRIC_DATA_UNITS": "6428",
"SAI_PORT_STAT_IF_IN_FEC_SYMBOL_ERRORS": "0",
"SAI_PORT_STAT_IF_IN_FEC_NOT_CORRECTABLE_FRAMES": "0",
"SAI_PORT_STAT_IF_IN_FEC_CORRECTABLE_FRAMES": "0",
"SAI_PORT_STAT_IF_OUT_OCTETS": "1007545",
"SAI_PORT_STAT_IF_IN_FABRIC_DATA_UNITS": "16807108",
"SAI_PORT_STAT_IF_IN_ERRORS": "0",
"SAI_PORT_STAT_IF_IN_OCTETS": "3747867283"
},
"PORT1-asic0": {
"SAI_PORT_STAT_IF_OUT_FABRIC_DATA_UNITS": "0",
"SAI_PORT_STAT_IF_IN_FEC_SYMBOL_ERRORS": "0",
"SAI_PORT_STAT_IF_IN_FEC_NOT_CORRECTABLE_FRAMES": "0",
"SAI_PORT_STAT_IF_IN_FEC_CORRECTABLE_FRAMES": "0",
"SAI_PORT_STAT_IF_OUT_OCTETS": "0",
"SAI_PORT_STAT_IF_IN_FABRIC_DATA_UNITS": "0",
"SAI_PORT_STAT_IF_IN_ERRORS": "0",
"SAI_PORT_STAT_IF_IN_OCTETS": "0"
}
}
10 changes: 10 additions & 0 deletions testdata/COUNTERS:oid:0x1000000000081.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"SAI_PORT_STAT_IF_OUT_FABRIC_DATA_UNITS": "6428",
"SAI_PORT_STAT_IF_IN_FEC_SYMBOL_ERRORS": "0",
"SAI_PORT_STAT_IF_IN_FEC_NOT_CORRECTABLE_FRAMES": "0",
"SAI_PORT_STAT_IF_IN_FEC_CORRECTABLE_FRAMES": "0",
"SAI_PORT_STAT_IF_OUT_OCTETS": "1007545",
"SAI_PORT_STAT_IF_IN_FABRIC_DATA_UNITS": "16807108",
"SAI_PORT_STAT_IF_IN_ERRORS": "0",
"SAI_PORT_STAT_IF_IN_OCTETS": "3747867283"
}
11 changes: 11 additions & 0 deletions testdata/COUNTERS:oid:0x1000000000082.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"SAI_PORT_STAT_IF_OUT_FABRIC_DATA_UNITS": "0",
"SAI_PORT_STAT_IF_IN_FEC_SYMBOL_ERRORS": "0",
"SAI_PORT_STAT_IF_IN_FEC_NOT_CORRECTABLE_FRAMES": "0",
"SAI_PORT_STAT_IF_IN_FEC_CORRECTABLE_FRAMES": "0",
"SAI_PORT_STAT_IF_OUT_OCTETS": "0",
"SAI_PORT_STAT_IF_IN_FABRIC_DATA_UNITS": "0",
"SAI_PORT_STAT_IF_IN_ERRORS": "0",
"SAI_PORT_STAT_IF_IN_OCTETS": "0"
}

4 changes: 4 additions & 0 deletions testdata/COUNTERS_FABRIC_PORT_NAME_MAP.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"PORT0": "oid:0x1000000000081",
"PORT1": "oid:0x1000000000082"
}

0 comments on commit bcfc802

Please sign in to comment.