From 4af4337b2adeb053d9f9f4fe9aa18289c6795814 Mon Sep 17 00:00:00 2001 From: ShiriMoran <139739065+ShiriMoran@users.noreply.github.com> Date: Thu, 28 Sep 2023 08:42:39 +0300 Subject: [PATCH] 98 grouping with self loops (#152) added an optimization to grouping in which self loops are treated as don't cares --- .../examples/acl_testing3_with_grouping.txt | 4 +- ...ting1_new_groupingsubnetsBased_withPGW.txt | 4 +- pkg/vpcmodel/grouping.go | 43 +- pkg/vpcmodel/groupingSelfLoop.go | 367 ++++++++++++++++++ pkg/vpcmodel/grouping_test.go | 158 ++++++++ 5 files changed, 555 insertions(+), 21 deletions(-) create mode 100644 pkg/vpcmodel/groupingSelfLoop.go diff --git a/pkg/ibmvpc/examples/acl_testing3_with_grouping.txt b/pkg/ibmvpc/examples/acl_testing3_with_grouping.txt index 96979709b..91b56d2da 100644 --- a/pkg/ibmvpc/examples/acl_testing3_with_grouping.txt +++ b/pkg/ibmvpc/examples/acl_testing3_with_grouping.txt @@ -5,9 +5,7 @@ vsi1-ky[10.240.10.4] => Public Internet 161.26.0.0/16 : protocol: UDP * vsi1-ky[10.240.10.4] => vsi2-ky[10.240.20.4] : protocol: TCP,UDP vsi2-ky[10.240.20.4] => Public Internet (all ranges) : All Connections vsi2-ky[10.240.20.4] => vsi1-ky[10.240.10.4] : All Connections * -vsi3a-ky[10.240.30.5],vsi3b-ky[10.240.30.6] => vsi3c-ky[10.240.30.4] : All Connections vsi3a-ky[10.240.30.5],vsi3b-ky[10.240.30.6],vsi3c-ky[10.240.30.4] => vsi1-ky[10.240.10.4] : All Connections * -vsi3a-ky[10.240.30.5],vsi3c-ky[10.240.30.4] => vsi3b-ky[10.240.30.6] : All Connections -vsi3b-ky[10.240.30.6],vsi3c-ky[10.240.30.4] => vsi3a-ky[10.240.30.5] : All Connections +vsi3a-ky[10.240.30.5],vsi3b-ky[10.240.30.6],vsi3c-ky[10.240.30.4] => vsi3a-ky[10.240.30.5],vsi3b-ky[10.240.30.6],vsi3c-ky[10.240.30.4] : All Connections connections are stateful unless marked with * diff --git a/pkg/ibmvpc/examples/sg_testing1_new_groupingsubnetsBased_withPGW.txt b/pkg/ibmvpc/examples/sg_testing1_new_groupingsubnetsBased_withPGW.txt index 3c666ad44..4b2da2897 100644 --- a/pkg/ibmvpc/examples/sg_testing1_new_groupingsubnetsBased_withPGW.txt +++ b/pkg/ibmvpc/examples/sg_testing1_new_groupingsubnetsBased_withPGW.txt @@ -1,7 +1,5 @@ combined connections between subnets: subnet1-ky => Public Internet (all ranges) : All Connections -subnet1-ky => subnet2-ky,subnet3-ky : All Connections -subnet2-ky => subnet1-ky,subnet3-ky : All Connections -subnet3-ky => subnet1-ky,subnet2-ky : All Connections +subnet1-ky,subnet2-ky,subnet3-ky => subnet1-ky,subnet2-ky,subnet3-ky : All Connections connections are stateful unless marked with * diff --git a/pkg/vpcmodel/grouping.go b/pkg/vpcmodel/grouping.go index cb397e989..2d985f8cb 100644 --- a/pkg/vpcmodel/grouping.go +++ b/pkg/vpcmodel/grouping.go @@ -145,8 +145,8 @@ func (g *groupingConnections) addPublicConnectivity(ep EndpointElem, conn string (*g)[ep][conn] = append((*g)[ep][conn], targetNode) } -// vsiGroupingBySubnets returns a slice of EndpointElem objects produced from an input slice, by grouping -// set of elements that represent network interface nodes from the same subnet into a single groupedNetworkInterfaces object +// vsiGroupingBySubnets returns a slice of EndpointElem objects, by grouping set of elements that +// represent network interface nodes from the same subnet into a single groupedNetworkInterfaces object func vsiGroupingBySubnets(groupedConnLines *GroupConnLines, elemsList []EndpointElem, c *CloudConfig) []EndpointElem { res := []EndpointElem{} @@ -174,7 +174,8 @@ func vsiGroupingBySubnets(groupedConnLines *GroupConnLines, return res } -// subnetGrouping returns a slice of EndpointElem objects produced from an input slice, by grouping EndpointElem that represents a subnet +// subnetGrouping returns a slice of EndpointElem objects produced from an input slice, by grouping +// set of elements that represent subnets into a single groupedNetworkInterfaces object func subnetGrouping(groupedConnLines *GroupConnLines, elemsList []EndpointElem) []EndpointElem { res := []EndpointElem{} @@ -244,6 +245,20 @@ func (g *GroupConnLines) groupExternalAddressesForSubnets() { g.GroupedLines = res } +// aux func, returns true iff the EndpointElem is Node if grouping vsis or NodeSet if grouping subnets +func isInternalOfRequiredType(ep EndpointElem, groupVsi bool) bool { + if groupVsi { // groups vsis Nodes + if _, ok := ep.(Node); !ok { + return false + } + } else { // groups subnets NodeSets + if _, ok := ep.(NodeSet); !ok { + return false + } + } + return true +} + // groups src/targets for either Vsis or Subnets func (g *GroupConnLines) groupLinesByKey(srcGrouping, groupVsi bool) (res []*GroupedConnLine, groupingSrcOrDst map[string][]*GroupedConnLine) { @@ -253,24 +268,22 @@ func (g *GroupConnLines) groupLinesByKey(srcGrouping, groupVsi bool) (res []*Gro // populate map groupingSrcOrDst for _, line := range g.GroupedLines { srcOrDst, dstOrSrc := line.getSrcOrDst(srcGrouping), line.getSrcOrDst(!srcGrouping) - if groupVsi { // groups vsis Nodes - if _, ok := srcOrDst.(Node); !ok { - res = append(res, line) - continue - } - } else { // groups subnets NodeSets - if _, ok := srcOrDst.(NodeSet); !ok { - res = append(res, line) - continue - } + if !isInternalOfRequiredType(srcOrDst, groupVsi) { + res = append(res, line) + continue } - key := dstOrSrc.Name() + ";" + line.Conn + key := getKeyOfGroupConnLines(dstOrSrc, line.Conn) if _, ok := groupingSrcOrDst[key]; !ok { groupingSrcOrDst[key] = []*GroupedConnLine{} } groupingSrcOrDst[key] = append(groupingSrcOrDst[key], line) } - return res, groupingSrcOrDst + newGroupingSrcOrDst := g.extendGroupingSelfLoops(groupingSrcOrDst, srcGrouping) + return res, newGroupingSrcOrDst +} + +func getKeyOfGroupConnLines(ep EndpointElem, connection string) string { + return ep.Name() + commaSeparator + connection } // assuming the g.groupedLines was already initialized by previous step groupExternalAddresses() diff --git a/pkg/vpcmodel/groupingSelfLoop.go b/pkg/vpcmodel/groupingSelfLoop.go new file mode 100644 index 000000000..17e50fdc4 --- /dev/null +++ b/pkg/vpcmodel/groupingSelfLoop.go @@ -0,0 +1,367 @@ +package vpcmodel + +import ( + "strings" +) + +// extends grouping by considering self loops as don't care https://github.com/np-guard/vpc-network-config-analyzer/issues/98 +// e.g. a => b,c b => a, c and c => a,b is actually a clique a,b,c => a,b,c +// a => b,c, b => c can be presented in one line as a,b => b,c + +// After the basic grouping, which is of worst time complexity O(n^2), we optimize grouping treating self loops as don't care. +// Intuitively, we check if two GroupedConnLine can be merged treating self loops as don't care +// 1. groupsToBeMerged find couples of GroupedConnLine that should be merged using the alg below +// mergeSelfLoops merges the groupsToBeMerged: +// 2. It creates sets of GroupedConnLine s.t. each set should be merged +// note that the "should be merged" is an equivalence relation +// 3. It merges them + +// alg: GroupedConnLine to be merged: +// GroupedConnLine whose distance is an empty set should be merged; +// the claim below guarantees that the result is coherent +// +// The distance between two GroupedConnLine is defined as following: +// Let l_1 be a line with source s_1 and dest d_1 and let l_2 be a line with source s_2 and dest d_2. +// l_1 / l_2 is the vsis/subnets in d_1 that are not in d_2 minus the single vsi/subnet in s_1 if |s_1| = 1 +// +// The distance between lines l_1 and l_2 is l_1 / l_2 union l_2 / l_1 +// +// claim: if the distance between line l_1 and l_2 is an empty set and +// the distance between lines l_2 and l_3 is an empty set +// then so is the distance between l_1 and l_3 + +// main function. Input: +// output: grouping after treating self loops as don't care +func (g *GroupConnLines) extendGroupingSelfLoops(groupingSrcOrDst map[string][]*GroupedConnLine, + srcGrouping bool) map[string][]*GroupedConnLine { + toMergeCouples := g.groupsToBeMerged(groupingSrcOrDst, srcGrouping) + return mergeSelfLoops(toMergeCouples, groupingSrcOrDst, srcGrouping) +} + +// detects couples of groups that can be merged when self loops are treated as don't cares +// Input: +// Output: couples of groups that can be merged. Each couple is presented as a couple of strings where each string is the +// key of the group from the map of the current grouping +func (g *GroupConnLines) groupsToBeMerged(groupingSrcOrDst map[string][]*GroupedConnLine, srcGrouping bool) (toMergeCouples [][2]string) { + toMergeCouples = make([][2]string, 0) + // the to be grouped src/dst in set representation, will be needed to compute potential groups to be merged + // and to compute the deltas + setsToGroup := createGroupingSets(groupingSrcOrDst, srcGrouping) + relevantKeys := g.relevantKeysToCompare(groupingSrcOrDst) + keyToMergeCandidates := g.findMergeCandidates(groupingSrcOrDst, srcGrouping, setsToGroup, relevantKeys) + + for _, key := range relevantKeys { + keyLines := groupingSrcOrDst[key] + // is there a different line s.t. the keyLines were not merged only due to self loops? + // going over all couples of items: merging them if they differ only in self loop element + // findMergeCandidates of a singleton 'key' are all lines in which the group contains 'key' + // if key is not a singleton then findMergeCandidates will be empty + mergeCandidates, ok := keyToMergeCandidates[key] + if !ok { + continue + } + for candidate := range mergeCandidates { + candidateLines := groupingSrcOrDst[candidate] + // delta between keyLines to candidateLines must be 0 + mergeGroups := isDeltaOfGroupedLinesZero(srcGrouping, keyLines, candidateLines, setsToGroup[key], setsToGroup[candidate]) + // delta between the keyLines is 0 - merge keyLines + if mergeGroups { + toMergeCouples = append(toMergeCouples, [2]string{key, candidate}) + } + } + } + return toMergeCouples +} + +// gets a list of keys of groups that have the potential of being merged with the +// self loop don't care optimization +// a group is candidate to be merged only if it has only internal nodes +// and if vsis then of the same subnet +// the latter follows from the 3rd condition described in findMergeCandidates +// Input: +// Output: +func (g *GroupConnLines) relevantKeysToCompare(groupingSrcOrDst map[string][]*GroupedConnLine) (relevantKeys []string) { + relevantKeys = make([]string, 0, len(groupingSrcOrDst)) + for key, lines := range groupingSrcOrDst { + if lines[0].isSrcOrDstExternalNodes() { + continue + } + // if vsi's then the subnets must be equal; if not vsis then empty string equals empty string + if g.getSubnetIfVsi(lines[0].Src) != g.getSubnetIfVsi(lines[0].Dst) { + continue + } + relevantKeys = append(relevantKeys, key) + } + return +} + +// optimization to reduce the worst case of finding couples to merge from O(n^4) to O(n^2) +// where n is the number of nodes. +// a couple of []*GroupedConnLine is candidate to be merged only if: +// 1. They are of the same connection +// 2. If vsis, of the same subnet +// 3. The src (dst) in one group is a singleton contained in the dst (src) in the other +// in one pass on groupingSrcOrDst we prepare a map between each key to the keys that are candidates to be merged with it. +// Before the grouping there are at most O(n^2) lines of src -> dst +// The last condition implies that each original src -> dst (where src and dst are a single endpoint) can induce a single +// candidate (at most), and each singleton key have at most n candidates. Hence, there are at most +// O(n^3) merge candidate, which implies O(n^3) time complexity of groupsToBeMerged +// +// Input: +// Output: +func (g *GroupConnLines) findMergeCandidates(groupingSrcOrDst map[string][]*GroupedConnLine, srcGrouping bool, + keyToGroupedSets map[string]map[string]struct{}, relevantKeys []string) map[string]map[string]struct{} { + // 1. Create buckets for each connection + vsi's subnet if vsi; merge candidates are within each bucket + bucketToKeys := make(map[string]map[string]struct{}) + for _, key := range relevantKeys { + lines := groupingSrcOrDst[key] + bucket := lines[0].Conn + subnetIfVsi := g.getSubnetIfVsi(lines[0].Src) + if subnetIfVsi != "" { + bucket += ";" + subnetIfVsi + } + if _, ok := bucketToKeys[bucket]; !ok { + bucketToKeys[bucket] = make(map[string]struct{}) + } + bucketToKeys[bucket][key] = struct{}{} + } + + keyToMergeCandidates := make(map[string]map[string]struct{}) + // 2. in each bucket finds for each key the candidates to be merged, in two stages + for _, keysInBucket := range bucketToKeys { + singletonsInBucket := make(map[string]string) + // 2.1 for a group g_1 s.t. the non-grouped src/dst is a singleton, + // all groups in which the grouped dst/src contains the singleton + // 2.1.1 finds for each bucket all singletons + for key := range keysInBucket { + lines := groupingSrcOrDst[key] + elemsInKey := elemInKeys(!srcGrouping, *lines[0]) + if len(elemsInKey) > 1 { // not a singleton + continue + } + singleton := elemsInKey[0] + singletonsInBucket[singleton] = key + } + // 2.1.2 finds for each singleton candidates: groups with that singleton + // stores the candidates in keyToMergeCandidates + for key := range keysInBucket { + itemsInGroup := keyToGroupedSets[key] + for item := range itemsInGroup { + if mergeCandidateKey, ok := singletonsInBucket[item]; ok { + if mergeCandidateKey != key { + if _, ok := keyToMergeCandidates[mergeCandidateKey]; !ok { + keyToMergeCandidates[mergeCandidateKey] = make(map[string]struct{}) + } + if _, ok := keyToMergeCandidates[key]; !ok { + keyToMergeCandidates[key] = make(map[string]struct{}) + } + keyToMergeCandidates[mergeCandidateKey][key] = struct{}{} + } + } + } + } + } + return keyToMergeCandidates +} + +func (g *GroupConnLines) getSubnetIfVsi(ep EndpointElem) string { + if isVsi, node := isEpVsi(ep); isVsi { + // if ep is groupedEndpointsElems of vsis then all belong to the same subnet + return g.c.getSubnetOfNode(node).Name() + } + return "" +} + +// input: Endpoint +// output: : +// if the endpoint element represents a vsi or is a slice of elements the first of which represents vsi +// then it returns +// otherwise it returns +func isEpVsi(ep EndpointElem) (bool, Node) { + if _, ok := ep.(*groupedEndpointsElems); ok { + ep1GroupedEps := ep.(*groupedEndpointsElems) + if node, ok := (*ep1GroupedEps)[0].(Node); ok { + if node.IsInternal() { + return true, node + } + } + return false, nil + } + if node, ok := ep.(Node); ok { + if node.IsInternal() { + return true, node + } + } + return false, nil +} + +// creates an aux database in which all the grouped endpoints are stored in a set +// Input: +// Output: +func createGroupingSets(groupingSrcOrDst map[string][]*GroupedConnLine, srcGrouping bool) map[string]map[string]struct{} { + keyToGroupedSets := make(map[string]map[string]struct{}) + for key, groupedConnLine := range groupingSrcOrDst { + mySet := make(map[string]struct{}) + for _, line := range groupedConnLine { + srcOrDst := line.getSrcOrDst(srcGrouping) + mySet[srcOrDst.Name()] = struct{}{} + } + keyToGroupedSets[key] = mySet + } + return keyToGroupedSets +} + +// computes delta between group connection lines as defined in the beginning of the file +// Input: +// Output: true if the delta is zero, false otherwise +func isDeltaOfGroupedLinesZero(srcGrouping bool, groupedConnLine1, groupedConnLine2 []*GroupedConnLine, + setToGroup1, setToGroup2 map[string]struct{}) bool { + // at least one of the keys must be a single vsi/subnet for the self loop check to be meaningful + if len(elemInKeys(srcGrouping, *groupedConnLine1[0])) > 1 && len(elemInKeys(srcGrouping, *groupedConnLine2[0])) > 1 { + return false + } + // is there is a real delta between sets and not only due to self loop + set1MinusSet2 := setMinusSet(srcGrouping, *groupedConnLine2[0], setToGroup1, setToGroup2) + set2MinusSet1 := setMinusSet(srcGrouping, *groupedConnLine1[0], setToGroup2, setToGroup1) + if len(set1MinusSet2) == 0 && len(set2MinusSet1) == 0 { + return true + } + return false +} + +// given a GroupedConnLine returns a list of the names of the endpoint elements +// in its key +func elemInKeys(srcGrouping bool, groupedConnLine GroupedConnLine) []string { + srcOrDst := groupedConnLine.getSrcOrDst(srcGrouping) + return strings.Split(srcOrDst.Name(), commaSeparator) +} + +// computes the distance between two GroupedConnLine as defined in the beginning of the file +// Input: +// Output: the distance between the groups +func setMinusSet(srcGrouping bool, groupedConnLine GroupedConnLine, set1, set2 map[string]struct{}) map[string]struct{} { + minusResult := make(map[string]struct{}) + for k := range set1 { + if _, ok := set2[k]; !ok { + minusResult[k] = struct{}{} + } + } + // if set2's groupedConnLine key has a single item, then this single item is not relevant to the delta + // since any EndpointElement is connected to itself + if len(elemInKeys(srcGrouping, groupedConnLine)) == 1 { + keyOfGrouped2 := groupedConnLine.getSrcOrDst(!srcGrouping) // all non-grouping items are the same in a groupedConnLine + delete(minusResult, keyOfGrouped2.Name()) // if keyOfGrouped2.Name() does not exist in minusResult then this is no-op + } + return minusResult +} + +func (g *GroupedConnLine) isSrcOrDstExternalNodes() bool { + if _, ok := g.Src.(*groupedExternalNodes); ok { + return true + } + if _, ok := g.Dst.(*groupedExternalNodes); ok { + return true + } + return false +} + +// actual merge of groupedConnLine that should be merged +// input: +// output: +func mergeSelfLoops(toMergeCouples [][2]string, oldGroupingSrcOrDst map[string][]*GroupedConnLine, + srcGrouping bool) map[string][]*GroupedConnLine { + // 1. Create dedicated data structure: a slice of slices of string toMergeList s.t. each slice contains a list of keys to be merged + // and a map toMergeExistingIndexes between key to its index in the slice + toMergeList := make([][]string, 0) + toMergeExistingIndexes := make(map[string]int) + for _, coupleKeys := range toMergeCouples { + existingIndx1, ok1 := toMergeExistingIndexes[coupleKeys[0]] + existingIndx2, ok2 := toMergeExistingIndexes[coupleKeys[1]] + switch ok1 { + case true: + if !ok2 { + toMergeExistingIndexes[coupleKeys[1]] = existingIndx1 + toMergeList[existingIndx1] = append(toMergeList[existingIndx1], coupleKeys[1]) + } + case false: + if ok2 { + toMergeExistingIndexes[coupleKeys[0]] = existingIndx2 + toMergeList[existingIndx2] = append(toMergeList[existingIndx2], coupleKeys[0]) + } else { + // if both []*GroupedConnLine already exist in toMergeExistingIndexes then + // existingIndx1 equals existingIndx2 and nothing to be done here + nextIndx := len(toMergeList) + toMergeExistingIndexes[coupleKeys[0]], toMergeExistingIndexes[coupleKeys[1]] = nextIndx, nextIndx + newList := []string{coupleKeys[0], coupleKeys[1]} + toMergeList = append(toMergeList, newList) + } + } + } + // 2. Performs the actual merge + // Build New map[string][]*GroupedConnLine : + mergedGroupedConnLine := make(map[string][]*GroupedConnLine) + // 2.1 go over the new data structure, merge groups to be merged and add to New + // 2.2 go over old map[string][]*GroupedConnLine and for each element whose key not in toMergeKeys then just add it as is + for _, toBeMergedKeys := range toMergeList { + newKey, newGroupedConnLines := mergeGivenList(oldGroupingSrcOrDst, srcGrouping, toBeMergedKeys) + mergedGroupedConnLine[newKey] = newGroupedConnLines + } + for oldKey, oldLines := range oldGroupingSrcOrDst { + // not merged with other groups, add as is + if _, ok := toMergeExistingIndexes[oldKey]; !ok { + mergedGroupedConnLine[oldKey] = oldLines + } + } + return mergedGroupedConnLine +} + +// given a list of keys to be merged from the old grouping, computes unique list of endpoints +// of either sources or destination as by srcGrouping +// returns the unique list of endpoints and the connection +// input: +// output: +func listOfUniqueEndpoints(oldGroupingSrcOrDst map[string][]*GroupedConnLine, srcGrouping bool, + toMergeKeys []string) (listOfEndpoints groupedEndpointsElems, conn string) { + setOfNames := make(map[string]struct{}) + listOfEndpoints = make(groupedEndpointsElems, 0) + for _, oldKeyToMerge := range toMergeKeys { + for _, line := range oldGroupingSrcOrDst[oldKeyToMerge] { + endPointInKey := line.getSrcOrDst(!srcGrouping) + if conn == "" { + conn = line.Conn // connection is the same for all lines to be merged + } + if _, isSliceEndpoints := endPointInKey.(*groupedEndpointsElems); isSliceEndpoints { + for _, endpoint := range *endPointInKey.(*groupedEndpointsElems) { + if _, ok := setOfNames[endpoint.Name()]; !ok { // was endpoint added already? + listOfEndpoints = append(listOfEndpoints, endpoint) + setOfNames[endpoint.Name()] = struct{}{} + } + } + } else { // endpoint is Node or NodeSet + if _, ok := setOfNames[endPointInKey.Name()]; !ok { // was endpoint added already? + listOfEndpoints = append(listOfEndpoints, endPointInKey) + setOfNames[endPointInKey.Name()] = struct{}{} + } + } + } + } + return +} + +// merges a list of GroupedConnLine to be merged +// input: +// output: new key, new []*GroupedConnLine +func mergeGivenList(oldGroupingSrcOrDst map[string][]*GroupedConnLine, srcGrouping bool, + toMergeKeys []string) (newKey string, newGroupedConnLine []*GroupedConnLine) { + epsInNewKey, _ := listOfUniqueEndpoints(oldGroupingSrcOrDst, srcGrouping, toMergeKeys) + epsInNewLines, conn := listOfUniqueEndpoints(oldGroupingSrcOrDst, !srcGrouping, toMergeKeys) + for _, epInLineValue := range epsInNewLines { + if srcGrouping { + newGroupedConnLine = append(newGroupedConnLine, &GroupedConnLine{epInLineValue, &epsInNewKey, conn}) + } else { + newGroupedConnLine = append(newGroupedConnLine, &GroupedConnLine{&epsInNewKey, epInLineValue, conn}) + } + } + newKey = getKeyOfGroupConnLines(&epsInNewKey, conn) + return +} diff --git a/pkg/vpcmodel/grouping_test.go b/pkg/vpcmodel/grouping_test.go index e934d2f53..cd5327245 100644 --- a/pkg/vpcmodel/grouping_test.go +++ b/pkg/vpcmodel/grouping_test.go @@ -219,3 +219,161 @@ func TestIPRange(t *testing.T) { fmt.Println(groupingStr) fmt.Println("done") } + +// Simple test of self loop (don't care): clique of the same subnet. Should end in a single line +func configSelfLoopClique() (*CloudConfig, *VPCConnectivity) { + res := &CloudConfig{Nodes: []Node{}} + res.Nodes = append(res.Nodes, + &mockNetIntf{cidr: "10.0.20.5/32", name: "vsi1"}, + &mockNetIntf{cidr: "10.0.20.6/32", name: "vsi2"}, + &mockNetIntf{cidr: "10.0.20.7/32", name: "vsi3"}) + + res.NodeSets = append(res.NodeSets, &mockSubnet{"10.0.20.0/22", "subnet1", []Node{res.Nodes[0], res.Nodes[1], res.Nodes[2]}}) + + res1 := &VPCConnectivity{AllowedConnsCombined: NewNodesConnectionsMap()} + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[0], res.Nodes[1], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[0], res.Nodes[2], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[1], res.Nodes[0], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[1], res.Nodes[2], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[2], res.Nodes[1], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[2], res.Nodes[0], common.NewConnectionSet(true)) + + return res, res1 +} + +func TestSelfLoopClique(t *testing.T) { + c, v := configSelfLoopClique() + res := &GroupConnLines{c: c, v: v, srcToDst: newGroupingConnections(), dstToSrc: newGroupingConnections(), + groupedEndpointsElemsMap: make(map[string]*groupedEndpointsElems), + groupedExternalNodesMap: make(map[string]*groupedExternalNodes)} + res.groupExternalAddresses() + res.groupInternalSrcOrDst(true, true) + groupingStr := res.String() + require.Equal(t, "vsi1,vsi2,vsi3 => vsi1,vsi2,vsi3 : All Connections\n\n"+ + "connections are stateful unless marked with *\n", groupingStr) + fmt.Println(groupingStr) + fmt.Println("done") +} + +// Simple test of self loop (don't care): clique in which the vsis belongs to two subnets. +// Should end in three lines +func configSelfLoopCliqueDiffSubnets() (*CloudConfig, *VPCConnectivity) { + res := &CloudConfig{Nodes: []Node{}} + res.Nodes = append(res.Nodes, + &mockNetIntf{cidr: "10.0.20.5/32", name: "vsi1-1"}, + &mockNetIntf{cidr: "10.0.20.6/32", name: "vsi1-2"}, + &mockNetIntf{cidr: "10.240.10.7/32", name: "vsi2-1"}) + + res.NodeSets = append(res.NodeSets, &mockSubnet{"10.0.20.0/22", "subnet1", []Node{res.Nodes[0], res.Nodes[1]}}, + &mockSubnet{"10.240.10.0/22", "subnet2", []Node{res.Nodes[2]}}) + + res1 := &VPCConnectivity{AllowedConnsCombined: NewNodesConnectionsMap()} + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[0], res.Nodes[1], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[0], res.Nodes[2], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[1], res.Nodes[0], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[1], res.Nodes[2], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[2], res.Nodes[1], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[2], res.Nodes[0], common.NewConnectionSet(true)) + + return res, res1 +} + +func TestSelfLoopCliqueDiffSubnets(t *testing.T) { + c, v := configSelfLoopCliqueDiffSubnets() + res := &GroupConnLines{c: c, v: v, srcToDst: newGroupingConnections(), dstToSrc: newGroupingConnections(), + groupedEndpointsElemsMap: make(map[string]*groupedEndpointsElems), + groupedExternalNodesMap: make(map[string]*groupedExternalNodes)} + res.groupExternalAddresses() + res.groupInternalSrcOrDst(true, true) + res.groupInternalSrcOrDst(false, true) + groupingStr := res.String() + require.Equal(t, "vsi1-1,vsi1-2 => vsi1-1,vsi1-2 : All Connections\n"+ + "vsi1-1,vsi1-2 => vsi2-1 : All Connections\n"+ + "vsi2-1 => vsi1-1,vsi1-2 : All Connections\n\n"+ + "connections are stateful unless marked with *\n", groupingStr) + fmt.Println(groupingStr) + fmt.Println("done") +} + +// Simple test of self loop: two lines with 3 vsis of the same subnet and same connection. +// +// should end in a single line, where one of the vsis being added a self loop +func configSimpleSelfLoop() (*CloudConfig, *VPCConnectivity) { + res := &CloudConfig{Nodes: []Node{}} + res.Nodes = append(res.Nodes, + &mockNetIntf{cidr: "10.0.20.5/32", name: "vsi1"}, + &mockNetIntf{cidr: "10.0.20.6/32", name: "vsi2"}, + &mockNetIntf{cidr: "10.0.20.7/32", name: "vsi3"}) + + res.NodeSets = append(res.NodeSets, &mockSubnet{"10.0.20.0/22", "subnet1", []Node{res.Nodes[0], res.Nodes[1], res.Nodes[2]}}) + + res1 := &VPCConnectivity{AllowedConnsCombined: NewNodesConnectionsMap()} + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[0], res.Nodes[1], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[0], res.Nodes[2], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[1], res.Nodes[2], common.NewConnectionSet(true)) + + return res, res1 +} + +func TestSimpleSelfLoop(t *testing.T) { + c, v := configSimpleSelfLoop() + res := &GroupConnLines{c: c, v: v, srcToDst: newGroupingConnections(), dstToSrc: newGroupingConnections(), + groupedEndpointsElemsMap: make(map[string]*groupedEndpointsElems), + groupedExternalNodesMap: make(map[string]*groupedExternalNodes)} + res.groupExternalAddresses() + res.groupInternalSrcOrDst(false, true) + res.groupInternalSrcOrDst(true, true) + groupingStr := res.String() + require.Equal(t, "vsi1,vsi2 => vsi2,vsi3 : All Connections\n\n"+ + "connections are stateful unless marked with *\n", groupingStr) + fmt.Println(groupingStr) + fmt.Println("done") +} + +// Test of self loop (don't care): clique of the same subnet + a simple lace. +// todo: Should end in a single line for the clique and two more lines for the lace +// +// but ends in another local minimal grouping. Do we want to optimize? +// try source and then dest and vice versa and choose the one +// with less lines? +func configSelfLoopCliqueLace() (*CloudConfig, *VPCConnectivity) { + res := &CloudConfig{Nodes: []Node{}} + res.Nodes = append(res.Nodes, + &mockNetIntf{cidr: "10.0.20.5/32", name: "vsi1"}, + &mockNetIntf{cidr: "10.0.20.6/32", name: "vsi2"}, + &mockNetIntf{cidr: "10.0.20.7/32", name: "vsi3"}, + &mockNetIntf{cidr: "10.0.20.7/32", name: "vsi4"}, + &mockNetIntf{cidr: "10.0.20.7/32", name: "vsi5"}) + + res.NodeSets = append(res.NodeSets, &mockSubnet{"10.0.20.0/22", "subnet1", + []Node{res.Nodes[0], res.Nodes[1], res.Nodes[2], res.Nodes[3], res.Nodes[4]}}) + + res1 := &VPCConnectivity{AllowedConnsCombined: NewNodesConnectionsMap()} + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[0], res.Nodes[1], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[0], res.Nodes[2], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[1], res.Nodes[0], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[1], res.Nodes[2], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[2], res.Nodes[1], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[2], res.Nodes[0], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[2], res.Nodes[3], common.NewConnectionSet(true)) + res1.AllowedConnsCombined.updateAllowedConnsMap(res.Nodes[3], res.Nodes[4], common.NewConnectionSet(true)) + + return res, res1 +} + +func TestConfigSelfLoopCliqueLace(t *testing.T) { + c, v := configSelfLoopCliqueLace() + res := &GroupConnLines{c: c, v: v, srcToDst: newGroupingConnections(), dstToSrc: newGroupingConnections(), + groupedEndpointsElemsMap: make(map[string]*groupedEndpointsElems), + groupedExternalNodesMap: make(map[string]*groupedExternalNodes)} + res.groupExternalAddresses() + res.groupInternalSrcOrDst(false, true) + res.groupInternalSrcOrDst(true, true) + groupingStr := res.String() + require.Equal(t, "vsi1,vsi2 => vsi1,vsi2,vsi3 : All Connections\n"+ + "vsi3 => vsi1,vsi2,vsi4 : All Connections\n"+ + "vsi4 => vsi5 : All Connections\n\n"+ + "connections are stateful unless marked with *\n", groupingStr) + fmt.Println(groupingStr) + fmt.Println("done") +}