cmd/ziffy/node/print.go (261 lines of code) (raw):

/* Copyright (c) Facebook, Inc. and its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package node import ( "encoding/csv" "fmt" "math" "os" "sort" "strconv" "strings" ptp "github.com/facebook/time/ptp/protocol" "github.com/olekukonko/tablewriter" log "github.com/sirupsen/logrus" ) type status int const ( tcTrue = iota tcFalse tcNA ) var statusToString = map[status]string{ tcTrue: "On", tcFalse: "Off", tcNA: "Unknown", } const maxColWidth = 100 type keyPair struct { host string hop int } func min(x int, y int) int { if x < y { return x } return y } // getHostNoPrefix has ip as input and returns device hostname without interface prefix func getHostNoPrefix(ip string) string { lun := getLookUpName(ip) if strings.Contains(lun, ".") && lun[:3] == "eth" { return lun[strings.Index(lun, ".")+1:] } return lun } // getHostIfPrefix has ip as input and returns device interface without suffix func getHostIfPrefix(ip string) string { lun := getLookUpName(ip) if strings.Contains(lun, ".") && lun[:3] == "eth" { return lun[:strings.Index(lun, ".")] } return "" } // PrettyPrint formats and prints the output to stdout func PrettyPrint(routes []PathInfo, cfThreshold ptp.Correction) { debugPrint(routes) info := computeInfo(routes, cfThreshold) detailedPrint(info) } func debugPrint(routes []PathInfo) { var notTCEnabled []SwitchTrafficInfo tested := make(map[string]bool) enabled := 0 for _, route := range routes { for swIndex, swh := range route.switches { if swIndex == len(route.switches)-1 { continue } corrField := route.switches[swIndex+1].corrField - route.switches[swIndex].corrField if corrField == ptp.Correction(0) && !tested[swh.ip] { notTCEnabled = append(notTCEnabled, swh) } tested[swh.ip] = true enabled++ } } log.Debugf("%v switches tested: %v TC enabled | %v TC not enabled", len(tested), enabled, len(notTCEnabled)) for _, brkSw := range notTCEnabled { log.Debugf("%v: PTP TC not enabled", getLookUpName(brkSw.ip)) for index, swh := range routes[brkSw.routeIdx].switches { if index != len(routes[brkSw.routeIdx].switches)-1 { log.Debugf(" | %v", getLookUpName(swh.ip)) } else { log.Debugf(" V %v", getLookUpName(swh.ip)) } } } log.Debugf("TESTED:") var aux []string for key := range tested { aux = append(aux, getLookUpName(key)) } sort.Slice(aux, func(i, j int) bool { return aux[i] > aux[j] }) for index, element := range aux { log.Debugf("%v %v", index, element) } } func computeInfo(routes []PathInfo, cfThreshold ptp.Correction) map[keyPair]*SwitchPrintInfo { discovered := make(map[keyPair]*SwitchPrintInfo) for _, route := range routes { for swIndex, swh := range route.switches { corrField := ptp.Correction(0) last := false host := getHostNoPrefix(swh.ip) intf := getHostIfPrefix(swh.ip) if swh.ip == route.switches[len(route.switches)-1].ip { last = true } else { // If switches are not adjacent (hop count difference > 1) do not subtract CF // This may happen in two cases: switch does not send ICMP Hop Limit Exceeded // back to source or the packet may be lost in transit if route.switches[swIndex+1].hop != route.switches[swIndex].hop+1 { last = true } else { corrField = route.switches[swIndex+1].corrField - route.switches[swIndex].corrField } } if swh.hop == 1 && route.rackSwHostname != "" { host = route.rackSwHostname } pair := keyPair{ host: host, hop: int(swh.hop), } if discovered[pair] == nil { discovered[pair] = &SwitchPrintInfo{ ip: swh.ip, hostname: host, interf: intf, totalCF: corrField, routes: 1, hop: int(swh.hop), last: last, maxCF: corrField, minCF: corrField, divRoutes: 1, } } else { discovered[pair].routes++ if !last { discovered[pair].totalCF += corrField discovered[pair].divRoutes++ if discovered[pair].maxCF < corrField { discovered[pair].maxCF = corrField } if discovered[pair].minCF > corrField { discovered[pair].minCF = corrField } } } } } for _, sw := range discovered { if sw.last { sw.avgCF = ptp.Correction(0) sw.tcEnable = tcNA continue } sw.avgCF = sw.totalCF / ptp.Correction(sw.divRoutes) avgCFNs := sw.avgCF.Nanoseconds() cfThresholdNs := cfThreshold.Nanoseconds() if math.Abs(avgCFNs) > cfThresholdNs { sw.tcEnable = tcTrue } else { sw.tcEnable = tcFalse } } return discovered } func parseSwitchMap(info map[keyPair]*SwitchPrintInfo) []SwitchPrintInfo { var aux []SwitchPrintInfo for _, val := range info { aux = append(aux, *val) } sort.Slice(aux, func(i, j int) bool { return aux[i].hop < aux[j].hop }) return aux } func hopCount(sw []SwitchPrintInfo, hop int) int { width := 0 for _, val := range sw { if val.hop == hop { width++ } } return width } func colNumber(header []string, colName string) (int, error) { for ind, val := range header { if val == colName { return ind, nil } } return -1, fmt.Errorf("no column with this name") } func computePrintData(sw []SwitchPrintInfo) [][]string { var ret [][]string ret = append(ret, []string{"uniq", "width", "hop", "ip_address", "intf", "hostname", "flows", "TC", "avg_CF(ns)", "max_CF(ns)", "min_CF(ns)"}) // unique counts number of devices discovered unique := 1 already := make(map[string]bool) for _, val := range sw { avgDisplay := strconv.FormatFloat(val.avgCF.Nanoseconds(), 'f', 4, 64) minDisplay := strconv.FormatFloat(val.minCF.Nanoseconds(), 'f', 4, 64) maxDisplay := strconv.FormatFloat(val.maxCF.Nanoseconds(), 'f', 4, 64) uniqDisplay := "" if val.last { avgDisplay = "" minDisplay = "" maxDisplay = "" } if !already[val.hostname] { uniqDisplay = strconv.Itoa(unique) already[val.hostname] = true unique++ } ret = append(ret, []string{uniqDisplay, strconv.Itoa(hopCount(sw, val.hop)), strconv.Itoa(val.hop), val.ip, val.interf, val.hostname, strconv.Itoa(val.routes), statusToString[val.tcEnable], avgDisplay, maxDisplay, minDisplay}) } return ret } func detailedPrint(info map[keyPair]*SwitchPrintInfo) { aux := parseSwitchMap(info) data := computePrintData(aux) // currentHop is incremented each time we pass to the next hop. Used to print newline between hops currentHop := 0 // headerRow is the row index for the header headerRow := 0 // blank space for data blank := []string{"", "", "", "", "", "", "", "", "", "", ""} table := tablewriter.NewWriter(os.Stdout) table.SetColWidth(maxColWidth) table.SetHeader(data[headerRow]) for _, val := range data[headerRow+1:] { hopInd, err := colNumber(data[headerRow], "hop") if err != nil { log.Errorf("detailedPrint failed: %v", err) return } nextHop, err := strconv.Atoi(val[hopInd]) if err != nil { log.Errorf("strconv atoi failed: %v", err) return } if nextHop > currentHop { table.Append(blank) } table.Append(val) currentHop = nextHop } table.Render() } // CsvPrint outputs the data in a csv file func CsvPrint(routes []PathInfo, path string, cfThreshold ptp.Correction) { info := computeInfo(routes, cfThreshold) aux := parseSwitchMap(info) data := computePrintData(aux) file, err := os.Create(path) if err != nil { log.Errorf("unable to open file: %v", err) return } defer file.Close() writer := csv.NewWriter(file) defer writer.Flush() for _, val := range data { if err := writer.Write(val); err != nil { log.Debugf("unable to write to file: %v", err) } } }