2 # NEPI, a framework to manage network experiments
3 # Copyright (C) 2013 INRIA
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 # Author: Alina Quereilhac <alina.quereilhac@inria.fr>
33 ## - AQ: Add support for hypergraphs (to be able to add hyper edges to
34 ## model CSMA or wireless networks)
36 class NetGraph(object):
37 """ NetGraph represents a network topology.
38 Network graphs are internally using the networkx library.
42 def __init__(self, **kwargs):
43 """ A graph can be generated using a specified pattern
44 (LADDER, MESH, TREE, etc), or provided as an argument.
46 :param graph: Undirected graph to use as internal representation
47 :type graph: networkx.Graph
49 :param topo_type: One of TopologyType.{LINEAR,LADDER,MESH,TREE,STAR}
50 used to automatically generate the topology graph.
51 :type topo_type: TopologyType
53 :param node_count: Number of nodes in the topology to be generated.
56 :param branches: Number of branches (arms) for the STAR topology.
60 :param assign_ips: Automatically assign IP addresses to each node.
61 :type assign_ips: bool
63 :param network: Base network segment for IP address assignment.
66 :param prefix: Base network prefix for IP address assignment.
69 :param version: IP version for IP address assignment.
73 :param assign_st: Select source and target nodes on the graph.
76 NOTE: Only point-to-point like network topologies are supported for now.
77 (Wireless and Ethernet networks were several nodes share the same
78 edge (hyperedge) can not be modeled for the moment).
81 self._graph = kwargs.get("graph")
82 self._topo_type = TopologyType.ADHOC
84 if not self._graph and kwargs.get("topo_type") and \
85 kwargs.get("node_count"):
86 topo_type = kwargs["topo_type"]
87 node_count = kwargs["node_count"]
88 branches = kwargs.get("branches")
90 self._topo_type = topo_type
91 self._graph = self.generate_graph(topo_type, node_count,
94 if kwargs.get("assign_ips"):
95 network = kwargs.get("network", "10.0.0.0")
96 prefix = kwargs.get("prefix", 8)
97 version = kwargs.get("version", 4)
99 self.assign_p2p_ips(self, network = network, prefix = prefix,
102 if kwargs.get("assign_st"):
103 self.select_target_zero()
104 self.select_random_leaf_source()
112 return self._topo_type
116 return self.graph.order()
120 return self.graph.nodes()
124 return self.graph.edges()
126 def generate_graph(self, topo_type, node_count, branches = None):
127 if topo_type == TopologyType.LADDER:
128 total_nodes = node_count/2
129 graph = networkx.ladder_graph(total_nodes)
131 elif topo_type == TopologyType.LINEAR:
132 graph = networkx.path_graph(node_count)
134 elif topo_type == TopologyType.MESH:
135 graph = networkx.complete_graph(node_count)
137 elif topo_type == TopologyType.TREE:
138 h = math.log(node_count + 1)/math.log(2) - 1
139 graph = networkx.balanced_tree(2, h)
141 elif topo_type == TopologyType.STAR:
142 graph = networkx.Graph()
145 nodesinbranch = (node_count - 1)/ BRANCHES
148 for i in xrange(BRANCHES):
150 for n in xrange(1, nodesinbranch + 1):
152 graph.add_edge(prev, c)
156 # node ids are int, make them str
158 g.add_nodes_from(map(lambda nid: str(nid), graph.nodes()))
159 g.add_edges_from(map(lambda t: (str(t[0]), str(t[1])),
164 def add_node(self, nid):
167 if nid not in self.graph:
168 self.graph.add_node(nid)
170 def add_edge(self, nid1, nid2):
177 if nid1 not in self.graph[nid2]:
178 self.graph.add_edge(nid2, nid1)
180 # The weight of the edge is the delay of the link
181 self.graph.edge[nid1][nid2]["weight"] = None
182 # confidence interval of the mean RTT
183 self.graph.edge[nid1][nid2]["weight_ci"] = None
185 def annotate_node_ip(self, nid, ip):
186 if "ips" not in self.graph.node[nid]:
187 self.graph.node[nid]["ips"] = list()
189 self.graph.node[nid]["ips"].append(ip)
191 def annotate_node(self, nid, name, value):
192 self.graph.node[nid][name] = value
194 def node_annotation(self, nid, name):
195 return self.graph.node[nid].get(name)
197 def del_node_annotation(self, nid, name):
198 del self.graph.node[nid][name]
200 def annotate_edge(self, nid1, nid2, name, value):
201 self.graph.edge[nid1][nid2][name] = value
203 def edge_annotation(self, nid1, nid2, name):
204 return self.graph.edge[nid1][nid2].get(name)
206 def del_edge_annotation(self, nid1, nid2, name):
207 del self.graph.edge[nid1][nid2][name]
209 def assign_p2p_ips(self, network = "10.0.0.0", prefix = 8, version = 4):
210 """ Assign IP addresses to each end of each edge of the network graph,
211 computing all the point to point subnets and addresses in the network
214 :param network: Base network address used for subnetting.
217 :param prefix: Prefix for the base network address used for subnetting.
220 :param version: IP version (either 4 or 6).
224 if len(networkx.connected_components(self.graph)) > 1:
225 raise RuntimeError("Disconnected graph!!")
227 # Assign IP addresses to host
228 netblock = "%s/%d" % (network, prefix)
230 net = ipaddr.IPv4Network(netblock)
233 net = ipaddr.IPv6Network(netblock)
236 raise RuntimeError, "Invalid IP version %d" % version
238 ## Clear all previusly assigned IPs
239 for nid in self.graph.node():
240 self.graph.node[nid]["ips"] = list()
242 ## Generate and assign new IPs
243 sub_itr = net.iter_subnets(new_prefix = new_prefix)
245 for nid1, nid2 in self.graph.edges():
246 #### Compute subnets for each link
248 # get a subnet of base_add with prefix /30
249 subnet = sub_itr.next()
250 mask = subnet.netmask.exploded
251 network = subnet.network.exploded
252 prefixlen = subnet.prefixlen
254 # get host addresses in that subnet
255 i = subnet.iterhosts()
261 self.graph.edge[nid1][nid2]["net"] = dict()
262 self.graph.edge[nid1][nid2]["net"][nid1] = ip1
263 self.graph.edge[nid1][nid2]["net"][nid2] = ip2
264 self.graph.edge[nid1][nid2]["net"]["mask"] = mask
265 self.graph.edge[nid1][nid2]["net"]["network"] = mask
266 self.graph.edge[nid1][nid2]["net"]["prefix"] = prefixlen
268 self.annotate_node_ip(nid1, ip1)
269 self.annotate_node_ip(nid2, ip2)
271 def get_p2p_info(self, nid1, nid2):
272 net = self.graph.edge[nid1][nid2]["net"]
273 return ( net[nid1], net[nid2], net["mask"], net["network"],
276 def set_source(self, nid):
277 self.graph.node[nid]["source"] = True
279 def set_target(self, nid):
280 self.graph.node[nid]["target"] = True
283 """ Returns the nodes that are targets """
284 return [nid for nid in self.graph.nodes() \
285 if self.graph.node[nid].get("target")]
288 """ Returns the nodes that are sources """
289 return [nid for nid in self.graph.nodes() \
290 if self.graph.node[nid].get("source")]
292 def select_target_zero(self):
293 """ Marks the node 0 as target
297 def select_random_leaf_source(self):
298 """ Marks a random leaf node as source.
301 # The ladder is a special case because is not symmetric.
302 if self.topo_type == TopologyType.LADDER:
303 total_nodes = self.order/2
304 leaf1 = str(total_nodes - 1)
305 leaf2 = str(nodes - 1)
306 leaves = [leaf1, leaf2]
307 source = leaves.pop(random.randint(0, len(leaves) - 1))
309 # options must not be already sources or targets
310 options = [ k for k,v in self.graph.degree().iteritems() \
311 if v == 1 and not self.graph.node[k].get("source") \
312 and not self.graph.node[k].get("target")]
314 source = options.pop(random.randint(0, len(options) - 1))
316 self.set_source(source)