"""
+Copyright (c) 2015 SONATA-NFV and Paderborn University
+ALL RIGHTS RESERVED.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+Neither the name of the SONATA-NFV [, ANY ADDITIONAL AFFILIATION]
+nor the names of its contributors may be used to endorse or promote
+products derived from this software without specific prior written
+permission.
+
+This work has been performed in the framework of the SONATA project,
+funded by the European Commission under Grant number 671517 through
+the Horizon 2020 and 5G-PPP programmes. The authors would like to
+acknowledge the contributions of their colleagues of the SONATA
+partner consortium (www.sonata-nfv.eu).
+"""
+"""
This module implements a simple REST API that behaves like SONATA's gatekeeper.
It is only used to support the development of SONATA's SDK tools and to demonstrate
import hashlib
import zipfile
import yaml
+import threading
from docker import Client as DockerClient
from flask import Flask, request
import flask_restful as fr
from collections import defaultdict
+import pkg_resources
logging.basicConfig()
LOG = logging.getLogger("sonata-dummy-gatekeeper")
# should a new version of an image be pulled even if its available
FORCE_PULL = False
+# Automatically deploy SAPs (endpoints) of the service as new containers
+# Attention: This is not a configuration switch but a global variable! Don't change its default value.
+DEPLOY_SAP = False
+
class Gatekeeper(object):
def __init__(self):
self.local_docker_files = dict()
self.remote_docker_image_urls = dict()
self.instances = dict()
- self.vnfname2num = dict()
+ self.vnf_name2docker_name = dict()
+ self.sap_identifiers = set()
+ # 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._load_package_descriptor()
self._load_nsd()
self._load_vnfd()
+ if DEPLOY_SAP:
+ self._load_saps()
# 3. prepare container images (e.g. download or build Dockerfile)
if BUILD_DOCKERFILE:
self._load_docker_files()
else:
self._load_docker_urls()
self._pull_predefined_dockerimages()
- LOG.info("On-boarded service: %r" % self.manifest.get("package_name"))
+ LOG.info("On-boarded service: %r" % self.manifest.get("name"))
def start_service(self):
"""
self.instances[instance_uuid] = dict()
self.instances[instance_uuid]["vnf_instances"] = list()
- # 2. compute placement of this service instance (adds DC names to VNFDs)
+ # 2. Configure the chaining of the network functions (currently only E-Line and E-LAN links supported)
+ 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"])))
+
+ # 3. compute placement of this service instance (adds DC names to VNFDs)
if not GK_STANDALONE_MODE:
- self._calculate_placement(FirstDcPlacement)
+ #self._calculate_placement(FirstDcPlacement)
+ self._calculate_placement(RoundRobinDcPlacement)
# iterate over all vnfds that we have to start
for vnfd in self.vnfds.itervalues():
vnfi = None
vnfi = self._start_vnfd(vnfd)
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"]},
- 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")]
+ elan_fwd_links = [l for l in vlinks if (l["id"] in fwd_links) and (l["connectivity_type"] == "E-LAN")]
- cookie = 1 # not clear why this is needed - to check with Steven
+ # 4a. deploy E-Line links
+ # cookie is used as identifier for the flowrules installed by the dummygatekeeper
+ # eg. different services get a unique cookie for their flowrules
+ cookie = 1
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(":")
+ src_id, src_if_name = link["connection_points_reference"][0].split(":")
+ dst_id, dst_if_name = link["connection_points_reference"][1].split(":")
- srcname = nfid2name[src_node]
- dstname = nfid2name[dst_node]
- LOG.debug("src name: "+srcname+" dst name: "+dstname)
+ # check if there is a SAP in the link
+ if src_id in self.sap_identifiers:
+ src_docker_name = "{0}_{1}".format(src_id, src_if_name)
+ src_id = src_docker_name
+ else:
+ src_docker_name = src_id
- 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)
- cookie += 1
+ if dst_id in self.sap_identifiers:
+ dst_docker_name = "{0}_{1}".format(dst_id, dst_if_name)
+ dst_id = dst_docker_name
+ else:
+ dst_docker_name = dst_id
+
+ 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
+ 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, priority=10)
+
+ # 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))
+
+ # 4b. deploy E-LAN links
+ base = 10
+ for link in elan_fwd_links:
+ # generate lan ip address
+ ip = 1
+ for intf in link["connection_points_reference"]:
+ ip_address = generate_lan_string("10.0", base, subnet_size=24, ip=ip)
+ vnf_id, intf_name = intf.split(":")
+ if vnf_id in self.sap_identifiers:
+ src_docker_name = "{0}_{1}".format(vnf_id, intf_name)
+ vnf_id = src_docker_name
+ vnf_name = vnf_id2vnf_name[vnf_id]
+ LOG.debug(
+ "Setting up E-LAN link. %s(%s:%s) -> %s" % (
+ vnf_name, vnf_id, intf_name, ip_address))
+
+ if vnf_name in self.vnfds:
+ # re-configure the VNFs IP assignment and ensure that a new subnet is used for each E-LAN
+ # E-LAN relies on the learning switch capability of Ryu which has to be turned on in the topology
+ # (DCNetwork(controller=RemoteController, enable_learning=True)), so no explicit chaining is necessary.
+ vnfi = self._get_vnf_instance(instance_uuid, vnf_name)
+ if vnfi is not None:
+ self._vnf_reconfigure_network(vnfi, intf_name, ip_address)
+ # increase for the next ip address on this E-LAN
+ ip += 1
+ # increase the base ip address for the next E-LAN
+ base += 1
+
+ # 5. 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"])
LOG.info("Service started. Instance id: %r" % instance_uuid)
return instance_uuid
# 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")
- # 6. store references to the compute objects in self.instances
+
+ # TODO: get all vnf id's from the nsd for this vnfd and use those as dockername
+ # use the vnf_id in the nsd as docker name
+ # so deployed containers can be easily mapped back to the nsd
+ vnf_name2id = defaultdict(lambda: "NotExistingNode",
+ reduce(lambda x, y: dict(x, **y),
+ map(lambda d: {d["vnf_name"]: d["vnf_id"]},
+ self.nsd["network_functions"])))
+ self.vnf_name2docker_name[vnf_name] = vnf_name2id[vnf_name]
+ # 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())
+ env = config.get("Env", list())
+ for env_var in env:
+ if "SON_EMU_CMD=" in env_var:
+ cmd = str(env_var.split("=")[1])
+ LOG.info("Executing entry point script in %r: %r" % (vnfi.name, cmd))
+ # execute command in new thread to ensure that GK is not blocked by VNF
+ t = threading.Thread(target=vnfi.cmdPrint, args=(cmd,))
+ t.daemon = True
+ t.start()
+
def _unpack_service_package(self):
"""
unzip *.son file and store contents in CATALOG_FOLDER/services/<service_uuid>/
self.vnfds[vnfd.get("name")] = vnfd
LOG.debug("Loaded VNFD: %r" % vnfd.get("name"))
+ def _load_saps(self):
+ # Each Service Access Point (connection_point) in the nsd is getting its own container
+ SAPs = [p["id"] for p in self.nsd["connection_points"] if p["type"] == "interface"]
+ for sap in SAPs:
+ # endpoints needed in this service
+ sap_vnf_id, sap_vnf_interface = sap.split(':')
+ # set of the connection_point ids found in the nsd (in the examples this is 'ns')
+ self.sap_identifiers.add(sap_vnf_id)
+
+ sap_docker_name = sap.replace(':', '_')
+
+ # add SAP to self.vnfds
+ sapfile = pkg_resources.resource_filename(__name__, "sap_vnfd.yml")
+ sap_vnfd = load_yaml(sapfile)
+ sap_vnfd["connection_points"][0]["id"] = sap_vnf_interface
+ sap_vnfd["name"] = sap_docker_name
+ self.vnfds[sap_docker_name] = sap_vnfd
+ # add SAP vnf to list in the NSD so it is deployed later on
+ # each SAP get a unique VNFD and vnf_id in the NSD
+ self.nsd["network_functions"].append({"vnf_id": sap_docker_name, "vnf_name": sap_docker_name})
+ LOG.debug("Loaded SAP: %r" % sap_vnfd.get("name"))
+
def _load_docker_files(self):
"""
Get all paths to Dockerfiles from VNFDs and store them in dict.
vnfd["dc"] = list(dcs.itervalues())[0]
+class RoundRobinDcPlacement(object):
+ """
+ Placement: Distribute VNFs across all available DCs in a round robin fashion.
+ """
+ def place(self, nsd, vnfds, dcs):
+ c = 0
+ dcs_list = list(dcs.itervalues())
+ for name, vnfd in vnfds.iteritems():
+ vnfd["dc"] = dcs_list[c % len(dcs_list)]
+ c += 1 # inc. c to use next DC
+
+
+
+
"""
Resource definitions and API endpoints
"""
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_lan_string(prefix, base, subnet_size=24, ip=0):
+ """
+ Helper to generate different network configuration strings.
+ """
+ r = "%s.%d.%d/%d" % (prefix, base, ip, subnet_size)
+ return r
+
+
+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.