#
# NEPI, a framework to manage network experiments
# Copyright (C) 2013 INRIA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# Author: Alina Quereilhac
import ipaddr
import networkx
import random
class TopologyType:
LINEAR = "linear"
LADDER = "ladder"
MESH = "mesh"
TREE = "tree"
STAR = "star"
ADHOC = "adhoc"
## TODO:
## - AQ: Add support for hypergraphs (to be able to add hyper edges to
## model CSMA or wireless networks)
class NetGraph(object):
""" NetGraph represents a network topology.
Network graphs are internally using the networkx library.
"""
def __init__(self, *args, **kwargs):
""" A graph can be generated using a specified pattern
(LADDER, MESH, TREE, etc), or provided as an argument.
:param graph: Undirected graph to use as internal representation
:type graph: networkx.Graph
:param topo_type: One of TopologyType.{LINEAR,LADDER,MESH,TREE,STAR}
used to automatically generate the topology graph.
:type topo_type: TopologyType
:param node_count: Number of nodes in the topology to be generated.
:type node_count: int
:param branches: Number of branches (arms) for the STAR topology.
:type branches: int
:param assign_ips: Automatically assign IP addresses to each node.
:type assign_ips: bool
:param network: Base network segment for IP address assignment.
:type network: str
:param prefix: Base network prefix for IP address assignment.
:type prefix: int
:param version: IP version for IP address assignment.
:type version: int
:param assign_st: Select source and target nodes on the graph.
:type assign_st: bool
NOTE: Only point-to-point like network topologies are supported for now.
(Wireless and Ethernet networks were several nodes share the same
edge (hyperedge) can not be modeled for the moment).
"""
self._graph = kwargs.get("graph")
self._topo_type = TopologyType.ADHOC
if not self._graph and kwargs.get("topo_type") and \
kwargs.get("node_count"):
topo_type = kwargs["topo_type"]
node_count = kwargs["node_count"]
branches = kwargs.get("branches")
self._topo_type = topo_type
self._graph = self.generate_grap(topo_type, node_count,
branches = branches)
if kwargs.get("assign_ips"):
network = kwargs.get("network", "10.0.0.0")
prefix = kwargs.get("prefix", 8)
version = kwargs.get("version", 4)
self.assign_p2p_ips(self, network = network, prefix = prefix,
version = version)
if kwargs.get("assign_st"):
self.select_target_zero()
self.select_random_leaf_source()
@property
def graph(self):
return self._graph
@property
def topo_type(self):
return self._topo_type
@property
def order(self):
return self.graph.order()
@property
def nodes(self):
return self.graph.nodes()
@property
def edges(self):
return self.graph.edges()
def generate_graph(self, topo_type, node_count, branches = None):
if topo_type == LADDER:
total_nodes = node_count/2
graph = networkx.ladder_graph(total_nodes)
elif topo_type == LINEAR:
graph = networkx.path_graph(node_count)
elif topo_type == MESH:
graph = networkx.complete_graph(node_count)
elif topo_type == TREE:
h = math.log(node_count + 1)/math.log(2) - 1
graph = networkx.balanced_tree(2, h)
elif topo_type == STAR:
graph = networkx.Graph()
graph.add_node(0)
nodesinbranch = (node_count - 1)/ BRANCHES
c = 1
for i in xrange(BRANCHES):
prev = 0
for n in xrange(1, nodesinbranch + 1):
graph.add_node(c)
graph.add_edge(prev, c)
prev = c
c += 1
# node ids are int, make them str
g = networkx.Graph()
g.add_nodes_from(map(lambda nid: NODES[str(nid)],
graph.nodes()))
g.add_edges_from(map(lambda t: (NODES[str(t[0])], NODES[str(t[1])]),
graph.edges()))
return g
def add_node(self, nid):
nid = str(nid)
if nid not in self.graph:
self.graph.add_node(nid)
def add_edge(self, nid1, nid2):
nid1 = str(nid1)
nid2 = str(nid2)
self.add_node(nid1)
self.add_node( nid2)
if nid1 not in self.graph[nid2]:
self.graph.add_edge(nid2, nid1)
# The weight of the edge is the delay of the link
self.graph.edge[nid1][nid2]["weight"] = None
# confidence interval of the mean RTT
self.graph.edge[nid1][nid2]["weight_ci"] = None
def assign_p2p_ips(self, network = "10.0.0.0", prefix = 8, version = 4):
""" Assign IP addresses to each end of each edge of the network graph,
computing all the point to point subnets and addresses in the network
representation.
:param network: Base network address used for subnetting.
:type network: str
:param prefix: Prefix for the base network address used for subnetting.
:type prefixt: int
:param version: IP version (either 4 or 6).
:type version: int
"""
if len(networkx.connected_components(self.graph)) > 1:
raise RuntimeError("Disconnected graph!!")
# Assign IP addresses to host
netblock = "%s/%d" % (network, prefix)
if version == 4:
net = ipaddr.IPv4Network(netblock)
new_prefix = 31
elif version == 6:
net = ipaddr.IPv6Network(netblock)
new_prefix = 31
else:
raise RuntimeError, "Invalid IP version %d" % version
sub_itr = net.iter_subnets(new_prefix = new_prefix)
for nid1, nid2 in self.graph.edges():
#### Compute subnets for each link
# get a subnet of base_add with prefix /30
subnet = sub_itr.next()
mask = subnet.netmask.exploded
network = subnet.network.exploded
prefixlen = subnet.prefixlen
# get host addresses in that subnet
i = subnet.iterhosts()
addr1 = i.next()
addr2 = i.next()
ip1 = addr1.exploded
ip2 = addr2.exploded
self.graph.edge[nid1][nid2]["net"] = dict()
self.graph.edge[nid1][nid2]["net"][nid1] = ip1
self.graph.edge[nid1][nid2]["net"][nid2] = ip2
self.graph.edge[nid1][nid2]["net"]["mask"] = mask
self.graph.edge[nid1][nid2]["net"]["network"] = mask
self.graph.edge[nid1][nid2]["net"]["prefix"] = prefixlen
def get_p2p_info(self, nid1, nid2):
net = self.graph.edge[nid1][nid2]["net"]
return ( net[nid1], net[nid2], net["mask"], net["network"],
net["prefixlen"] )
def set_source(self, nid):
graph.node[nid]["source"] = True
def set_target(self, nid):
graph.node[nid]["target"] = True
def targets(self):
""" Returns the nodes that are targets """
return [nid for nid in self.graph.nodes() \
if self.graph.node[nid].get("target")]
def sources(self):
""" Returns the nodes that are sources """
return [nid for nid in self.graph.nodes() \
if self.graph.node[nid].get("sources")]
def select_target_zero(self):
""" Marks the node 0 as target
"""
self.set_target("0")
def select_random_leaf_source(self):
""" Marks a random leaf node as source.
"""
# The ladder is a special case because is not symmetric.
if self.topo_type == TopologyType.LADDER:
total_nodes = self.order/2
leaf1 = str(total_nodes - 1)
leaf2 = str(nodes - 1)
leaves = [leaf1, leaf2]
source = leaves.pop(random.randint(0, len(leaves) - 1))
else:
# options must not be already sources or targets
options = [ k for k,v in graph.degree().iteritems() \
if v == 1 and not graph.node[k].get("source") \
and not graph.node[k].get("target")]
source = options.pop(random.randint(0, len(options) - 1))
self.set_source(source)