1 # Copyright (c) 2015 SONATA-NFV and Paderborn University
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
16 # Neither the name of the SONATA-NFV, Paderborn University
17 # nor the names of its contributors may be used to endorse or promote
18 # products derived from this software without specific prior written
21 # This work has been performed in the framework of the SONATA project,
22 # funded by the European Commission under Grant number 671517 through
23 # the Horizon 2020 and 5G-PPP programmes. The authors would like to
24 # acknowledge the contributions of their colleagues of the SONATA
25 # partner consortium (www.sonata-nfv.eu).
33 from docker
import DockerClient
34 from flask
import Flask
, request
35 import flask_restful
as fr
36 from collections
import defaultdict
38 from subprocess
import Popen
39 from random
import randint
43 from functools
import reduce
46 LOG
= logging
.getLogger("sonata-dummy-gatekeeper")
47 LOG
.setLevel(logging
.DEBUG
)
48 logging
.getLogger("werkzeug").setLevel(logging
.WARNING
)
50 GK_STORAGE
= "/tmp/son-dummy-gk/"
51 UPLOAD_FOLDER
= os
.path
.join(GK_STORAGE
, "uploads/")
52 CATALOG_FOLDER
= os
.path
.join(GK_STORAGE
, "catalog/")
54 # Enable Dockerfile build functionality
55 BUILD_DOCKERFILE
= False
57 # flag to indicate that we run without the emulator (only the bare API for
58 # integration testing)
59 GK_STANDALONE_MODE
= False
61 # should a new version of an image be pulled even if its available
64 # Automatically deploy SAPs (endpoints) of the service as new containers
65 # Attention: This is not a configuration switch but a global variable!
66 # Don't change its default value.
69 # flag to indicate if we use bidirectional forwarding rules in the
70 # automatic chaining process
71 BIDIRECTIONAL_CHAIN
= False
73 # override the management interfaces in the descriptors with default
74 # docker0 interfaces in the containers
75 USE_DOCKER_MGMT
= False
77 # automatically deploy uploaded packages (no need to execute son-access
78 # deploy --latest separately)
81 # and also automatically terminate any other running services
85 def generate_subnets(prefix
, base
, subnet_size
=50, mask
=24):
86 # Generate a list of ipaddress in subnets
88 for net
in range(base
, base
+ subnet_size
):
89 subnet
= "{0}.{1}.0/{2}".format(prefix
, net
, mask
)
90 r
.append(ipaddress
.ip_network(unicode(subnet
)))
94 # private subnet definitions for the generated interfaces
96 SAP_SUBNETS
= generate_subnets('10.10', 0, subnet_size
=50, mask
=30)
98 ELAN_SUBNETS
= generate_subnets('10.20', 0, subnet_size
=50, mask
=24)
100 ELINE_SUBNETS
= generate_subnets('10.30', 0, subnet_size
=50, mask
=30)
102 # path to the VNFD for the SAP VNF that is deployed as internal SAP point
105 # Time in seconds to wait for vnf stop scripts to execute fully
106 VNF_STOP_WAIT_TIME
= 5
109 class Gatekeeper(object):
112 self
.services
= dict()
115 # used to generate short names for VNFs (Mininet limitation)
117 LOG
.info("Create SONATA dummy gatekeeper.")
119 def register_service_package(self
, service_uuid
, service
):
121 register new service package
123 :param service object
125 self
.services
[service_uuid
] = service
126 # lets perform all steps needed to onboard the service
129 def get_next_vnf_name(self
):
130 self
.vnf_counter
+= 1
131 return "vnf%d" % self
.vnf_counter
134 class Service(object):
136 This class represents a NS uploaded as a *.son package to the
138 Can have multiple running instances of this service.
145 self
.uuid
= service_uuid
146 self
.package_file_hash
= package_file_hash
147 self
.package_file_path
= package_file_path
148 self
.package_content_path
= os
.path
.join(
149 CATALOG_FOLDER
, "services/%s" % self
.uuid
)
154 self
.saps_ext
= list()
155 self
.saps_int
= list()
156 self
.local_docker_files
= dict()
157 self
.remote_docker_image_urls
= dict()
158 self
.instances
= dict()
159 # dict to find the vnf_name for any vnf id
160 self
.vnf_id2vnf_name
= dict()
164 Do all steps to prepare this service to be instantiated
167 # 1. extract the contents of the package and store them in our catalog
168 self
._unpack
_service
_package
()
169 # 2. read in all descriptor files
170 self
._load
_package
_descriptor
()
175 # 3. prepare container images (e.g. download or build Dockerfile)
177 self
._load
_docker
_files
()
178 self
._build
_images
_from
_dockerfiles
()
180 self
._load
_docker
_urls
()
181 self
._pull
_predefined
_dockerimages
()
182 LOG
.info("On-boarded service: %r" % self
.manifest
.get("name"))
184 def start_service(self
):
186 This methods creates and starts a new service instance.
187 It computes placements, iterates over all VNFDs, and starts
188 each VNFD as a Docker container in the data center selected
189 by the placement algorithm.
192 LOG
.info("Starting service %r" % self
.uuid
)
194 # 1. each service instance gets a new uuid to identify it
195 instance_uuid
= str(uuid
.uuid4())
196 # build a instances dict (a bit like a NSR :))
197 self
.instances
[instance_uuid
] = dict()
198 self
.instances
[instance_uuid
]["vnf_instances"] = list()
200 # 2. compute placement of this service instance (adds DC names to
202 if not GK_STANDALONE_MODE
:
203 # self._calculate_placement(FirstDcPlacement)
204 self
._calculate
_placement
(RoundRobinDcPlacementWithSAPs
)
205 # 3. start all vnfds that we have in the service (except SAPs)
206 for vnf_id
in self
.vnfds
:
207 vnfd
= self
.vnfds
[vnf_id
]
209 if not GK_STANDALONE_MODE
:
210 vnfi
= self
._start
_vnfd
(vnfd
, vnf_id
)
211 self
.instances
[instance_uuid
]["vnf_instances"].append(vnfi
)
213 # 4. start all SAPs in the service
214 for sap
in self
.saps
:
215 self
._start
_sap
(self
.saps
[sap
], instance_uuid
)
217 # 5. Deploy E-Line and E_LAN links
218 # Attention: Only done if ""forwarding_graphs" section in NSD exists,
219 # even if "forwarding_graphs" are not used directly.
220 if "virtual_links" in self
.nsd
and "forwarding_graphs" in self
.nsd
:
221 vlinks
= self
.nsd
["virtual_links"]
222 # constituent virtual links are not checked
223 # fwd_links = self.nsd["forwarding_graphs"][0]["constituent_virtual_links"]
224 eline_fwd_links
= [l
for l
in vlinks
if (
225 l
["connectivity_type"] == "E-Line")]
226 elan_fwd_links
= [l
for l
in vlinks
if (
227 l
["connectivity_type"] == "E-LAN")]
229 GK
.net
.deployed_elines
.extend(eline_fwd_links
)
230 GK
.net
.deployed_elans
.extend(elan_fwd_links
)
232 # 5a. deploy E-Line links
233 self
._connect
_elines
(eline_fwd_links
, instance_uuid
)
235 # 5b. deploy E-LAN links
236 self
._connect
_elans
(elan_fwd_links
, instance_uuid
)
238 # 6. run the emulator specific entrypoint scripts in the VNFIs of this
240 self
._trigger
_emulator
_start
_scripts
_in
_vnfis
(
241 self
.instances
[instance_uuid
]["vnf_instances"])
243 LOG
.info("Service started. Instance id: %r" % instance_uuid
)
246 def stop_service(self
, instance_uuid
):
248 This method stops a running service instance.
249 It iterates over all VNF instances, stopping them each
250 and removing them from their data center.
252 :param instance_uuid: the uuid of the service instance to be stopped
254 LOG
.info("Stopping service %r" % self
.uuid
)
255 # get relevant information
256 # instance_uuid = str(self.uuid.uuid4())
257 vnf_instances
= self
.instances
[instance_uuid
]["vnf_instances"]
259 # trigger stop skripts in vnf instances and wait a few seconds for
261 self
._trigger
_emulator
_stop
_scripts
_in
_vnfis
(vnf_instances
)
262 time
.sleep(VNF_STOP_WAIT_TIME
)
264 for v
in vnf_instances
:
267 for sap_name
in self
.saps_ext
:
268 ext_sap
= self
.saps
[sap_name
]
269 target_dc
= ext_sap
.get("dc")
270 target_dc
.removeExternalSAP(sap_name
)
271 LOG
.info("Stopping the SAP instance: %r in DC %r" %
272 (sap_name
, target_dc
))
274 if not GK_STANDALONE_MODE
:
276 # self._remove_placement(RoundRobinPlacement)
279 # last step: remove the instance from the list of all instances
280 del self
.instances
[instance_uuid
]
282 def _start_vnfd(self
, vnfd
, vnf_id
, **kwargs
):
284 Start a single VNFD of this service
285 :param vnfd: vnfd descriptor dict
286 :param vnf_id: unique id of this vnf in the nsd
289 # the vnf_name refers to the container image to be deployed
290 vnf_name
= vnfd
.get("name")
292 # iterate over all deployment units within each VNFDs
293 for u
in vnfd
.get("virtual_deployment_units"):
294 # 1. get the name of the docker image to start and the assigned DC
295 if vnf_id
not in self
.remote_docker_image_urls
:
296 raise Exception("No image name for %r found. Abort." % vnf_id
)
297 docker_name
= self
.remote_docker_image_urls
.get(vnf_id
)
298 target_dc
= vnfd
.get("dc")
299 # 2. perform some checks to ensure we can start the container
300 assert(docker_name
is not None)
301 assert(target_dc
is not None)
302 if not self
._check
_docker
_image
_exists
(docker_name
):
304 "Docker image %r not found. Abort." % docker_name
)
306 # 3. get the resource limits
307 res_req
= u
.get("resource_requirements")
308 cpu_list
= res_req
.get("cpu").get("cores")
310 cpu_list
= res_req
.get("cpu").get("vcpus")
313 cpu_bw
= res_req
.get("cpu").get("cpu_bw")
316 mem_num
= str(res_req
.get("memory").get("size"))
317 if len(mem_num
) == 0:
319 mem_unit
= str(res_req
.get("memory").get("size_unit"))
320 if str(mem_unit
) == 0:
322 mem_limit
= float(mem_num
)
324 mem_limit
= mem_limit
* 1024 * 1024 * 1024
325 elif mem_unit
== "MB":
326 mem_limit
= mem_limit
* 1024 * 1024
327 elif mem_unit
== "KB":
328 mem_limit
= mem_limit
* 1024
329 mem_lim
= int(mem_limit
)
330 cpu_period
, cpu_quota
= self
._calculate
_cpu
_cfs
_values
(
333 # check if we need to deploy the management ports (defined as
334 # type:management both on in the vnfd and nsd)
335 intfs
= vnfd
.get("connection_points", [])
338 mgmt_intfs
= [vnf_id
+ ':' + intf
['id']
339 for intf
in intfs
if intf
.get('type') == 'management']
340 # check if any of these management interfaces are used in a
341 # management-type network in the nsd
342 for nsd_intf_name
in mgmt_intfs
:
343 vlinks
= [l
["connection_points_reference"]
344 for l
in self
.nsd
.get("virtual_links", [])]
346 if nsd_intf_name
in link
and self
.check_mgmt_interface(
348 # this is indeed a management interface and can be
350 vnf_id
, vnf_interface
, vnf_sap_docker_name
= parse_interface(
353 intf
for intf
in intfs
if intf
.get('id') == vnf_interface
]
354 intfs
.remove(found_interfaces
[0])
355 mgmt_intf_names
.append(vnf_interface
)
357 # 4. generate the volume paths for the docker container
359 # a volume to extract log files
360 docker_log_path
= "/tmp/results/%s/%s" % (self
.uuid
, vnf_id
)
361 LOG
.debug("LOG path for vnf %s is %s." % (vnf_id
, docker_log_path
))
362 if not os
.path
.exists(docker_log_path
):
363 LOG
.debug("Creating folder %s" % docker_log_path
)
364 os
.makedirs(docker_log_path
)
366 volumes
.append(docker_log_path
+ ":/mnt/share/")
368 # 5. do the dc.startCompute(name="foobar") call to run the container
369 # TODO consider flavors, and other annotations
370 # TODO: get all vnf id's from the nsd for this vnfd and use those as dockername
371 # use the vnf_id in the nsd as docker name
372 # so deployed containers can be easily mapped back to the nsd
373 LOG
.info("Starting %r as %r in DC %r" %
374 (vnf_name
, vnf_id
, vnfd
.get("dc")))
375 LOG
.debug("Interfaces for %r: %r" % (vnf_id
, intfs
))
376 vnfi
= target_dc
.startCompute(
382 cpu_period
=cpu_period
,
386 type=kwargs
.get('type', 'docker'))
388 # rename the docker0 interfaces (eth0) to the management port name
389 # defined in the VNFD
391 for intf_name
in mgmt_intf_names
:
392 self
._vnf
_reconfigure
_network
(
393 vnfi
, 'eth0', new_name
=intf_name
)
397 def _stop_vnfi(self
, vnfi
):
401 :param vnfi: vnf instance to be stopped
403 # Find the correct datacenter
404 status
= vnfi
.getStatus()
408 LOG
.info("Stopping the vnf instance contained in %r in DC %r" %
409 (status
["name"], dc
))
410 dc
.stopCompute(status
["name"])
412 def _get_vnf_instance(self
, instance_uuid
, vnf_id
):
414 Returns the Docker object for the given VNF id (or Docker name).
415 :param instance_uuid: UUID of the service instance to search in.
416 :param name: VNF name or Docker name. We are fuzzy here.
420 for vnfi
in self
.instances
[instance_uuid
]["vnf_instances"]:
423 LOG
.warning("No container with name: {0} found.".format(dn
))
427 def _vnf_reconfigure_network(vnfi
, if_name
, net_str
=None, new_name
=None):
429 Reconfigure the network configuration of a specific interface
430 of a running container.
431 :param vnfi: container instance
432 :param if_name: interface name
433 :param net_str: network configuration string, e.g., 1.2.3.4/24
437 # assign new ip address
438 if net_str
is not None:
439 intf
= vnfi
.intf(intf
=if_name
)
442 LOG
.debug("Reconfigured network of %s:%s to %r" %
443 (vnfi
.name
, if_name
, net_str
))
445 LOG
.warning("Interface not found: %s:%s. Network reconfiguration skipped." % (
448 if new_name
is not None:
449 vnfi
.cmd('ip link set', if_name
, 'down')
450 vnfi
.cmd('ip link set', if_name
, 'name', new_name
)
451 vnfi
.cmd('ip link set', new_name
, 'up')
452 LOG
.debug("Reconfigured interface name of %s:%s to %s" %
453 (vnfi
.name
, if_name
, new_name
))
455 def _trigger_emulator_start_scripts_in_vnfis(self
, vnfi_list
):
456 for vnfi
in vnfi_list
:
457 config
= vnfi
.dcinfo
.get("Config", dict())
458 env
= config
.get("Env", list())
460 var
, cmd
= map(str.strip
, map(str, env_var
.split('=', 1)))
461 LOG
.debug("%r = %r" % (var
, cmd
))
462 if var
== "SON_EMU_CMD":
463 LOG
.info("Executing entry point script in %r: %r" %
465 # execute command in new thread to ensure that GK is not
467 t
= threading
.Thread(target
=vnfi
.cmdPrint
, args
=(cmd
,))
471 def _trigger_emulator_stop_scripts_in_vnfis(self
, vnfi_list
):
472 for vnfi
in vnfi_list
:
473 config
= vnfi
.dcinfo
.get("Config", dict())
474 env
= config
.get("Env", list())
476 var
, cmd
= map(str.strip
, map(str, env_var
.split('=', 1)))
477 if var
== "SON_EMU_CMD_STOP":
478 LOG
.info("Executing stop script in %r: %r" %
480 # execute command in new thread to ensure that GK is not
482 t
= threading
.Thread(target
=vnfi
.cmdPrint
, args
=(cmd
,))
486 def _unpack_service_package(self
):
488 unzip *.son file and store contents in CATALOG_FOLDER/services/<service_uuid>/
490 LOG
.info("Unzipping: %r" % self
.package_file_path
)
491 with zipfile
.ZipFile(self
.package_file_path
, "r") as z
:
492 z
.extractall(self
.package_content_path
)
494 def _load_package_descriptor(self
):
496 Load the main package descriptor YAML and keep it as dict.
499 self
.manifest
= load_yaml(
501 self
.package_content_path
, "META-INF/MANIFEST.MF"))
505 Load the entry NSD YAML and keep it as dict.
508 if "entry_service_template" in self
.manifest
:
509 nsd_path
= os
.path
.join(
510 self
.package_content_path
,
511 make_relative_path(self
.manifest
.get("entry_service_template")))
512 self
.nsd
= load_yaml(nsd_path
)
513 GK
.net
.deployed_nsds
.append(self
.nsd
)
514 # create dict to find the vnf_name for any vnf id
515 self
.vnf_id2vnf_name
= defaultdict(lambda: "NotExistingNode",
516 reduce(lambda x
, y
: dict(x
, **y
),
517 map(lambda d
: {d
["vnf_id"]: d
["vnf_name"]},
518 self
.nsd
["network_functions"])))
520 LOG
.debug("Loaded NSD: %r" % self
.nsd
.get("name"))
522 def _load_vnfd(self
):
524 Load all VNFD YAML files referenced in MANIFEST.MF and keep them in dict.
528 # first make a list of all the vnfds in the package
530 if "package_content" in self
.manifest
:
531 for pc
in self
.manifest
.get("package_content"):
533 "content-type") == "application/sonata.function_descriptor":
534 vnfd_path
= os
.path
.join(
535 self
.package_content_path
,
536 make_relative_path(pc
.get("name")))
537 vnfd
= load_yaml(vnfd_path
)
538 vnfd_set
[vnfd
.get("name")] = vnfd
539 # then link each vnf_id in the nsd to its vnfd
540 for vnf_id
in self
.vnf_id2vnf_name
:
541 vnf_name
= self
.vnf_id2vnf_name
[vnf_id
]
542 self
.vnfds
[vnf_id
] = vnfd_set
[vnf_name
]
543 LOG
.debug("Loaded VNFD: {0} id: {1}".format(vnf_name
, vnf_id
))
545 def _load_saps(self
):
546 # create list of all SAPs
547 # check if we need to deploy management ports
549 SAPs
= [p
for p
in self
.nsd
["connection_points"]
550 if 'management' not in p
.get('type')]
552 SAPs
= [p
for p
in self
.nsd
["connection_points"]]
555 # endpoint needed in this service
556 sap_id
, sap_interface
, sap_docker_name
= parse_interface(sap
['id'])
557 # make sure SAP has type set (default internal)
558 sap
["type"] = sap
.get("type", 'internal')
560 # Each Service Access Point (connection_point) in the nsd is an IP
561 # address on the host
562 if sap
["type"] == "external":
563 # add to vnfds to calculate placement later on
564 sap_net
= SAP_SUBNETS
.pop(0)
565 self
.saps
[sap_docker_name
] = {
566 "name": sap_docker_name
, "type": "external", "net": sap_net
}
567 # add SAP vnf to list in the NSD so it is deployed later on
568 # each SAP gets a unique VNFD and vnf_id in the NSD and custom
569 # type (only defined in the dummygatekeeper)
570 self
.nsd
["network_functions"].append(
571 {"vnf_id": sap_docker_name
, "vnf_name": sap_docker_name
, "vnf_type": "sap_ext"})
573 # Each Service Access Point (connection_point) in the nsd is
574 # getting its own container (default)
575 elif sap
["type"] == "internal" or sap
["type"] == "management":
576 # add SAP to self.vnfds
578 sapfile
= pkg_resources
.resource_filename(
579 __name__
, "sap_vnfd.yml")
582 sap_vnfd
= load_yaml(sapfile
)
583 sap_vnfd
["connection_points"][0]["id"] = sap_interface
584 sap_vnfd
["name"] = sap_docker_name
585 sap_vnfd
["type"] = "internal"
586 # add to vnfds to calculate placement later on and deploy
587 self
.saps
[sap_docker_name
] = sap_vnfd
588 # add SAP vnf to list in the NSD so it is deployed later on
589 # each SAP get a unique VNFD and vnf_id in the NSD
590 self
.nsd
["network_functions"].append(
591 {"vnf_id": sap_docker_name
, "vnf_name": sap_docker_name
, "vnf_type": "sap_int"})
593 LOG
.debug("Loaded SAP: name: {0}, type: {1}".format(
594 sap_docker_name
, sap
['type']))
597 self
.saps_ext
= [self
.saps
[sap
]['name']
598 for sap
in self
.saps
if self
.saps
[sap
]["type"] == "external"]
599 self
.saps_int
= [self
.saps
[sap
]['name']
600 for sap
in self
.saps
if self
.saps
[sap
]["type"] == "internal"]
602 def _start_sap(self
, sap
, instance_uuid
):
606 LOG
.info('start SAP: {0} ,type: {1}'.format(sap
['name'], sap
['type']))
607 if sap
["type"] == "internal":
609 if not GK_STANDALONE_MODE
:
610 vnfi
= self
._start
_vnfd
(sap
, sap
['name'], type='sap_int')
611 self
.instances
[instance_uuid
]["vnf_instances"].append(vnfi
)
613 elif sap
["type"] == "external":
614 target_dc
= sap
.get("dc")
615 # add interface to dc switch
616 target_dc
.attachExternalSAP(sap
['name'], sap
['net'])
618 def _connect_elines(self
, eline_fwd_links
, instance_uuid
):
620 Connect all E-LINE links in the NSD
621 :param eline_fwd_links: list of E-LINE links in the NSD
622 :param: instance_uuid of the service
625 # cookie is used as identifier for the flowrules installed by the dummygatekeeper
626 # eg. different services get a unique cookie for their flowrules
628 for link
in eline_fwd_links
:
629 # check if we need to deploy this link when its a management link:
631 if self
.check_mgmt_interface(
632 link
["connection_points_reference"]):
635 src_id
, src_if_name
, src_sap_id
= parse_interface(
636 link
["connection_points_reference"][0])
637 dst_id
, dst_if_name
, dst_sap_id
= parse_interface(
638 link
["connection_points_reference"][1])
641 # check if there is a SAP in the link and chain everything together
642 if src_sap_id
in self
.saps
and dst_sap_id
in self
.saps
:
644 '2 SAPs cannot be chained together : {0} - {1}'.format(src_sap_id
, dst_sap_id
))
647 elif src_sap_id
in self
.saps_ext
:
649 # set intf name to None so the chaining function will choose
652 dst_vnfi
= self
._get
_vnf
_instance
(instance_uuid
, dst_id
)
653 if dst_vnfi
is not None:
654 # choose first ip address in sap subnet
655 sap_net
= self
.saps
[src_sap_id
]['net']
656 sap_ip
= "{0}/{1}".format(str(sap_net
[2]),
658 self
._vnf
_reconfigure
_network
(
659 dst_vnfi
, dst_if_name
, sap_ip
)
662 elif dst_sap_id
in self
.saps_ext
:
664 # set intf name to None so the chaining function will choose
667 src_vnfi
= self
._get
_vnf
_instance
(instance_uuid
, src_id
)
668 if src_vnfi
is not None:
669 sap_net
= self
.saps
[dst_sap_id
]['net']
670 sap_ip
= "{0}/{1}".format(str(sap_net
[2]),
672 self
._vnf
_reconfigure
_network
(
673 src_vnfi
, src_if_name
, sap_ip
)
676 # Link between 2 VNFs
678 # make sure we use the correct sap vnf name
679 if src_sap_id
in self
.saps_int
:
681 if dst_sap_id
in self
.saps_int
:
683 # re-configure the VNFs IP assignment and ensure that a new
684 # subnet is used for each E-Link
685 src_vnfi
= self
._get
_vnf
_instance
(instance_uuid
, src_id
)
686 dst_vnfi
= self
._get
_vnf
_instance
(instance_uuid
, dst_id
)
687 if src_vnfi
is not None and dst_vnfi
is not None:
688 eline_net
= ELINE_SUBNETS
.pop(0)
689 ip1
= "{0}/{1}".format(str(eline_net
[1]),
691 ip2
= "{0}/{1}".format(str(eline_net
[2]),
693 self
._vnf
_reconfigure
_network
(src_vnfi
, src_if_name
, ip1
)
694 self
._vnf
_reconfigure
_network
(dst_vnfi
, dst_if_name
, ip2
)
701 vnf_src_interface
=src_if_name
, vnf_dst_interface
=dst_if_name
,
702 bidirectional
=BIDIRECTIONAL_CHAIN
, cmd
="add-flow", cookie
=cookie
, priority
=10)
704 "Setting up E-Line link. (%s:%s) -> (%s:%s)" % (
705 src_id
, src_if_name
, dst_id
, dst_if_name
))
707 def _connect_elans(self
, elan_fwd_links
, instance_uuid
):
709 Connect all E-LAN links in the NSD
710 :param elan_fwd_links: list of E-LAN links in the NSD
711 :param: instance_uuid of the service
714 for link
in elan_fwd_links
:
715 # check if we need to deploy this link when its a management link:
717 if self
.check_mgmt_interface(
718 link
["connection_points_reference"]):
722 # check if an external SAP is in the E-LAN (then a subnet is
724 intfs_elan
= [intf
for intf
in link
["connection_points_reference"]]
725 lan_sap
= self
.check_ext_saps(intfs_elan
)
727 lan_net
= self
.saps
[lan_sap
]['net']
728 lan_hosts
= list(lan_net
.hosts())
730 lan_net
= ELAN_SUBNETS
.pop(0)
731 lan_hosts
= list(lan_net
.hosts())
733 # generate lan ip address for all interfaces except external SAPs
734 for intf
in link
["connection_points_reference"]:
736 # skip external SAPs, they already have an ip
737 vnf_id
, vnf_interface
, vnf_sap_docker_name
= parse_interface(
739 if vnf_sap_docker_name
in self
.saps_ext
:
740 elan_vnf_list
.append(
741 {'name': vnf_sap_docker_name
, 'interface': vnf_interface
})
744 ip_address
= "{0}/{1}".format(str(lan_hosts
.pop(0)),
746 vnf_id
, intf_name
, vnf_sap_id
= parse_interface(intf
)
748 # make sure we use the correct sap vnf name
749 src_docker_name
= vnf_id
750 if vnf_sap_id
in self
.saps_int
:
751 src_docker_name
= vnf_sap_id
755 "Setting up E-LAN interface. (%s:%s) -> %s" % (
756 vnf_id
, intf_name
, ip_address
))
758 # re-configure the VNFs IP assignment and ensure that a new subnet is used for each E-LAN
759 # E-LAN relies on the learning switch capability of Ryu which has to be turned on in the topology
760 # (DCNetwork(controller=RemoteController, enable_learning=True)), so no explicit chaining is necessary.
761 vnfi
= self
._get
_vnf
_instance
(instance_uuid
, vnf_id
)
763 self
._vnf
_reconfigure
_network
(vnfi
, intf_name
, ip_address
)
764 # add this vnf and interface to the E-LAN for tagging
765 elan_vnf_list
.append(
766 {'name': src_docker_name
, 'interface': intf_name
})
768 # install the VLAN tags for this E-LAN
769 GK
.net
.setLAN(elan_vnf_list
)
771 def _load_docker_files(self
):
773 Get all paths to Dockerfiles from VNFDs and store them in dict.
776 for k
, v
in self
.vnfds
.iteritems():
777 for vu
in v
.get("virtual_deployment_units"):
778 if vu
.get("vm_image_format") == "docker":
779 vm_image
= vu
.get("vm_image")
780 docker_path
= os
.path
.join(
781 self
.package_content_path
,
782 make_relative_path(vm_image
))
783 self
.local_docker_files
[k
] = docker_path
784 LOG
.debug("Found Dockerfile (%r): %r" % (k
, docker_path
))
786 def _load_docker_urls(self
):
788 Get all URLs to pre-build docker images in some repo.
791 # also merge sap dicts, because internal saps also need a docker
793 all_vnfs
= self
.vnfds
.copy()
794 all_vnfs
.update(self
.saps
)
796 for k
, v
in all_vnfs
.iteritems():
797 for vu
in v
.get("virtual_deployment_units", {}):
798 if vu
.get("vm_image_format") == "docker":
799 url
= vu
.get("vm_image")
801 url
= url
.replace("http://", "")
802 self
.remote_docker_image_urls
[k
] = url
803 LOG
.debug("Found Docker image URL (%r): %r" %
804 (k
, self
.remote_docker_image_urls
[k
]))
806 def _build_images_from_dockerfiles(self
):
808 Build Docker images for each local Dockerfile found in the package: self.local_docker_files
810 if GK_STANDALONE_MODE
:
811 return # do not build anything in standalone mode
813 LOG
.info("Building %d Docker images (this may take several minutes) ..." % len(
814 self
.local_docker_files
))
815 for k
, v
in self
.local_docker_files
.iteritems():
816 for line
in dc
.build(path
=v
.replace(
817 "Dockerfile", ""), tag
=k
, rm
=False, nocache
=False):
818 LOG
.debug("DOCKER BUILD: %s" % line
)
819 LOG
.info("Docker image created: %s" % k
)
821 def _pull_predefined_dockerimages(self
):
823 If the package contains URLs to pre-build Docker images, we download them with this method.
826 for url
in self
.remote_docker_image_urls
.itervalues():
827 # only pull if not present (speedup for development)
829 if len(dc
.images
.list(name
=url
)) > 0:
830 LOG
.debug("Image %r present. Skipping pull." % url
)
832 LOG
.info("Pulling image: %r" % url
)
833 # this seems to fail with latest docker api version 2.0.2
834 # dc.images.pull(url,
835 # insecure_registry=True)
836 # using docker cli instead
843 def _check_docker_image_exists(self
, image_name
):
845 Query the docker service and check if the given image exists
846 :param image_name: name of the docker image
849 return len(DockerClient().images
.list(name
=image_name
)) > 0
851 def _calculate_placement(self
, algorithm
):
853 Do placement by adding the a field "dc" to
854 each VNFD that points to one of our
855 data center objects known to the gatekeeper.
857 assert(len(self
.vnfds
) > 0)
858 assert(len(GK
.dcs
) > 0)
859 # instantiate algorithm an place
861 p
.place(self
.nsd
, self
.vnfds
, self
.saps
, GK
.dcs
)
862 LOG
.info("Using placement algorithm: %r" % p
.__class
__.__name
__)
863 # lets print the placement result
864 for name
, vnfd
in self
.vnfds
.iteritems():
865 LOG
.info("Placed VNF %r on DC %r" % (name
, str(vnfd
.get("dc"))))
866 for sap
in self
.saps
:
867 sap_dict
= self
.saps
[sap
]
868 LOG
.info("Placed SAP %r on DC %r" % (sap
, str(sap_dict
.get("dc"))))
870 def _calculate_cpu_cfs_values(self
, cpu_time_percentage
):
872 Calculate cpu period and quota for CFS
873 :param cpu_time_percentage: percentage of overall CPU to be used
874 :return: cpu_period, cpu_quota
876 if cpu_time_percentage
is None:
878 if cpu_time_percentage
< 0:
880 # (see: https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt)
881 # Attention minimum cpu_quota is 1ms (micro)
882 cpu_period
= 1000000 # lets consider a fixed period of 1000000 microseconds for now
883 LOG
.debug("cpu_period is %r, cpu_percentage is %r" %
884 (cpu_period
, cpu_time_percentage
))
885 # calculate the fraction of cpu time for this container
886 cpu_quota
= cpu_period
* cpu_time_percentage
887 # ATTENTION >= 1000 to avoid a invalid argument system error ... no
890 LOG
.debug("cpu_quota before correcting: %r" % cpu_quota
)
892 LOG
.warning("Increased CPU quota to avoid system error.")
893 LOG
.debug("Calculated: cpu_period=%f / cpu_quota=%f" %
894 (cpu_period
, cpu_quota
))
895 return int(cpu_period
), int(cpu_quota
)
897 def check_ext_saps(self
, intf_list
):
898 # check if the list of interfacs contains an external SAP
899 saps_ext
= [self
.saps
[sap
]['name']
900 for sap
in self
.saps
if self
.saps
[sap
]["type"] == "external"]
901 for intf_name
in intf_list
:
902 vnf_id
, vnf_interface
, vnf_sap_docker_name
= parse_interface(
904 if vnf_sap_docker_name
in saps_ext
:
905 return vnf_sap_docker_name
907 def check_mgmt_interface(self
, intf_list
):
908 SAPs_mgmt
= [p
.get('id') for p
in self
.nsd
["connection_points"]
909 if 'management' in p
.get('type')]
910 for intf_name
in intf_list
:
911 if intf_name
in SAPs_mgmt
:
916 Some (simple) placement algorithms
920 class FirstDcPlacement(object):
922 Placement: Always use one and the same data center from the GK.dcs dict.
925 def place(self
, nsd
, vnfds
, saps
, dcs
):
926 for id, vnfd
in vnfds
.iteritems():
927 vnfd
["dc"] = list(dcs
.itervalues())[0]
930 class RoundRobinDcPlacement(object):
932 Placement: Distribute VNFs across all available DCs in a round robin fashion.
935 def place(self
, nsd
, vnfds
, saps
, dcs
):
937 dcs_list
= list(dcs
.itervalues())
938 for id, vnfd
in vnfds
.iteritems():
939 vnfd
["dc"] = dcs_list
[c
% len(dcs_list
)]
940 c
+= 1 # inc. c to use next DC
943 class RoundRobinDcPlacementWithSAPs(object):
945 Placement: Distribute VNFs across all available DCs in a round robin fashion,
946 every SAP is instantiated on the same DC as the connected VNF.
949 def place(self
, nsd
, vnfds
, saps
, dcs
):
953 dcs_list
= list(dcs
.itervalues())
954 for id, vnfd
in vnfds
.iteritems():
955 vnfd
["dc"] = dcs_list
[c
% len(dcs_list
)]
956 c
+= 1 # inc. c to use next DC
959 vlinks
= nsd
.get("virtual_links", [])
960 eline_fwd_links
= [l
for l
in vlinks
if (
961 l
["connectivity_type"] == "E-Line")]
962 elan_fwd_links
= [l
for l
in vlinks
if (
963 l
["connectivity_type"] == "E-LAN")]
965 # SAPs on E-Line links are placed on the same DC as the VNF on the
967 for link
in eline_fwd_links
:
968 src_id
, src_if_name
, src_sap_id
= parse_interface(
969 link
["connection_points_reference"][0])
970 dst_id
, dst_if_name
, dst_sap_id
= parse_interface(
971 link
["connection_points_reference"][1])
973 # check if there is a SAP in the link
974 if src_sap_id
in saps
:
975 # get dc where connected vnf is mapped to
976 dc
= vnfds
[dst_id
]['dc']
977 saps
[src_sap_id
]['dc'] = dc
979 if dst_sap_id
in saps
:
980 # get dc where connected vnf is mapped to
981 dc
= vnfds
[src_id
]['dc']
982 saps
[dst_sap_id
]['dc'] = dc
984 # SAPs on E-LANs are placed on a random DC
985 dcs_list
= list(dcs
.itervalues())
986 dc_len
= len(dcs_list
)
987 for link
in elan_fwd_links
:
988 for intf
in link
["connection_points_reference"]:
989 # find SAP interfaces
990 intf_id
, intf_name
, intf_sap_id
= parse_interface(intf
)
991 if intf_sap_id
in saps
:
992 dc
= dcs_list
[randint(0, dc_len
- 1)]
993 saps
[intf_sap_id
]['dc'] = dc
997 Resource definitions and API endpoints
1001 class Packages(fr
.Resource
):
1005 Upload a *.son service package to the dummy gatekeeper.
1007 We expect request with a *.son file and store it in UPLOAD_FOLDER
1012 LOG
.info("POST /packages called")
1013 # lets search for the package in the request
1014 is_file_object
= False # make API more robust: file can be in data or in files field
1015 if "package" in request
.files
:
1016 son_file
= request
.files
["package"]
1017 is_file_object
= True
1018 elif len(request
.data
) > 0:
1019 son_file
= request
.data
1021 return {"service_uuid": None, "size": 0, "sha1": None,
1022 "error": "upload failed. file not found."}, 500
1023 # generate a uuid to reference this package
1024 service_uuid
= str(uuid
.uuid4())
1025 file_hash
= hashlib
.sha1(str(son_file
)).hexdigest()
1026 # ensure that upload folder exists
1027 ensure_dir(UPLOAD_FOLDER
)
1028 upload_path
= os
.path
.join(UPLOAD_FOLDER
, "%s.son" % service_uuid
)
1029 # store *.son file to disk
1031 son_file
.save(upload_path
)
1033 with
open(upload_path
, 'wb') as f
:
1035 size
= os
.path
.getsize(upload_path
)
1037 # first stop and delete any other running services
1039 service_list
= copy
.copy(GK
.services
)
1040 for service_uuid
in service_list
:
1041 instances_list
= copy
.copy(
1042 GK
.services
[service_uuid
].instances
)
1043 for instance_uuid
in instances_list
:
1044 # valid service and instance UUID, stop service
1045 GK
.services
.get(service_uuid
).stop_service(
1047 LOG
.info("service instance with uuid %r stopped." %
1050 # create a service object and register it
1051 s
= Service(service_uuid
, file_hash
, upload_path
)
1052 GK
.register_service_package(service_uuid
, s
)
1054 # automatically deploy the service
1056 # ok, we have a service uuid, lets start the service
1058 GK
.services
.get(service_uuid
).start_service()
1060 # generate the JSON result
1061 return {"service_uuid": service_uuid
, "size": size
,
1062 "sha1": file_hash
, "error": None}, 201
1063 except BaseException
:
1064 LOG
.exception("Service package upload failed:")
1065 return {"service_uuid": None, "size": 0,
1066 "sha1": None, "error": "upload failed"}, 500
1070 Return a list of UUID's of uploaded service packages.
1073 LOG
.info("GET /packages")
1074 return {"service_uuid_list": list(GK
.services
.iterkeys())}
1077 class Instantiations(fr
.Resource
):
1081 Instantiate a service specified by its UUID.
1082 Will return a new UUID to identify the running service instance.
1085 LOG
.info("POST /instantiations (or /requests) called")
1086 # try to extract the service uuid from the request
1087 json_data
= request
.get_json(force
=True)
1088 service_uuid
= json_data
.get("service_uuid")
1090 # lets be a bit fuzzy here to make testing easier
1091 if (service_uuid
is None or service_uuid
==
1092 "latest") and len(GK
.services
) > 0:
1093 # if we don't get a service uuid, we simple start the first service
1095 service_uuid
= list(GK
.services
.iterkeys())[0]
1096 if service_uuid
in GK
.services
:
1097 # ok, we have a service uuid, lets start the service
1098 service_instance_uuid
= GK
.services
.get(
1099 service_uuid
).start_service()
1100 return {"service_instance_uuid": service_instance_uuid
}, 201
1101 return "Service not found", 404
1105 Returns a list of UUIDs containing all running services.
1106 :return: dict / list
1108 LOG
.info("GET /instantiations")
1109 return {"service_instantiations_list": [
1110 list(s
.instances
.iterkeys()) for s
in GK
.services
.itervalues()]}
1114 Stops a running service specified by its service and instance UUID.
1116 # try to extract the service and instance UUID from the request
1117 json_data
= request
.get_json(force
=True)
1118 service_uuid
= json_data
.get("service_uuid")
1119 instance_uuid
= json_data
.get("service_instance_uuid")
1122 if service_uuid
is None and len(GK
.services
) > 0:
1123 # if we don't get a service uuid, we simply stop the last service
1125 service_uuid
= list(GK
.services
.iterkeys())[0]
1126 if instance_uuid
is None and len(
1127 GK
.services
[service_uuid
].instances
) > 0:
1128 instance_uuid
= list(
1129 GK
.services
[service_uuid
].instances
.iterkeys())[0]
1131 if service_uuid
in GK
.services
and instance_uuid
in GK
.services
[service_uuid
].instances
:
1132 # valid service and instance UUID, stop service
1133 GK
.services
.get(service_uuid
).stop_service(instance_uuid
)
1134 return "service instance with uuid %r stopped." % instance_uuid
, 200
1135 return "Service not found", 404
1138 class Exit(fr
.Resource
):
1142 Stop the running Containernet instance regardless of data transmitted
1144 list(GK
.dcs
.values())[0].net
.stop()
1147 def initialize_GK():
1152 # create a single, global GK object
1156 app
= Flask(__name__
)
1157 app
.config
['MAX_CONTENT_LENGTH'] = 512 * 1024 * 1024 # 512 MB max upload
1160 api
.add_resource(Packages
, '/packages', '/api/v2/packages')
1161 api
.add_resource(Instantiations
, '/instantiations',
1162 '/api/v2/instantiations', '/api/v2/requests')
1163 api
.add_resource(Exit
, '/emulator/exit')
1166 def start_rest_api(host
, port
, datacenters
=dict()):
1167 GK
.dcs
= datacenters
1168 GK
.net
= get_dc_network()
1169 # start the Flask server (not the best performance but ok for our use case)
1173 use_reloader
=False # this is needed to run Flask in a non-main thread
1177 def ensure_dir(name
):
1178 if not os
.path
.exists(name
):
1182 def load_yaml(path
):
1183 with
open(path
, "r") as f
:
1186 except yaml
.YAMLError
as exc
:
1187 LOG
.exception("YAML parse error: %r" % str(exc
))
1192 def make_relative_path(path
):
1193 if path
.startswith("file://"):
1194 path
= path
.replace("file://", "", 1)
1195 if path
.startswith("/"):
1196 path
= path
.replace("/", "", 1)
1200 def get_dc_network():
1202 retrieve the DCnetwork where this dummygatekeeper (GK) connects to.
1203 Assume at least 1 datacenter is connected to this GK, and that all datacenters belong to the same DCNetwork
1206 assert (len(GK
.dcs
) > 0)
1207 return GK
.dcs
.values()[0].net
1210 def parse_interface(interface_name
):
1212 convert the interface name in the nsd to the according vnf_id, vnf_interface names
1213 :param interface_name:
1217 if ':' in interface_name
:
1218 vnf_id
, vnf_interface
= interface_name
.split(':')
1219 vnf_sap_docker_name
= interface_name
.replace(':', '_')
1221 vnf_id
= interface_name
1222 vnf_interface
= interface_name
1223 vnf_sap_docker_name
= interface_name
1225 return vnf_id
, vnf_interface
, vnf_sap_docker_name
1228 def reset_subnets():
1229 # private subnet definitions for the generated interfaces
1232 SAP_SUBNETS
= generate_subnets('10.10', 0, subnet_size
=50, mask
=30)
1235 ELAN_SUBNETS
= generate_subnets('10.20', 0, subnet_size
=50, mask
=24)
1237 global ELINE_SUBNETS
1238 ELINE_SUBNETS
= generate_subnets('10.30', 0, subnet_size
=50, mask
=30)
1241 if __name__
== '__main__':
1243 Lets allow to run the API in standalone mode.
1245 GK_STANDALONE_MODE
= True
1246 logging
.getLogger("werkzeug").setLevel(logging
.INFO
)
1247 start_rest_api("0.0.0.0", 8000)