From 9a273028eb0f04dffa7b453aad31fe29cfe3dcee Mon Sep 17 00:00:00 2001 From: Partha Dutta <51353699+dutta-partha@users.noreply.github.com> Date: Mon, 5 Oct 2020 20:12:35 +0530 Subject: [PATCH] CVL Changes #4: Implementation of new CVL APIs (#22) Implementing following CVL APIs : SortDepTables GetOrderedTables GetDepTables GetDepDataForDelete GetValidationTimeStats ClearValidationTimeStats --- cvl/cvl.go | 82 +++++++- cvl/cvl_api.go | 504 ++++++++++++++++++++++++++++++++++++++++++++++++- go.mod | 2 +- go.sum | 2 + 4 files changed, 574 insertions(+), 16 deletions(-) diff --git a/cvl/cvl.go b/cvl/cvl.go index 05c331cd0b22..012d9f5ea072 100644 --- a/cvl/cvl.go +++ b/cvl/cvl.go @@ -62,6 +62,12 @@ var dbNameToDbNum map[string]uint8 //map of lua script loaded var luaScripts map[string]*redis.Script +type leafRefInfo struct { + path string //leafref path + yangListNames []string //all yang list in path + targetNodeName string //target node name +} + //var tmpDbCache map[string]interface{} //map of table storing map of key-value pair //m["PORT_TABLE] = {"key" : {"f1": "v1"}} //Important schema information to be loaded at bootup time @@ -74,10 +80,11 @@ type modelTableInfo struct { redisKeyDelim string redisKeyPattern string mapLeaf []string //for 'mapping list' - leafRef map[string][]string //for storing all leafrefs for a leaf in a table, + leafRef map[string][]*leafRefInfo //for storing all leafrefs for a leaf in a table, //multiple leafref possible for union mustExp map[string]string tablesForMustExp map[string]CVLOperation + refFromTables []tblFieldPair //list of table or table/field referring to this table dfltLeafVal map[string]string //map of leaf names and default value } @@ -359,7 +366,7 @@ func storeModelInfo(modelFile string, module *yparser.YParserModule) { //such mo continue } - tableInfo.leafRef = make(map[string][]string) + tableInfo.leafRef = make(map[string][]*leafRefInfo) for _, leafRefNode := range leafRefNodes { if (leafRefNode.Parent == nil || leafRefNode.FirstChild == nil) { continue @@ -378,7 +385,7 @@ func storeModelInfo(modelFile string, module *yparser.YParserModule) { //such mo //Store the leafref path if (leafName != "") { tableInfo.leafRef[leafName] = append(tableInfo.leafRef[leafName], - getXmlNodeAttr(leafRefNode.FirstChild, "value")) + &leafRefInfo{path: getXmlNodeAttr(leafRefNode.FirstChild, "value")}) } } @@ -429,6 +436,69 @@ func getYangListToRedisTbl(yangListName string) string { return yangListName } +//This functions build info of dependent table/fields +//which uses a particular table through leafref +func buildRefTableInfo() { + + CVL_LOG(INFO_API, "Building reverse reference info from leafref") + + for tblName, tblInfo := range modelInfo.tableInfo { + if (len(tblInfo.leafRef) == 0) { + continue + } + + //For each leafref update the table used through leafref + for fieldName, leafRefs := range tblInfo.leafRef { + for _, leafRef := range leafRefs { + + for _, yangListName := range leafRef.yangListNames { + refTblInfo := modelInfo.tableInfo[yangListName] + + refFromTables := &refTblInfo.refFromTables + *refFromTables = append(*refFromTables, tblFieldPair{tblName, fieldName}) + modelInfo.tableInfo[yangListName] = refTblInfo + } + + } + } + + } + + //Now sort list 'refFromTables' under each table based on dependency among them + for tblName, tblInfo := range modelInfo.tableInfo { + if (len(tblInfo.refFromTables) == 0) { + continue + } + + depTableList := []string{} + for i:=0; i < len(tblInfo.refFromTables); i++ { + depTableList = append(depTableList, tblInfo.refFromTables[i].tableName) + } + + sortedTableList, _ := cvg.cv.SortDepTables(depTableList) + if (len(sortedTableList) == 0) { + continue + } + + newRefFromTables := []tblFieldPair{} + + for i:=0; i < len(sortedTableList); i++ { + //Find fieldName + fieldName := "" + for j :=0; j < len(tblInfo.refFromTables); j++ { + if (sortedTableList[i] == tblInfo.refFromTables[j].tableName) { + fieldName = tblInfo.refFromTables[j].field + newRefFromTables = append(newRefFromTables, tblFieldPair{sortedTableList[i], fieldName}) + } + } + } + //Update sorted refFromTables + tblInfo.refFromTables = newRefFromTables + modelInfo.tableInfo[tblName] = tblInfo + } + +} + //Find the tables names in must expression, these tables data need to be fetched //during semantic validation func addTableNamesForMustExp() { @@ -998,8 +1068,8 @@ func (c *CVL) findUsedAsLeafRef(tableName, field string) []tblFieldPair { found := false //Find leafref by searching table and field name for _, leafRef := range leafRefs { - if ((strings.Contains(leafRef, tableName) == true) && - (strings.Contains(leafRef, field) == true)) { + if ((strings.Contains(leafRef.path, tableName) == true) && + (strings.Contains(leafRef.path, field) == true)) { tblFieldPairArr = append(tblFieldPairArr, tblFieldPair{tblName, fieldName}) //Found as leafref, no need to search further @@ -1030,7 +1100,7 @@ func (c *CVL) addLeafRef(config bool, tableName string, name string, value strin for _, leafRef := range modelInfo.tableInfo[tableName].leafRef[name] { //Get reference table name from the path and the leaf name - matches := reLeafRef.FindStringSubmatch(leafRef) + matches := reLeafRef.FindStringSubmatch(leafRef.path) //We have the leafref table name and the leaf name as well if (matches != nil && len(matches) == 5) { //whole + 4 sub matches diff --git a/cvl/cvl_api.go b/cvl/cvl_api.go index 5c352373b188..64af1bc76852 100644 --- a/cvl/cvl_api.go +++ b/cvl/cvl_api.go @@ -22,10 +22,14 @@ package cvl import ( "fmt" "encoding/json" + "github.com/go-redis/redis" + toposort "github.com/philopon/go-toposort" "path/filepath" "github.com/Azure/sonic-mgmt-common/cvl/internal/yparser" . "github.com/Azure/sonic-mgmt-common/cvl/internal/util" + "strings" "time" + "sync" ) type CVLValidateType uint @@ -127,6 +131,7 @@ type CVLDepDataForDelete struct { //Global data structure for maintaining validation stats var cfgValidationStats ValidationTimeStats +var statsMutex *sync.Mutex func Initialize() CVLRetCode { if (cvlInitialized == true) { @@ -508,48 +513,482 @@ func (c *CVL) ValidateEditConfig(cfgData []CVLEditConfigData) (CVLErrorInfo, CVL return cvlErrObj, CVL_SUCCESS } -/* Fetch the Error Message from CVL Return Code. */ +//GetErrorString Fetch the Error Message from CVL Return Code. func GetErrorString(retCode CVLRetCode) string{ - return cvlErrorMap[retCode] + return cvlErrorMap[retCode] } -//Validate key only +//ValidateKeys Validate key only func (c *CVL) ValidateKeys(key []string) CVLRetCode { return CVL_NOT_IMPLEMENTED } -//Validate key and data +//ValidateKeyData Validate key and data func (c *CVL) ValidateKeyData(key string, data string) CVLRetCode { return CVL_NOT_IMPLEMENTED } -//Validate key, field and value +//ValidateFields Validate key, field and value func (c *CVL) ValidateFields(key string, field string, value string) CVLRetCode { return CVL_NOT_IMPLEMENTED } +func (c *CVL) addDepEdges(graph *toposort.Graph, tableList []string) { + //Add all the depedency edges for graph nodes + for ti :=0; ti < len(tableList); ti++ { + + redisTblTo := getYangListToRedisTbl(tableList[ti]) + + for tj :=0; tj < len(tableList); tj++ { + + if (tableList[ti] == tableList[tj]) { + //same table, continue + continue + } + + redisTblFrom := getYangListToRedisTbl(tableList[tj]) + + //map for checking duplicate edge + dupEdgeCheck := map[string]string{} + + for _, leafRefs := range modelInfo.tableInfo[tableList[tj]].leafRef { + for _, leafRef := range leafRefs { + if !(strings.Contains(leafRef.path, tableList[ti] + "_LIST")) { + continue + } + + toName, exists := dupEdgeCheck[redisTblFrom] + if exists && (toName == redisTblTo) { + //Don't add duplicate edge + continue + } + + //Add and store the edge in map + graph.AddEdge(redisTblFrom, redisTblTo) + dupEdgeCheck[redisTblFrom] = redisTblTo + + CVL_LOG(INFO_DEBUG, + "addDepEdges(): Adding edge %s -> %s", redisTblFrom, redisTblTo) + } + } + } + } +} + //SortDepTables Sort list of given tables as per their dependency func (c *CVL) SortDepTables(inTableList []string) ([]string, CVLRetCode) { - return []string{}, CVL_NOT_IMPLEMENTED + + tableListMap := make(map[string]bool) + + //Skip all unknown tables + for ti := 0; ti < len(inTableList); ti++ { + _, exists := modelInfo.tableInfo[inTableList[ti]] + if !exists { + continue + } + + //Add to map to avoid duplicate nodes + tableListMap[inTableList[ti]] = true + } + + tableList := []string{} + + //Add all the table names in graph nodes + graph := toposort.NewGraph(len(tableListMap)) + for tbl := range tableListMap { + graph.AddNodes(tbl) + tableList = append(tableList, tbl) + } + + //Add all dependency egdes + c.addDepEdges(graph, tableList) + + //Now perform topological sort + result, ret := graph.Toposort() + if !ret { + return nil, CVL_ERROR + } + + return result, CVL_SUCCESS } //GetOrderedTables Get the order list(parent then child) of tables in a given YANG module //within a single model this is obtained using leafref relation func (c *CVL) GetOrderedTables(yangModule string) ([]string, CVLRetCode) { - return []string{}, CVL_NOT_IMPLEMENTED + tableList := []string{} + + //Get all the table names under this model + for tblName, tblNameInfo := range modelInfo.tableInfo { + if (tblNameInfo.modelName == yangModule) { + tableList = append(tableList, tblName) + } + } + + return c.SortDepTables(tableList) +} + +func (c *CVL) GetOrderedDepTables(yangModule, tableName string) ([]string, CVLRetCode) { + tableList := []string{} + + if _, exists := modelInfo.tableInfo[tableName]; !exists { + return nil, CVL_ERROR + } + + //Get all the table names under this yang module + for tblName, tblNameInfo := range modelInfo.tableInfo { + if (tblNameInfo.modelName == yangModule) { + tableList = append(tableList, tblName) + } + } + + graph := toposort.NewGraph(len(tableList)) + redisTblTo := getYangListToRedisTbl(tableName) + graph.AddNodes(redisTblTo) + + for _, tbl := range tableList { + if (tableName == tbl) { + //same table, continue + continue + } + redisTblFrom := getYangListToRedisTbl(tbl) + + //map for checking duplicate edge + dupEdgeCheck := map[string]string{} + + for _, leafRefs := range modelInfo.tableInfo[tbl].leafRef { + for _, leafRef := range leafRefs { + // If no relation through leaf-ref, then skip + if !(strings.Contains(leafRef.path, tableName + "_LIST")) { + continue + } + + // if target node of leaf-ref is not key, then skip + var isLeafrefTargetIsKey bool + for _, key := range modelInfo.tableInfo[tbl].keys { + if key == leafRef.targetNodeName { + isLeafrefTargetIsKey = true + } + } + if !(isLeafrefTargetIsKey) { + continue + } + + toName, exists := dupEdgeCheck[redisTblFrom] + if exists && (toName == redisTblTo) { + //Don't add duplicate edge + continue + } + + //Add and store the edge in map + graph.AddNodes(redisTblFrom) + graph.AddEdge(redisTblFrom, redisTblTo) + dupEdgeCheck[redisTblFrom] = redisTblTo + } + } + } + + //Now perform topological sort + result, ret := graph.Toposort() + if !ret { + return nil, CVL_ERROR + } + + return result, CVL_SUCCESS +} + +func (c *CVL) addDepTables(tableMap map[string]bool, tableName string) { + + //Mark it is added in list + tableMap[tableName] = true + + //Now find all tables referred in leafref from this table + for _, leafRefs := range modelInfo.tableInfo[tableName].leafRef { + for _, leafRef := range leafRefs { + for _, refTbl := range leafRef.yangListNames { + c.addDepTables(tableMap, getYangListToRedisTbl(refTbl)) //call recursively + } + } + } } //GetDepTables Get the list of dependent tables for a given table in a YANG module func (c *CVL) GetDepTables(yangModule string, tableName string) ([]string, CVLRetCode) { - return []string{}, CVL_NOT_IMPLEMENTED + tableList := []string{} + tblMap := make(map[string]bool) + + if _, exists := modelInfo.tableInfo[tableName]; !exists { + CVL_LOG(INFO_DEBUG, "GetDepTables(): Unknown table %s\n", tableName) + return []string{}, CVL_ERROR + } + + c.addDepTables(tblMap, tableName) + + for tblName := range tblMap { + tableList = append(tableList, tblName) + } + + //Add all the table names in graph nodes + graph := toposort.NewGraph(len(tableList)) + for ti := 0; ti < len(tableList); ti++ { + CVL_LOG(INFO_DEBUG, "GetDepTables(): Adding node %s\n", tableList[ti]) + graph.AddNodes(tableList[ti]) + } + + //Add all dependency egdes + c.addDepEdges(graph, tableList) + + //Now perform topological sort + result, ret := graph.Toposort() + if !ret { + return nil, CVL_ERROR + } + + return result, CVL_SUCCESS +} + +//Parses the JSON string buffer and returns +//array of dependent fields to be deleted +func getDepDeleteField(refKey, hField, hValue, jsonBuf string) ([]CVLDepDataForDelete) { + //Parse the JSON map received from lua script + var v interface{} + b := []byte(jsonBuf) + if err := json.Unmarshal(b, &v); err != nil { + return []CVLDepDataForDelete{} + } + + depEntries := []CVLDepDataForDelete{} + + var dataMap map[string]interface{} = v.(map[string]interface{}) + + for tbl, keys := range dataMap { + for key, fields := range keys.(map[string]interface{}) { + tblKey := tbl + modelInfo.tableInfo[getRedisTblToYangList(tbl, key)].redisKeyDelim + key + entryMap := make(map[string]map[string]string) + entryMap[tblKey] = make(map[string]string) + + for field := range fields.(map[string]interface{}) { + if ((field != hField) && (field != (hField + "@"))){ + continue + } + + if (field == (hField + "@")) { + //leaf-list - specific value to be deleted + entryMap[tblKey][field]= hValue + } else { + //leaf - specific field to be deleted + entryMap[tblKey][field]= "" + } + } + depEntries = append(depEntries, CVLDepDataForDelete{ + RefKey: refKey, + Entry: entryMap, + }) + } + } + + return depEntries } //GetDepDataForDelete Get the dependent (Redis keys) to be deleted or modified //for a given entry getting deleted func (c *CVL) GetDepDataForDelete(redisKey string) ([]CVLDepDataForDelete) { - return []CVLDepDataForDelete{} + + type filterScript struct { + script string + field string + value string + } + + tableName, key := splitRedisKey(redisKey) + // Determine the correct redis table name + // For ex. LOOPBACK_INTERFACE_LIST and LOOPBACK_INTERFACE_IPADDR_LIST are + // present in same container LOOPBACK_INTERFACE + tableName = getRedisTblToYangList(tableName, key) + + if (tableName == "") || (key == "") { + CVL_LOG(INFO_DEBUG, "GetDepDataForDelete(): Unknown or invalid table %s\n", + tableName) + } + + if _, exists := modelInfo.tableInfo[tableName]; !exists { + CVL_LOG(INFO_DEBUG, "GetDepDataForDelete(): Unknown table %s\n", tableName) + return []CVLDepDataForDelete{} + } + + redisKeySep := modelInfo.tableInfo[tableName].redisKeyDelim + redisMultiKeys := strings.Split(key, redisKeySep) + + // There can be multiple leaf in Reference table with leaf-ref to same target field + // Hence using array of filterScript and redis.StringSliceCmd + mCmd := map[string][]*redis.StringSliceCmd{} + mFilterScripts := map[string][]filterScript{} + pipe := redisClient.Pipeline() + + for _, refTbl := range modelInfo.tableInfo[tableName].refFromTables { + + //check if ref field is a key + numKeys := len(modelInfo.tableInfo[refTbl.tableName].keys) + refRedisTblName := getYangListToRedisTbl(refTbl.tableName) + idx := 0 + + if (refRedisTblName == "") { + continue + } + + // Find the targetnode from leaf-refs on refTbl.field + var refTblTargetNodeName string + for _, refTblLeafRef := range modelInfo.tableInfo[refTbl.tableName].leafRef[refTbl.field] { + if (refTblLeafRef.path != "non-leafref") && (len(refTblLeafRef.yangListNames) > 0) { + var isTargetNodeFound bool + for k := range refTblLeafRef.yangListNames { + if refTblLeafRef.yangListNames[k] == tableName { + refTblTargetNodeName = refTblLeafRef.targetNodeName + isTargetNodeFound = true + break + } + } + if isTargetNodeFound { + break + } + } + } + + // Determine the correct value of key in case of composite key + if len(redisMultiKeys) > 1 { + rediskeyTblKeyPatterns := strings.Split(modelInfo.tableInfo[tableName].redisKeyPattern, redisKeySep) + for z := 1; z < len(rediskeyTblKeyPatterns); z++ { // Skipping 0th position, as it is a tableName + if rediskeyTblKeyPatterns[z] == fmt.Sprintf("{%s}", refTblTargetNodeName) { + key = redisMultiKeys[z - 1] + break + } + } + } + + if _, exists := mCmd[refTbl.tableName]; !exists { + mCmd[refTbl.tableName] = make([]*redis.StringSliceCmd, 0) + } + mCmdArr := mCmd[refTbl.tableName] + + for ; idx < numKeys; idx++ { + if (modelInfo.tableInfo[refTbl.tableName].keys[idx] != refTbl.field) { + continue + } + + expr := CreateFindKeyExpression(refTbl.tableName, map[string]string{refTbl.field: key}) + CVL_LOG(INFO_DEBUG, "GetDepDataForDelete()->CreateFindKeyExpression: %s\n", expr) + + mCmdArr = append(mCmdArr, pipe.Keys(expr)) + break + } + mCmd[refTbl.tableName] = mCmdArr + + if (idx == numKeys) { + //field is hash-set field, not a key, match with hash-set field + //prepare the lua filter script + // ex: (h['members'] == 'Ethernet4' or h['members@'] == 'Ethernet4' or + //(string.find(h['members@'], 'Ethernet4,') != nil) + //',' to include leaf-list case + if _, exists := mFilterScripts[refTbl.tableName]; !exists { + mFilterScripts[refTbl.tableName] = make([]filterScript, 0) + } + fltScrs := mFilterScripts[refTbl.tableName] + fltScrs = append(fltScrs, filterScript { + script: fmt.Sprintf("return (h['%s'] ~= nil and (h['%s'] == '%s' or h['%s'] == '[%s|%s]')) or " + + "(h['%s@'] ~= nil and ((h['%s@'] == '%s') or " + + "(string.find(h['%s@']..',', '%s,') ~= nil)))", + refTbl.field, refTbl.field, key, refTbl.field, tableName, key, + refTbl.field, refTbl.field, key, + refTbl.field, key), + field: refTbl.field, + value: key, + } ) + mFilterScripts[refTbl.tableName] = fltScrs + } + } + + _, err := pipe.Exec() + if err != nil { + CVL_LOG(WARNING, "Failed to fetch dependent key details for table %s", tableName) + } + pipe.Close() + + depEntries := []CVLDepDataForDelete{} + + //Add dependent keys which should be modified + for tableName, mFilterScriptArr := range mFilterScripts { + for _, mFilterScript := range mFilterScriptArr { + refEntries, err := luaScripts["filter_entries"].Run(redisClient, []string{}, + tableName + "|*", strings.Join(modelInfo.tableInfo[tableName].keys, "|"), + mFilterScript.script, mFilterScript.field).Result() + + if (err != nil) { + CVL_LOG(WARNING, "Lua script status: (%v)", err) + } + if (refEntries == nil) { + //No reference field found + continue + } + + refEntriesJson := string(refEntries.(string)) + + if (refEntriesJson != "") { + //Add all keys whose fields to be deleted + depEntries = append(depEntries, getDepDeleteField(redisKey, + mFilterScript.field, mFilterScript.value, refEntriesJson)...) + } + } + } + + keysArr := []string{} + for tblName, mCmdArr := range mCmd { + for idx := range mCmdArr { + keys := mCmdArr[idx] + res, err := keys.Result() + if (err != nil) { + CVL_LOG(WARNING, "Failed to fetch dependent key details for table %s", tblName) + continue + } + + //Add keys found + for _, k := range res { + entryMap := make(map[string]map[string]string) + entryMap[k] = make(map[string]string) + depEntries = append(depEntries, CVLDepDataForDelete{ + RefKey: redisKey, + Entry: entryMap, + }) + } + + keysArr = append(keysArr, res...) + } + } + + TRACE_LOG(INFO_API, INFO_TRACE, "GetDepDataForDelete() : input key %s, " + + "entries to be deleted : %v", redisKey, depEntries) + + //For each key, find dependent data for delete recursively + for i :=0; i< len(keysArr); i++ { + retDepEntries := c.GetDepDataForDelete(keysArr[i]) + depEntries = append(depEntries, retDepEntries...) + } + + return depEntries +} + +//Update global stats for all sessions +func updateValidationTimeStats(td time.Duration) { + statsMutex.Lock() + + cfgValidationStats.Hits++ + if (td > cfgValidationStats.Peak) { + cfgValidationStats.Peak = td + } + + cfgValidationStats.Time += td + + statsMutex.Unlock() } //GetValidationTimeStats Retrieve global stats @@ -559,4 +998,51 @@ func GetValidationTimeStats() ValidationTimeStats { //ClearValidationTimeStats Clear global stats func ClearValidationTimeStats() { + statsMutex.Lock() + + cfgValidationStats.Hits = 0 + cfgValidationStats.Peak = 0 + cfgValidationStats.Time = 0 + + statsMutex.Unlock() +} + +//CreateFindKeyExpression Create expression for searching DB entries based on given key fields and values. +// Expressions created will be like CFG_L2MC_STATIC_MEMBER_TABLE|*|*|Ethernet0 +func CreateFindKeyExpression(tableName string, keyFldValPair map[string]string) string { + var expr string + + refRedisTblName := getYangListToRedisTbl(tableName) + tempSlice := []string{refRedisTblName} + sep := modelInfo.tableInfo[tableName].redisKeyDelim + + tblKeyPatterns := strings.Split(modelInfo.tableInfo[tableName].redisKeyPattern, sep) + for z := 1; z < len(tblKeyPatterns); z++ { + fldFromPattern := tblKeyPatterns[z][1:len(tblKeyPatterns[z])-1] //remove "{" and "}" + if val, exists := keyFldValPair[fldFromPattern]; exists { + tempSlice = append(tempSlice, val) + } else { + tempSlice = append(tempSlice, "*") + } + } + + expr = strings.Join(tempSlice, sep) + + return expr +} + +// GetAllReferringTables Returns list of all tables and fields which has leaf-ref +// to given table. For ex. tableName="PORT" will return all tables and fields +// which has leaf-ref to "PORT" table. +func (c *CVL) GetAllReferringTables(tableName string) (map[string][]string) { + var refTbls = make(map[string][]string) + if tblInfo, exists := modelInfo.tableInfo[tableName]; exists { + for _, refTbl := range tblInfo.refFromTables { + fldArr := refTbls[refTbl.tableName] + fldArr = append(fldArr, refTbl.field) + refTbls[refTbl.tableName] = fldArr + } + } + + return refTbls } diff --git a/go.mod b/go.mod index 30829ee16e8c..165473375d07 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,11 @@ require ( github.com/go-redis/redis/v7 v7.0.0-beta.3.0.20190824101152-d19aba07b476 // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect - github.com/kylelemons/godebug v1.1.0 // indirect github.com/openconfig/gnmi v0.0.0-20200617225440-d2b4e6a45802 github.com/openconfig/goyang v0.0.0-20200309174518-a00bece872fc github.com/openconfig/ygot v0.7.1 github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 // indirect + github.com/philopon/go-toposort v0.0.0-20170620085441-9be86dbd762f golang.org/x/text v0.3.0 ) diff --git a/go.sum b/go.sum index 4f0831d843fe..c81d0ece567b 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,8 @@ github.com/openconfig/ygot v0.7.1 h1:kqDRYQpowXTr7EhGwr2BBDKJzqs+H8aFYjffYQ8lBsw github.com/openconfig/ygot v0.7.1/go.mod h1:5MwNX6DMP1QMf2eQjW+aJN/KNslVqRJtbfSL3SO6Urk= github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 h1:YtFkrqsMEj7YqpIhRteVxJxCeC3jJBieuLr0d4C4rSA= github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/philopon/go-toposort v0.0.0-20170620085441-9be86dbd762f h1:WyCn68lTiytVSkk7W1K9nBiSGTSRlUOdyTnSjwrIlok= +github.com/philopon/go-toposort v0.0.0-20170620085441-9be86dbd762f/go.mod h1:/iRjX3DdSK956SzsUdV55J+wIsQ+2IBWmBrB4RvZfk4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=