Note: The [son-push](https://github.com/mpeuster/son-cli) tool can be used instead of CURL.
-### CLI Commands
+### Further Documentation
* [Full CLI command documentation](https://github.com/sonata-nfv/son-emu/wiki/CLI-Command-Overview)
+* [Requirements for Docker containers executed by the emulator](https://github.com/sonata-nfv/son-emu/wiki/Container-Requirements)
## License
self.local_docker_files = dict()
self.remote_docker_image_urls = dict()
self.instances = dict()
- self.vnfname2num = dict()
+ self.vnf_name2docker_name = dict()
+ # lets generate a set of subnet configurations used for e-line chaining setup
+ self.eline_subnets_src = generate_subnet_strings(50, start=200, subnet_size=24, ip=1)
+ self.eline_subnets_dst = generate_subnet_strings(50, start=200, subnet_size=24, ip=2)
def onboard(self):
"""
self.instances[instance_uuid]["vnf_instances"].append(vnfi)
# 3. Configure the chaining of the network functions (currently only E-Line links supported)
- nfid2name = defaultdict(lambda :"NotExistingNode",
- reduce(lambda x,y: dict(x, **y),
- map(lambda d:{d["vnf_id"]:d["vnf_name"]},
+ vnf_id2vnf_name = defaultdict(lambda: "NotExistingNode",
+ reduce(lambda x, y: dict(x, **y),
+ map(lambda d: {d["vnf_id"]: d["vnf_name"]},
self.nsd["network_functions"])))
vlinks = self.nsd["virtual_links"]
fwd_links = self.nsd["forwarding_graphs"][0]["constituent_virtual_links"]
eline_fwd_links = [l for l in vlinks if (l["id"] in fwd_links) and (l["connectivity_type"] == "E-Line")]
- cookie = 1 # not clear why this is needed - to check with Steven
+ cookie = 1 # not clear why this is needed - to check with Steven
for link in eline_fwd_links:
- src_node, src_port = link["connection_points_reference"][0].split(":")
- dst_node, dst_port = link["connection_points_reference"][1].split(":")
-
- srcname = nfid2name[src_node]
- dstname = nfid2name[dst_node]
- LOG.debug("src name: "+srcname+" dst name: "+dstname)
-
- if (srcname in self.vnfds) and (dstname in self.vnfds) :
- network = self.vnfds[srcname].get("dc").net # there should be a cleaner way to find the DCNetwork
- src_vnf = self.vnfname2num[srcname]
- dst_vnf = self.vnfname2num[dstname]
- ret = network.setChain(src_vnf, dst_vnf, vnf_src_interface=src_port, vnf_dst_interface=dst_port, bidirectional = True, cmd="add-flow", cookie = cookie)
+ src_id, src_if_name = link["connection_points_reference"][0].split(":")
+ dst_id, dst_if_name = link["connection_points_reference"][1].split(":")
+
+ src_name = vnf_id2vnf_name[src_id]
+ dst_name = vnf_id2vnf_name[dst_id]
+
+ LOG.debug(
+ "Setting up E-Line link. %s(%s:%s) -> %s(%s:%s)" % (
+ src_name, src_id, src_if_name, dst_name, dst_id, dst_if_name))
+
+ if (src_name in self.vnfds) and (dst_name in self.vnfds):
+ network = self.vnfds[src_name].get("dc").net # there should be a cleaner way to find the DCNetwork
+ src_docker_name = self.vnf_name2docker_name[src_name]
+ dst_docker_name = self.vnf_name2docker_name[dst_name]
+ LOG.debug(src_docker_name)
+ ret = network.setChain(
+ src_docker_name, dst_docker_name,
+ vnf_src_interface=src_if_name, vnf_dst_interface=dst_if_name,
+ bidirectional=True, cmd="add-flow", cookie=cookie)
cookie += 1
+ # re-configure the VNFs IP assignment and ensure that a new subnet is used for each E-Link
+ src_vnfi = self._get_vnf_instance(instance_uuid, src_name)
+ if src_vnfi is not None:
+ self._vnf_reconfigure_network(src_vnfi, src_if_name, self.eline_subnets_src.pop(0))
+ dst_vnfi = self._get_vnf_instance(instance_uuid, dst_name)
+ if dst_vnfi is not None:
+ self._vnf_reconfigure_network(dst_vnfi, dst_if_name, self.eline_subnets_dst.pop(0))
+
# 4. run the emulator specific entrypoint scripts in the VNFIs of this service instance
self._trigger_emulator_start_scripts_in_vnfis(self.instances[instance_uuid]["vnf_instances"])
# 3. do the dc.startCompute(name="foobar") call to run the container
# TODO consider flavors, and other annotations
intfs = vnfd.get("connection_points")
- self.vnfname2num[vnf_name] = GK.get_next_vnf_name()
- LOG.info("VNF "+vnf_name+" mapped to "+self.vnfname2num[vnf_name]+" on dc "+str(vnfd.get("dc")))
- vnfi = target_dc.startCompute(self.vnfname2num[vnf_name], network=intfs, image=docker_name, flavor_name="small")
+ self.vnf_name2docker_name[vnf_name] = GK.get_next_vnf_name()
+ LOG.info("Starting %r as %r in DC %r" % (vnf_name, self.vnf_name2docker_name[vnf_name], vnfd.get("dc")))
+ LOG.debug("Interfaces for %r: %r" % (vnf_name, intfs))
+ vnfi = target_dc.startCompute(self.vnf_name2docker_name[vnf_name], network=intfs, image=docker_name, flavor_name="small")
return vnfi
+ def _get_vnf_instance(self, instance_uuid, name):
+ """
+ Returns the Docker object for the given VNF name (or Docker name).
+ :param instance_uuid: UUID of the service instance to search in.
+ :param name: VNF name or Docker name. We are fuzzy here.
+ :return:
+ """
+ dn = name
+ if name in self.vnf_name2docker_name:
+ dn = self.vnf_name2docker_name[name]
+ for vnfi in self.instances[instance_uuid]["vnf_instances"]:
+ if vnfi.name == dn:
+ return vnfi
+ LOG.warning("No container with name: %r found.")
+ return None
+
+ @staticmethod
+ def _vnf_reconfigure_network(vnfi, if_name, net_str):
+ """
+ Reconfigure the network configuration of a specific interface
+ of a running container.
+ :param vnfi: container instacne
+ :param if_name: interface name
+ :param net_str: network configuration string, e.g., 1.2.3.4/24
+ :return:
+ """
+ intf = vnfi.intf(intf=if_name)
+ if intf is not None:
+ intf.setIP(net_str)
+ LOG.debug("Reconfigured network of %s:%s to %r" % (vnfi.name, if_name, net_str))
+ else:
+ LOG.warning("Interface not found: %s:%s. Network reconfiguration skipped." % (vnfi.name, if_name))
+
+
def _trigger_emulator_start_scripts_in_vnfis(self, vnfi_list):
for vnfi in vnfi_list:
config = vnfi.dcinfo.get("Config", dict())
Return a list of UUID's of uploaded service packages.
:return: dict/list
"""
+ LOG.info("GET /packages")
return {"service_uuid_list": list(GK.services.iterkeys())}
Returns a list of UUIDs containing all running services.
:return: dict / list
"""
- return {"service_instance_list": [
+ LOG.info("GET /instantiations")
+ return {"service_instantiations_list": [
list(s.instances.iterkeys()) for s in GK.services.itervalues()]}
return path
+def generate_subnet_strings(n, start=1, subnet_size=24, ip=0):
+ """
+ Helper to generate different network configuration strings.
+ """
+ r = list()
+ for i in range(start, start + n):
+ r.append("%d.0.0.%d/%d" % (i, ip, subnet_size))
+ return r
+
+
if __name__ == '__main__':
"""
Lets allow to run the API in standalone mode.
"--match", "-m", dest="match",
help="string holding extra matches for the flow entries")
parser.add_argument(
- "--bidirectional", "-b", dest="bidirectional",
+ "--bidirectional", "-b", dest="bidirectional", action='store_true',
help="add/remove the flow entries from src to dst and back")
parser.add_argument(
"--cookie", "-c", dest="cookie",
from mininet.node import Controller, DefaultController, OVSSwitch, OVSKernelSwitch, Docker, RemoteController
from mininet.cli import CLI
from mininet.link import TCLink
+from mininet.clean import cleanup
import networkx as nx
from emuvim.dcemulator.monitoring import DCNetworkMonitor
from emuvim.dcemulator.node import Datacenter, EmulatorCompute
from emuvim.dcemulator.resourcemodel import ResourceModelRegistrar
+LOG = logging.getLogger("dcemulator.net")
+LOG.setLevel(logging.DEBUG)
+
class DCNetwork(Containernet):
"""
Wraps the original Mininet/Containernet class and provides
:param kwargs: path through for Mininet parameters
:return:
"""
+ # members
self.dcs = {}
+ self.ryu_process = None
- # make sure any remaining Ryu processes are killed
+ # always cleanup environment before we start the emulator
self.killRyu()
- # make sure no containers are left over from a previous emulator run.
- self.removeLeftoverContainers()
+ cleanup()
# call original Docker.__init__ and setup default controller
Containernet.__init__(
self, switch=OVSKernelSwitch, controller=controller, **kwargs)
-
# Ryu management
- self.ryu_process = None
if controller == RemoteController:
# start Ryu controller
self.startRyu(learning_switch=enable_learning)
dc.net = self # set reference to network
self.dcs[label] = dc
dc.create() # finally create the data center in our Mininet instance
- logging.info("added data center: %s" % label)
+ LOG.info("added data center: %s" % label)
return dc
def addLink(self, node1, node2, **params):
"""
assert node1 is not None
assert node2 is not None
- logging.debug("addLink: n1=%s n2=%s" % (str(node1), str(node2)))
+ LOG.debug("addLink: n1=%s n2=%s" % (str(node1), str(node2)))
# ensure type of node1
if isinstance( node1, basestring ):
if node1 in self.dcs:
Containernet.stop(self)
# stop Ryu controller
- self.stopRyu()
+ self.killRyu()
def CLI(self):
def _chainAddFlow(self, vnf_src_name, vnf_dst_name, vnf_src_interface=None, vnf_dst_interface=None, **kwargs):
+ src_sw = None
+ dst_sw = None
+ src_sw_inport_nr = 0
+ dst_sw_outport_nr = 0
+
+ LOG.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
+ vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface)
+
#check if port is specified (vnf:port)
if vnf_src_interface is None:
# take first interface by default
for connected_sw in self.DCNetwork_graph.neighbors(vnf_src_name):
link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
for link in link_dict:
- if link_dict[link]['src_port_id'] == vnf_src_interface:
+ if (link_dict[link]['src_port_id'] == vnf_src_interface or
+ link_dict[link]['src_port_name'] == vnf_src_interface): # Fix: we might also get interface names, e.g, from a son-emu-cli call
# found the right link and connected switch
src_sw = connected_sw
-
src_sw_inport_nr = link_dict[link]['dst_port_nr']
break
for connected_sw in self.DCNetwork_graph.neighbors(vnf_dst_name):
link_dict = self.DCNetwork_graph[connected_sw][vnf_dst_name]
for link in link_dict:
- if link_dict[link]['dst_port_id'] == vnf_dst_interface:
+ if link_dict[link]['dst_port_id'] == vnf_dst_interface or \
+ link_dict[link]['dst_port_name'] == vnf_dst_interface: # Fix: we might also get interface names, e.g, from a son-emu-cli call
# found the right link and connected switch
dst_sw = connected_sw
dst_sw_outport_nr = link_dict[link]['src_port_nr']
# if all shortest paths are wanted, use: all_shortest_paths
path = nx.shortest_path(self.DCNetwork_graph, src_sw, dst_sw, weight=kwargs.get('weight'))
except:
- logging.info("No path could be found between {0} and {1}".format(vnf_src_name, vnf_dst_name))
+ LOG.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
+ vnf_src_name, vnf_dst_name, src_sw, dst_sw))
+ LOG.debug("Graph nodes: %r" % self.DCNetwork_graph.nodes())
+ LOG.debug("Graph edges: %r" % self.DCNetwork_graph.edges())
+ for e, v in self.DCNetwork_graph.edges():
+ LOG.debug("%r" % self.DCNetwork_graph[e][v])
return "No path could be found between {0} and {1}".format(vnf_src_name, vnf_dst_name)
- logging.info("Path between {0} and {1}: {2}".format(vnf_src_name, vnf_dst_name, path))
+ LOG.info("Path between {0} and {1}: {2}".format(vnf_src_name, vnf_dst_name, path))
current_hop = src_sw
switch_inport_nr = src_sw_inport_nr
if next_hop == vnf_dst_name:
switch_outport_nr = dst_sw_outport_nr
- logging.info("end node reached: {0}".format(vnf_dst_name))
+ LOG.info("end node reached: {0}".format(vnf_dst_name))
elif not isinstance( next_node, OVSSwitch ):
- logging.info("Next node: {0} is not a switch".format(next_hop))
+ LOG.info("Next node: {0} is not a switch".format(next_hop))
return "Next node: {0} is not a switch".format(next_hop)
else:
# take first link between switches by default
ofcmd = ''
node.dpctl(cmd, ofcmd)
- logging.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node.name, switch_inport_nr,
+ LOG.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node.name, switch_inport_nr,
switch_outport_nr, cmd))
# start Ryu Openflow controller as Remote Controller for the DCNetwork
self.ryu_process = Popen([ryu_cmd, ryu_path2, ryu_option, ryu_of_port], stdout=FNULL, stderr=FNULL)
time.sleep(1)
- def stopRyu(self):
+ def killRyu(self):
+ """
+ Stop the Ryu controller that might be started by son-emu.
+ :return:
+ """
+ # try it nicely
if self.ryu_process is not None:
self.ryu_process.terminate()
self.ryu_process.kill()
- self.killRyu()
-
- @staticmethod
- def removeLeftoverContainers():
- # TODO can be more python-based using eg. docker-py?
- Popen('docker ps -a -q --filter="name=mn.*" | xargs -r docker rm -f', shell=True)
-
- @staticmethod
- def killRyu():
+ # ensure its death ;-)
Popen(['pkill', '-f', 'ryu-manager'])
def ryu_REST(self, prefix, dpid=None, data=None):
else:
url = self.ryu_REST_api + '/' + str(prefix)
if data:
- #logging.info('POST: {0}'.format(str(data)))
+ #LOG.info('POST: {0}'.format(str(data)))
req = urllib2.Request(url, str(data))
else:
req = urllib2.Request(url)
ret = urllib2.urlopen(req).read()
return ret
except:
- logging.info('error url: {0}'.format(str(url)))
- if data: logging.info('error POST: {0}'.format(str(data)))
+ LOG.info('error url: {0}'.format(str(url)))
+ if data: LOG.info('error POST: {0}'.format(str(data)))
# need to respect that some match fields must be integers
# http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
import time
import json
-LOG = logging.getLogger("dcemulator")
+LOG = logging.getLogger("dcemulator.node")
LOG.setLevel(logging.DEBUG)
# if no --net option is given, network = [{}], so 1 empty dict in the list
# this results in 1 default interface with a default ip address
for nw in network:
+ # clean up network configuration (e.g. RTNETLINK does not allow ':' in intf names
+ if nw.get("id") is not None:
+ nw["id"] = self._clean_ifname(nw["id"])
# TODO we cannot use TCLink here (see: https://github.com/mpeuster/containernet/issues/3)
self.net.addLink(d, self.switch, params1=nw, cls=Link, intfName1=nw.get('id'))
# do bookkeeping
self.net.rm_registrar.register(self, rm)
LOG.info("Assigned RM: %r to DC: %r" % (rm, self))
+ @staticmethod
+ def _clean_ifname(name):
+ """
+ Cleans up given string to be a
+ RTNETLINK compatible interface name.
+ :param name: string
+ :return: string
+ """
+ if name is None:
+ return "if0"
+ name = name.replace(":", "-")
+ name = name.replace(" ", "-")
+ name = name.replace(".", "-")
+ name = name.replace("_", "-")
+ return name
+
def create_topology1():
# create topology
- net = DCNetwork(controller=RemoteController, monitor=False, enable_learning = False)
+ net = DCNetwork(controller=RemoteController, monitor=False, enable_learning=False)
dc1 = net.addDatacenter("dc1")
dc2 = net.addDatacenter("dc2")
s1 = net.addSwitch("s1")
# add the command line interface endpoint to each DC (REST API)
rapi1 = RestApiEndpoint("0.0.0.0", 5001)
+ rapi1.connectDCNetwork(net)
rapi1.connectDatacenter(dc1)
rapi1.connectDatacenter(dc2)
# run API endpoint server (in another thread, don't block)