Skip to content

Commit 0693c99

Browse files
tanyaveksleradisos
andauthored
Equivalence query optimized implementation (#524)
* Optimized implementation of EquivalenceQuery. Signed-off-by: Tanya <[email protected]> * Align EquivalenceQuery difference explanation according to the original implementation. Signed-off-by: Tanya <[email protected]> * Fixing lint errors. Signed-off-by: Tanya <[email protected]> * Fix: when src_peers/dst_peers is not active domain, returning the current full domain (according To DimensionManager) rather than None. Signed-off-by: Tanya <[email protected]> * Fix the fix: when src_peers/dst_peers is not active domain, returning the current full domain (according To DimensionManager) rather than None. No more expect src_peers/dst_peers be None. Check for is_active_dim instead. Signed-off-by: Tanya <[email protected]> * yet another small fix of the fix. Signed-off-by: Tanya <[email protected]> * When computing projection on one dimension, return the full dimension value for inactive dimensions. Adding check in command-line flow for non-implemented optimized queries. Optimized equivalence query code refactoring for better readability. Signed-off-by: Tanya <[email protected]> * Ignoring 'complex function' lint error. Returning 'passed' code for skipped queries. Signed-off-by: Tanya <[email protected]> * Undo returning 'passed' code for skipped queries. Skipping non-implemented optimized cmdline queries in run_all_tests Signed-off-by: Tanya <[email protected]> * Skipping non-implemented optimized cmdline queries in run_all_tests Signed-off-by: Tanya <[email protected]> * Fixed the refined code of optimized equivalence calculation. Signed-off-by: Tanya <[email protected]> * Update nca/NetworkConfig/NetworkConfigQuery.py Co-authored-by: Adi Sosnovich <[email protected]> * Reused definition of implemented_opt_queries Signed-off-by: Tanya <[email protected]> * Fix of reusing definition of implemented_opt_queries Signed-off-by: Tanya <[email protected]> * Removed redundant method. Signed-off-by: Tanya <[email protected]> --------- Signed-off-by: Tanya <[email protected]> Co-authored-by: Adi Sosnovich <[email protected]>
1 parent f456275 commit 0693c99

File tree

10 files changed

+137
-36
lines changed

10 files changed

+137
-36
lines changed

nca/CoreDS/CanonicalHyperCubeSet.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,3 +850,6 @@ def _apply_layer_elements_union(self):
850850
layer_0_new_elem |= elem
851851
new_layers[layer_0_new_elem] = layer_1_elem
852852
self.layers = new_layers
853+
854+
def is_active_dimension(self, dim_name):
855+
return dim_name in self.active_dimensions

nca/CoreDS/ConnectivityCube.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def __getitem__(self, dim_name):
141141
# translate CanonicalIntervalSet back to PeerSet
142142
return BasePeerSet().get_peer_set_by_indices(dim_value)
143143
else:
144-
return None
144+
return BasePeerSet().get_peer_set_by_indices(DimensionsManager().get_dimension_domain_by_name(dim_name))
145145
elif dim_name in ["src_ports", "dst_ports"]:
146146
res = PortSet()
147147
res.add_ports(dim_value)

nca/CoreDS/ConnectivityProperties.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,10 @@ def project_on_one_dimension(self, dim_name):
345345
"""
346346
assert dim_name not in ["icmp_type", "icmp_code"] # not supporting icmp dimensions
347347
if dim_name not in self.active_dimensions:
348-
return None
348+
if dim_name == "src_peers" or dim_name == "dst_peers":
349+
return BasePeerSet().get_peer_set_by_indices(DimensionsManager().get_dimension_domain_by_name(dim_name))
350+
else:
351+
return DimensionsManager().get_dimension_domain_by_name(dim_name)
349352
if dim_name == "src_peers" or dim_name == "dst_peers":
350353
res = PeerSet()
351354
elif dim_name == "src_ports" or dim_name == "dst_ports":
@@ -397,9 +400,9 @@ def make_conn_props(conn_cube):
397400

398401
src_ports = conn_cube["src_ports"]
399402
dst_ports = conn_cube["dst_ports"]
400-
dst_peers = conn_cube["dst_peers"]
401403
assert not src_ports.named_ports and not src_ports.excluded_named_ports
402-
if (not dst_ports.named_ports and not dst_ports.excluded_named_ports) or not dst_peers:
404+
if (not dst_ports.named_ports and not dst_ports.excluded_named_ports) or \
405+
not conn_cube.is_active_dim("dst_peers"):
403406
# Should not resolve named ports
404407
return ConnectivityProperties._make_conn_props_no_named_ports_resolution(conn_cube)
405408

@@ -414,7 +417,7 @@ def make_conn_props(conn_cube):
414417

415418
# Resolving dst named ports
416419
protocols = conn_cube["protocols"]
417-
assert dst_peers
420+
dst_peers = conn_cube["dst_peers"]
418421
for peer in dst_peers:
419422
real_ports = ConnectivityProperties._resolve_named_ports(dst_ports.named_ports, peer, protocols)
420423
if real_ports:

nca/NetworkConfig/NetworkConfig.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,25 @@ def append_policy_to_config(self, policy):
316316
:return: None
317317
"""
318318
self.policies_container.append_policy(policy)
319+
320+
def filter_conns_by_peer_types(self, conns, all_peers):
321+
"""
322+
Filter the given connections by removing several connection kinds that are never allowed
323+
(such as IpBlock to IpBlock connections, connections from DNSEntries, and more).
324+
:param ConnectivityProperties conns: the given connections.
325+
:param PeerSet all_peers: all peers in the system.
326+
:return The resulting connections.
327+
:rtype ConnectivityProperties
328+
"""
329+
res = conns
330+
# avoid IpBlock -> {IpBlock, DNSEntry} connections
331+
all_ips = Peer.IpBlock.get_all_ips_block_peer_set()
332+
all_dns_entries = self.peer_container.get_all_dns_entries()
333+
ip_to_ip_or_dns_conns = ConnectivityProperties.make_conn_props_from_dict({"src_peers": all_ips,
334+
"dst_peers": all_ips | all_dns_entries})
335+
res -= ip_to_ip_or_dns_conns
336+
# avoid DNSEntry->anything connections
337+
dns_to_any_conns = ConnectivityProperties.make_conn_props_from_dict({"src_peers": all_dns_entries,
338+
"dst_peers": all_peers})
339+
res -= dns_to_any_conns
340+
return res

nca/NetworkConfig/NetworkConfigQuery.py

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -184,28 +184,6 @@ def execute(self, cmd_line_flag):
184184
def exec(self):
185185
raise NotImplementedError
186186

187-
def filter_conns_by_peer_types(self, conns, all_peers):
188-
"""
189-
Filter the given connections by removing several connection kinds that are never allowed
190-
(such as IpBlock to IpBlock connections, connections from DNSEntries, and more).
191-
:param ConnectivityProperties conns: the given connections.
192-
:param PeerSet all_peers: all peers in the system.
193-
:return The resulting connections.
194-
:rtype ConnectivityProperties
195-
"""
196-
res = conns
197-
# avoid IpBlock -> {IpBlock, DNSEntry} connections
198-
all_ips = IpBlock.get_all_ips_block_peer_set()
199-
all_dns_entries = self.config.peer_container.get_all_dns_entries()
200-
ip_to_ip_or_dns_conns = ConnectivityProperties.make_conn_props_from_dict({"src_peers": all_ips,
201-
"dst_peers": all_ips | all_dns_entries})
202-
res -= ip_to_ip_or_dns_conns
203-
# avoid DNSEntry->anything connections
204-
dns_to_any_conns = ConnectivityProperties.make_conn_props_from_dict({"src_peers": all_dns_entries,
205-
"dst_peers": all_peers})
206-
res -= dns_to_any_conns
207-
return res
208-
209187

210188
class DisjointnessQuery(NetworkConfigQuery):
211189
"""
@@ -832,7 +810,7 @@ def compute_connectivity_output_optimized(self):
832810
subset_conns = ConnectivityProperties.make_conn_props_from_dict({"src_peers": subset_peers}) | \
833811
ConnectivityProperties.make_conn_props_from_dict({"dst_peers": subset_peers})
834812
all_conns_opt &= subset_conns
835-
all_conns_opt = self.filter_conns_by_peer_types(all_conns_opt, opt_peers_to_compare)
813+
all_conns_opt = self.config.filter_conns_by_peer_types(all_conns_opt, opt_peers_to_compare)
836814
if self.config.policies_container.layers.does_contain_layer(NetworkLayerName.Istio):
837815
output_res, opt_fw_rules_tcp, opt_fw_rules_non_tcp = \
838816
self.get_props_output_split_by_tcp(all_conns_opt, opt_peers_to_compare)
@@ -1185,9 +1163,36 @@ def disjoint_referenced_ip_blocks(self):
11851163
:rtype: PeerSet
11861164
"""
11871165
exclude_ipv6 = self.output_config.excludeIPv6Range
1166+
# TODO - consider including also non referenced IPBlocks, as in ConnectivityMapQuery
1167+
# (see issue https://github.com/IBM/network-config-analyzer/issues/522)
11881168
return IpBlock.disjoint_ip_blocks(self.config1.get_referenced_ip_blocks(exclude_ipv6),
11891169
self.config2.get_referenced_ip_blocks(exclude_ipv6), exclude_ipv6)
11901170

1171+
def filter_conns_by_input_or_internal_constraints(self, conns1, conns2):
1172+
"""
1173+
Given two allowed connections (in config1 and in config2 respectively), filter those connections
1174+
according to required IP blocks (external constrain - excludeIPv6Range option) and
1175+
peer types (internal constraints).
1176+
:param conns1: the first config allowed connections
1177+
:param conns2: the second config allowed connections
1178+
:rtype: [ConnectivityProperties, ConnectivityProperties]
1179+
:return: two resulting allowed connections
1180+
"""
1181+
peers_to_compare = conns1.project_on_one_dimension('src_peers') | conns1.project_on_one_dimension('dst_peers') | \
1182+
conns2.project_on_one_dimension('src_peers') | conns2.project_on_one_dimension('dst_peers')
1183+
exclude_ipv6 = self.output_config.excludeIPv6Range
1184+
ref_ip_blocks = self.config1.get_referenced_ip_blocks(exclude_ipv6) | \
1185+
self.config2.get_referenced_ip_blocks(exclude_ipv6)
1186+
ip_blocks_mask = IpBlock() if ref_ip_blocks else IpBlock.get_all_ips_block(exclude_ipv6)
1187+
for ip_block in ref_ip_blocks:
1188+
ip_blocks_mask |= ip_block
1189+
peers_to_compare.filter_ipv6_blocks(ip_blocks_mask)
1190+
conns_filter = ConnectivityProperties.make_conn_props_from_dict({"src_peers": peers_to_compare,
1191+
"dst_peers": peers_to_compare})
1192+
res_conns1 = self.config1.filter_conns_by_peer_types(conns1, peers_to_compare) & conns_filter
1193+
res_conns2 = self.config2.filter_conns_by_peer_types(conns2, peers_to_compare) & conns_filter
1194+
return res_conns1, res_conns2
1195+
11911196
@staticmethod
11921197
def clone_without_ingress(config):
11931198
"""
@@ -1227,7 +1232,12 @@ def exec(self, cmd_line_flag=False, layer_name=None):
12271232
if query_answer.output_result:
12281233
query_answer.numerical_result = not query_answer.bool_result
12291234
return query_answer
1235+
if self.config1.optimized_run == 'false':
1236+
return self.check_equivalence_original(layer_name)
1237+
else:
1238+
return self.check_equivalence_optimized(layer_name)
12301239

1240+
def check_equivalence_original(self, layer_name=None):
12311241
peers_to_compare = \
12321242
self.config1.peer_container.get_all_peers_group(include_dns_entries=True)
12331243
peers_to_compare |= self.disjoint_referenced_ip_blocks()
@@ -1252,6 +1262,49 @@ def exec(self, cmd_line_flag=False, layer_name=None):
12521262
return QueryAnswer(True, self.name1 + ' and ' + self.name2 + ' are semantically equivalent.',
12531263
numerical_result=0)
12541264

1265+
def _append_different_conns_to_list(self, conn_props, different_conns_list, props_based_on_config1):
1266+
"""
1267+
Adds difference between config1 and config2 connectivities into the list of differences
1268+
:param ConnectivityProperties conn_props: connectivity properties representing a difference between config1 and config2
1269+
:param list different_conns_list: the list to add differences to
1270+
:param bool props_based_on_config1: whether conn_props represent connections present in config1 but not in config2
1271+
(the value True) or connections present in config2 but not in config1 (the value False)
1272+
"""
1273+
no_conns = ConnectionSet()
1274+
for cube in conn_props:
1275+
conn_cube = conn_props.get_connectivity_cube(cube)
1276+
conns, src_peers, dst_peers = \
1277+
ConnectionSet.get_connection_set_and_peers_from_cube(conn_cube, self.config1.peer_container)
1278+
conns1 = conns if props_based_on_config1 else no_conns
1279+
conns2 = no_conns if props_based_on_config1 else conns
1280+
if self.output_config.fullExplanation:
1281+
if self.config1.optimized_run == 'true':
1282+
different_conns_list.append(PeersAndConnections(str(src_peers), str(dst_peers), conns1, conns2))
1283+
else: # 'debug': produce the same output format as in the original implementation (per peer pairs)
1284+
for src_peer in src_peers:
1285+
for dst_peer in dst_peers:
1286+
if src_peer != dst_peer:
1287+
different_conns_list.append(PeersAndConnections(str(src_peer), str(dst_peer),
1288+
conns1, conns2))
1289+
else:
1290+
different_conns_list.append(PeersAndConnections(src_peers.rep(), dst_peers.rep(), conns1, conns2))
1291+
1292+
def check_equivalence_optimized(self, layer_name=None):
1293+
conn_props1 = self.config1.allowed_connections_optimized(layer_name)
1294+
conn_props2 = self.config2.allowed_connections_optimized(layer_name)
1295+
all_conns1, all_conns2 = self.filter_conns_by_input_or_internal_constraints(conn_props1.all_allowed_conns,
1296+
conn_props2.all_allowed_conns)
1297+
if all_conns1 == all_conns2:
1298+
return QueryAnswer(True, self.name1 + ' and ' + self.name2 + ' are semantically equivalent.',
1299+
numerical_result=0)
1300+
1301+
conns1_not_in_conns2 = all_conns1 - all_conns2
1302+
conns2_not_in_conns1 = all_conns2 - all_conns1
1303+
different_conns_list = []
1304+
self._append_different_conns_to_list(conns1_not_in_conns2, different_conns_list, True)
1305+
self._append_different_conns_to_list(conns2_not_in_conns1, different_conns_list, False)
1306+
return self._query_answer_with_relevant_explanation(sorted(different_conns_list))
1307+
12551308
def _query_answer_with_relevant_explanation(self, explanation_list):
12561309
output_result = self.name1 + ' and ' + self.name2 + ' are not semantically equivalent.'
12571310
explanation_description = f'Connections allowed in {self.name1} which are different in {self.name2}'

nca/Parsers/GenericIngressLikeYamlParser.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,11 @@ def _make_rules_from_conns(conn_props):
7676
peers_to_conns = {}
7777
res = []
7878
# extract peers dimension from cubes
79+
assert not conn_props.is_active_dimension("src_peers")
7980
for cube in conn_props:
8081
conn_cube = conn_props.get_connectivity_cube(cube)
81-
src_peer_set = conn_cube["src_peers"]
82-
conn_cube.unset_dim("src_peers")
8382
dst_peer_set = conn_cube["dst_peers"]
8483
conn_cube.unset_dim("dst_peers")
85-
assert not src_peer_set
8684
new_props = ConnectivityProperties.make_conn_props(conn_cube)
8785
new_conns = ConnectionSet()
8886
new_conns.add_connections('TCP', new_props)

nca/SchemeRunner.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class SchemeRunner(GenericYamlParser):
1717
This class takes a scheme file, build all its network configurations and runs all its queries
1818
"""
1919

20+
implemented_opt_queries = set(['connectivityMap', 'equivalence'])
21+
2022
def __init__(self, scheme_file_name, output_format=None, output_path=None, optimized_run='false'):
2123
GenericYamlParser.__init__(self, scheme_file_name)
2224
self.network_configs = {}
@@ -35,6 +37,10 @@ def __init__(self, scheme_file_name, output_format=None, output_path=None, optim
3537
if not isinstance(self.scheme, dict):
3638
self.syntax_error("The scheme's top-level object must be a map")
3739

40+
@staticmethod
41+
def has_implemented_opt_queries(queries):
42+
return SchemeRunner.implemented_opt_queries.intersection(queries)
43+
3844
def _get_input_file(self, given_path, out_flag=False):
3945
"""
4046
Attempts to locate a file specified in the scheme file (possibly relatively to the scheme file)
@@ -190,10 +196,9 @@ def run_queries(self, query_array):
190196
not_executed = 0
191197
self.check_fields_validity(query, 'query', allowed_elements)
192198
query_name = query['name']
193-
if self.optimized_run == 'debug':
199+
if self.optimized_run == 'debug' or self.optimized_run == 'true':
194200
# TODO - update/remove the optimization below when all queries are supported in optimized implementation
195-
# optimization - currently only connectivityMap query has optimized implementation and can be compared
196-
if not query.get('connectivityMap'):
201+
if not self.has_implemented_opt_queries(set(query.keys())):
197202
print(f'Skipping query {query_name} since it does not have optimized implementation yet')
198203
continue
199204
print('Running query', query_name)

nca/nca_cli.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def _make_recursive(path_list):
133133
return path_list
134134

135135

136-
def run_args(args):
136+
def run_args(args): # noqa: C901
137137
"""
138138
Given the parsed cmdline, decide what to run
139139
:param Namespace args: argparse-style parsed cmdline
@@ -216,6 +216,12 @@ def run_args(args):
216216
pair_query_flag = True
217217
expected_output = args.expected_output or None
218218

219+
if args.optimized_run == 'debug' or args.optimized_run == 'true':
220+
# TODO - update/remove the optimization below when all queries are supported in optimized implementation
221+
if not SchemeRunner.has_implemented_opt_queries({query_name}):
222+
print(f'Not running query {query_name} since it does not have optimized implementation yet')
223+
return _compute_return_value(0, 0, 1)
224+
219225
resources_handler = ResourcesHandler()
220226
network_config = resources_handler.get_network_config(_make_recursive(np_list), _make_recursive(ns_list),
221227
_make_recursive(pod_list), _make_recursive(resource_list),

tests/istio_testcases/example_policies/bookinfo-demo/sidecar_examples/equivalence-with-sidecars-scheme.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ queries:
4848
- sidecar-with-selector-registery-only
4949
outputConfiguration:
5050
fullExplanation: True
51-
expectedOutput: ../../../expected_output/equiv_configs_w_sidecars_different_outbound_mode.txt
51+
# expectedOutput in the optimized solution is more refined than in the original one.
52+
# uncomment the line below and updated the expectedOutput result after moving to optimized solution.
53+
#expectedOutput: ../../../expected_output/equiv_configs_w_sidecars_different_outbound_mode.txt
5254
expected: 1 # not equal , the second restricts conns to ip-blocks for app: ratings
5355

5456
- name: strong-equiv-allow-any-different-outbound-modes

tests/run_all_tests.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ def initialize_test(self):
104104

105105
def run_all_test_flow(self, all_results):
106106
# should be overriden by inheriting classes
107+
tmp_opt = [i for i in self.test_queries_obj.args_obj.args if '-opt=' in i]
108+
opt = tmp_opt[0].split('=')[1] if tmp_opt else 'false'
109+
if isinstance(self.test_queries_obj, CliQuery) and (opt == 'debug' or opt == 'true'):
110+
implemented_opt_queries = ['--connectivity']
111+
# TODO - update/remove the optimization below when all queries are supported in optimized implementation
112+
if not set(implemented_opt_queries).intersection(set(self.test_queries_obj.args_obj.args)):
113+
print(f'Skipping {self.test_queries_obj.test_name} since it does not have optimized implementation yet')
114+
return 0, 0
115+
107116
self.initialize_test()
108117
self.run_test()
109118
self.evaluate_test_results()

0 commit comments

Comments
 (0)