Skip to content

Commit

Permalink
Removed usage of ConnectionSet.
Browse files Browse the repository at this point in the history
Signed-off-by: Tanya <[email protected]>
  • Loading branch information
tanyaveksler committed May 19, 2024
1 parent e9b1e7f commit 511a634
Show file tree
Hide file tree
Showing 187 changed files with 2,356 additions and 3,181 deletions.
63 changes: 61 additions & 2 deletions nca/CoreDS/ConnectivityProperties.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .MethodSet import MethodSet
from .Peer import PeerSet, BasePeerSet
from .ProtocolNameResolver import ProtocolNameResolver
from .ProtocolSet import ProtocolSet
from .MinDFA import MinDFA
from .ConnectivityCube import ConnectivityCube

Expand Down Expand Up @@ -99,9 +100,9 @@ def __bool__(self):

def __str__(self):
if self.is_all():
return ''
return 'All connections'
if not super().__bool__():
return 'Empty'
return 'No connections'
if self.active_dimensions == ['dst_ports']:
assert (len(self) == 1)
for cube in self:
Expand All @@ -115,6 +116,9 @@ def __str__(self):
def __hash__(self):
return super().__hash__()

def __lt__(self, other):
return len(self) < len(other)

def get_connectivity_cube(self, cube):
"""
translate the ordered cube to ConnectivityCube format
Expand Down Expand Up @@ -299,6 +303,10 @@ def print_diff(self, other, self_name, other_name):
:return: If self!=other, return a string showing a (source, target) pair that appears in only one of them
:rtype: str
"""
if self.is_all() and not other.is_all():
return self_name + ' allows all connections while ' + other_name + ' does not.'
if not self.is_all() and other.is_all():
return other_name + ' allows all connections while ' + self_name + ' does not.'
self_minus_other = self - other
other_minus_self = other - self
diff_str = self_name if self_minus_other else other_name
Expand Down Expand Up @@ -566,3 +574,54 @@ def _reorder_list_by_map(orig_list, new_to_old_map):
for i in range(len(orig_list)):
res.append(orig_list[new_to_old_map[i]])
return res

@staticmethod
def extract_src_dst_peers_from_cube(the_cube, peer_container, relevant_protocols=ProtocolSet(True)):
all_peers = peer_container.get_all_peers_group(True)
conn_cube = the_cube.copy()
src_peers = conn_cube["src_peers"] or all_peers
conn_cube.unset_dim("src_peers")
dst_peers = conn_cube["dst_peers"] or all_peers
conn_cube.unset_dim("dst_peers")
protocols = conn_cube["protocols"]
conn_cube.unset_dim("protocols")
if not conn_cube.has_active_dim() and (protocols == relevant_protocols or protocols.is_whole_range()):
props = ConnectivityProperties.make_all_props()
else:
conn_cube["protocols"] = protocols
assert conn_cube.has_active_dim()
props = ConnectivityProperties.make_conn_props(conn_cube)
return props, src_peers, dst_peers

def get_simplified_connections_representation(self, is_str, use_complement_simplification=True):
"""
Get a simplified representation of the connectivity properties - choose shorter version between self
and its complement.
representation as str is a string representation, and not str is representation as list of objects.
The representation is used at fw-rules representation of the connection.
:param bool is_str: should get str representation (True) or list representation (False)
:param bool use_complement_simplification: should choose shorter rep between self and complement
:return: the required representation of the connection set
:rtype Union[str, list]
"""
if self.is_all():
return "All connections" if is_str else ["All connections"]
if not super().__bool__():
return "No connections" if is_str else ["No connections"]

compl = ConnectivityProperties.make_all_props() - self
if len(self) > len(compl) and use_complement_simplification:
compl_rep = compl._get_connections_representation(is_str)
return f'All but {compl_rep}' if is_str else [{"All but": compl_rep}]
else:
return self._get_connections_representation(is_str)

def _get_connections_representation(self, is_str):
cubes_list = [self.get_cube_dict(cube, is_str) for cube in self]
if is_str:
return ','.join(self._get_cube_str_representation(cube) for cube in cubes_list)
return cubes_list

@staticmethod
def _get_cube_str_representation(cube):
return '{' + ','.join(f'{item[0]}:{item[1]}' for item in cube.items()) + '}'
151 changes: 26 additions & 125 deletions nca/FWRules/ConnectivityGraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import itertools
from collections import defaultdict
import networkx
from nca.CoreDS.Peer import IpBlock, ClusterEP, Pod
from nca.CoreDS.Peer import IpBlock, Pod
from nca.CoreDS.ProtocolSet import ProtocolSet
from nca.CoreDS.ConnectivityProperties import ConnectivityProperties
from .DotGraph import DotGraph
from .MinimizeFWRules import MinimizeBasic, MinimizeFWRules
from .ClusterInfo import ClusterInfo


Expand All @@ -26,32 +26,14 @@ def __init__(self, all_peers, allowed_labels, output_config):
:param allowed_labels: the set of allowed labels to be used in generated fw-rules, extracted from policy yamls
:param output_config: OutputConfiguration object
"""
# connections_to_peers holds the connectivity graph
# props_to_peers holds the connectivity graph
self.output_config = output_config
self.connections_to_peers = defaultdict(list)
self.props_to_peers = defaultdict(list)
if self.output_config.fwRulesOverrideAllowedLabels:
allowed_labels = set(label for label in self.output_config.fwRulesOverrideAllowedLabels.split(','))
self.cluster_info = ClusterInfo(all_peers, allowed_labels)
self.allowed_labels = allowed_labels

def add_edge(self, source_peer, dest_peer, connections):
"""
Adding a labeled edge to the graph
:param Peer source_peer: The source peer
:param Peer dest_peer: The dest peer
:param ConnectionSet connections: The allowed connections from source_peer to dest_peer
:return: None
"""
self.connections_to_peers[connections].append((source_peer, dest_peer))

def add_edges(self, connections):
"""
Adding a set of labeled edges to the graph
:param dict connections: a map from ConnectionSet to (src, dest) pairs
:return: None
"""
self.connections_to_peers.update(connections)

def add_edges_from_cube_dict(self, conn_cube, peer_container, connectivity_restriction=None):
"""
Add edges to the graph according to the give cube
Expand All @@ -68,13 +50,13 @@ def add_edges_from_cube_dict(self, conn_cube, peer_container, connectivity_restr
else: # connectivity_restriction == 'non-TCP'
relevant_protocols = ProtocolSet.get_non_tcp_protocols()

conns, src_peers, dst_peers = \
MinimizeBasic.get_connection_set_and_peers_from_cube(conn_cube, peer_container, relevant_protocols)
props, src_peers, dst_peers = \
ConnectivityProperties.extract_src_dst_peers_from_cube(conn_cube, peer_container, relevant_protocols)
split_src_peers = src_peers.split()
split_dst_peers = dst_peers.split()
for src_peer in split_src_peers:
for dst_peer in split_dst_peers:
self.connections_to_peers[conns].append((src_peer, dst_peer))
self.props_to_peers[props].append((src_peer, dst_peer))

def add_props_to_graph(self, props, peer_container, connectivity_restriction=None):
"""
Expand Down Expand Up @@ -290,16 +272,16 @@ def _get_equals_groups(self):
"""
# for each peer, we get a list of (peer,conn,direction) that it connected to:
peers_edges = {peer: [] for peer in set(self.cluster_info.all_peers)}
edges_connections = dict()
for connection, peer_pairs in self.connections_to_peers.items():
if not connection:
edges_props = dict()
for props, peer_pairs in self.props_to_peers.items():
if not props:
continue
for src_peer, dst_peer in peer_pairs:
if src_peer != dst_peer:
peers_edges[src_peer].append((dst_peer, connection, False))
peers_edges[dst_peer].append((src_peer, connection, True))
edges_connections[(src_peer, dst_peer)] = connection
edges_connections[(dst_peer, src_peer)] = connection
peers_edges[src_peer].append((dst_peer, props, False))
peers_edges[dst_peer].append((src_peer, props, True))
edges_props[(src_peer, dst_peer)] = props
edges_props[(dst_peer, src_peer)] = props

# for each peer, adding a self edge only for connection that the peer already have:
for peer, peer_edges in peers_edges.items():
Expand All @@ -311,7 +293,7 @@ def _get_equals_groups(self):
# find groups of peers that are also connected to each other:
connected_groups, left_out = self._find_equal_groups(peers_edges)
# for every group, also add the connection of the group (should be only one)
connected_groups = [(group, edges_connections.get((group[0], group[1]), None)) for group in connected_groups]
connected_groups = [(group, edges_props.get((group[0], group[1]), None)) for group in connected_groups]

# removing the peers of groups that we already found:
peers_edges = {peer: edges for peer, edges in peers_edges.items() if peer in left_out}
Expand All @@ -332,8 +314,8 @@ def get_connections_without_fw_rules_txt_format(self, connectivity_msg=None, exc
:return: a string of the original peers connectivity graph content (without minimization of fw-rules)
"""
lines = set()
for connections, peer_pairs in self.connections_to_peers.items():
if not connections:
for props, peer_pairs in self.props_to_peers.items():
if not props:
continue
for src_peer, dst_peer in peer_pairs:
if src_peer != dst_peer:
Expand All @@ -343,8 +325,7 @@ def get_connections_without_fw_rules_txt_format(self, connectivity_msg=None, exc
# not be added either
if exclude_self_loop_conns and src_peer_name == dst_peer_name:
continue
conn_str = connections.get_simplified_connections_representation(True)
conn_str = conn_str.title() if not conn_str.isupper() else conn_str
conn_str = props.get_simplified_connections_representation(True)
lines.add(f'{src_peer_name} => {dst_peer_name} : {conn_str}')

lines_list = []
Expand All @@ -370,7 +351,7 @@ def get_connectivity_dot_format_str(self, connectivity_restriction=None, simplif
# we are going to treat a peers_group as one peer.
# the first peer in the peers_group is representing the group
# we will add the text of all the peers in the group to this peer
for peers_group, group_connection in peers_groups:
for peers_group, group_props in peers_groups:
peer_name, node_type, nc_name, text = self._get_peer_details(peers_group[0])
if len(peers_group) > 1:
text = sorted(set(self._get_peer_details(peer)[3][0] for peer in peers_group))
Expand All @@ -379,20 +360,20 @@ def get_connectivity_dot_format_str(self, connectivity_restriction=None, simplif
node_type = DotGraph.NodeType.MultiPod if len(text) > 1 else node_type
dot_graph.add_node(nc_name, peer_name, node_type, text)
# adding the self edges:
if len(text) > 1 and group_connection:
conn_str = group_connection.get_simplified_connections_representation(True)
conn_str = conn_str.replace("Protocol:", "").replace('All connections', 'All')
if len(text) > 1 and group_props:
conn_str = group_props.get_simplified_connections_representation(True)
conn_str = conn_str.replace('All connections', 'All')
dot_graph.add_edge(peer_name, peer_name, label=conn_str, is_dir=False)

representing_peers = [multi_peer[0][0] for multi_peer in peers_groups]
for connections, peer_pairs in self.connections_to_peers.items():
for props, peer_pairs in self.props_to_peers.items():
directed_edges = set()
# todo - is there a better way to get edge details?
# we should revisit this code after reformatting connections labels
conn_str = connections.get_simplified_connections_representation(True)
conn_str = conn_str.replace("Protocol:", "").replace('All connections', 'All')
conn_str = props.get_simplified_connections_representation(True)
conn_str = conn_str.replace('All connections', 'All')
for src_peer, dst_peer in peer_pairs:
if src_peer != dst_peer and connections and src_peer in representing_peers and dst_peer in representing_peers:
if src_peer != dst_peer and props and src_peer in representing_peers and dst_peer in representing_peers:
src_peer_name, _, src_nc, _ = self._get_peer_details(src_peer)
dst_peer_name, _, dst_nc, _ = self._get_peer_details(dst_peer)
directed_edges.add(((src_peer_name, src_nc), (dst_peer_name, dst_nc)))
Expand All @@ -412,83 +393,3 @@ def get_connectivity_dot_format_str(self, connectivity_restriction=None, simplif
for edge in undirected_edges | cliques_edges:
dot_graph.add_edge(src_name=edge[0][0], dst_name=edge[1][0], label=conn_str, is_dir=False)
return dot_graph.to_str(self.output_config.outputFormat == 'dot')

def get_minimized_firewall_rules(self):
"""
computes and returns minimized firewall rules from original connectivity graph
:return: minimize_fw_rules: an object of type MinimizeFWRules holding the minimized fw-rules
"""

connections_sorted_by_size = list(self.connections_to_peers.items())
connections_sorted_by_size.sort(reverse=True)

connections_sorted_by_size = self._merge_ip_blocks(connections_sorted_by_size)

if self.output_config.fwRulesRunInTestMode:
# print the original connectivity graph
lines = set()
for connections, peer_pairs in connections_sorted_by_size:
for src_peer, dst_peer in peer_pairs:
src_peer_name = self._get_peer_details(src_peer)[0]
dst_peer_name = self._get_peer_details(dst_peer)[0]
# on level of deployments, omit the 'all connections' between a pod to itself
# a connection between deployment to itself is derived from connection between 2 different pods of
# the same deployment
if src_peer == dst_peer and self.output_config.outputEndpoints == 'deployments':
continue
lines.add(f'src: {src_peer_name}, dest: {dst_peer_name}, allowed conns: {connections}')
for line in lines:
print(line)
print('======================================================')
# compute the minimized firewall rules
return MinimizeFWRules.minimize_firewall_rules(self.cluster_info, self.output_config, connections_sorted_by_size)

@staticmethod
def _merge_ip_blocks(connections_sorted_by_size):
"""
Given an input connectivity graph, merge ip-blocks for peer-pairs when possible. e.g. if (pod_x ,
0.0.0.0-49.49.255.255) and ) and (pod_x, 49.50.0.0-255.255.255.255) are in connections_sorted_by_size[conn],
then in the output result, only (pod_x, 0.0.0.0-255.255.255.255) will be in: connections_sorted_by_size[conn]
:param connections_sorted_by_size: the original connectivity graph : a list of tuples
(connection set , peer_pairs), where peer_pairs is a list of (src,dst) tuples
:return: connections_sorted_by_size_new : a new connectivity graph with merged ip-blocks
"""
connections_sorted_by_size_new = []
for connections, peer_pairs in connections_sorted_by_size:
map_ip_blocks_per_dst = dict()
map_ip_blocks_per_src = dict()
merged_peer_pairs = []
for (src, dst) in peer_pairs:
if isinstance(src, IpBlock) and isinstance(dst, ClusterEP):
if dst not in map_ip_blocks_per_dst:
map_ip_blocks_per_dst[dst] = src.copy()
else:
map_ip_blocks_per_dst[dst] |= src
elif isinstance(dst, IpBlock) and isinstance(src, ClusterEP):
if src not in map_ip_blocks_per_src:
map_ip_blocks_per_src[src] = dst.copy()
else:
map_ip_blocks_per_src[src] |= dst
else:
merged_peer_pairs.append((src, dst))
for (src, ip_block) in map_ip_blocks_per_src.items():
merged_peer_pairs.append((src, ip_block))
for (dst, ip_block) in map_ip_blocks_per_dst.items():
merged_peer_pairs.append((ip_block, dst))
connections_sorted_by_size_new.append((connections, merged_peer_pairs))

return connections_sorted_by_size_new

def conn_graph_has_fw_rules(self):
"""
:return: bool flag indicating if the given conn_graph has fw_rules (and not considered empty)
"""
if not self.connections_to_peers:
return False
if len((self.connections_to_peers.items())) == 1:
conn = list(self.connections_to_peers.keys())[0]
# we currently do not create fw-rules for "no connections"
if not conn: # conn is "no connections":
return False
return True
21 changes: 11 additions & 10 deletions nca/FWRules/DotGraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ def _edge_to_str(self, edge):
line += f'[{label} {tooltip} color={edge_color} fontcolor=darkgreen {arrow_type}]\n'
return line

@staticmethod
def get_val_by_key_from_list(the_list, key):
res_items = [item for item in the_list if key in item]
return res_items[0].split(':')[1] if res_items else ''

def _set_labels_dict(self):
"""
creates a dict of label -> to label_short
Expand All @@ -227,19 +232,15 @@ def _set_labels_dict(self):
labels_short = {}
# for each label, the short will look like "tcp<port>" if there is a port, or "TCP" if there is no port
for label in self.labels:
splitted_label = label.split(' ', 1)
label_type = splitted_label.pop(0)
label_port = splitted_label[0] if splitted_label else ''
if label_port.startswith('{'):
# it is not a port, its a list of dict, a dict can have 'dst_ports'
# we will use only one 'dst_ports':
connections = ast.literal_eval(f'[{label_port}]')
ports = [conn['dst_ports'] for conn in connections if 'dst_ports' in conn.keys()]
label_port = ports[0] if ports else ''
splitted_label = label.replace('{', '').replace('}', '').split(',')
label_type = self.get_val_by_key_from_list(splitted_label, 'protocols')
label_port = self.get_val_by_key_from_list(splitted_label, 'dst_ports')
assert label == 'All' or label_type
# a 'dst_ports' can be too long (like 'port0,port1-port2' ) we trim it to the first port:
if len(label_port) > 6:
label_port = label_port.split(',')[0].split('-')[0]
labels_short[label] = f'{label_type.lower()}{label_port}' if label_port else label_type
labels_short[label] = 'All' if label == 'All' else f'{label_type.lower()}{label_port}' if label_port \
else label_type

# for labels sharing the same short, we will add a letter to the end of the short:
for short in set(labels_short.values()):
Expand Down
Loading

0 comments on commit 511a634

Please sign in to comment.