5GTANGO LLCM: Allow to publish multiple ports and UDP ports.
[osm/vim-emu.git] / src / emuvim / api / tango / llcm.py
1
2 # Copyright (c) 2018 SONATA-NFV, 5GTANGO and Paderborn University
3 # ALL RIGHTS RESERVED.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 #
17 # Neither the name of the SONATA-NFV, 5GTANGO, Paderborn University
18 # nor the names of its contributors may be used to endorse or promote
19 # products derived from this software without specific prior written
20 # permission.
21 #
22 # This work has been performed in the framework of the SONATA project,
23 # funded by the European Commission under Grant number 671517 through
24 # the Horizon 2020 and 5G-PPP programmes. The authors would like to
25 # acknowledge the contributions of their colleagues of the SONATA
26 # partner consortium (www.sonata-nfv.eu).
27 #
28 # This work has also been performed in the framework of the 5GTANGO project,
29 # funded by the European Commission under Grant number 761493 through
30 # the Horizon 2020 and 5G-PPP programmes. The authors would like to
31 # acknowledge the contributions of their colleagues of the 5GTANGO
32 # partner consortium (www.5gtango.eu).
33 import logging
34 import os
35 import uuid
36 import hashlib
37 import zipfile
38 import yaml
39 import threading
40 import datetime
41 from docker import DockerClient
42 from flask import Flask, request
43 import flask_restful as fr
44 from gevent.pywsgi import WSGIServer
45 from subprocess import Popen
46 import ipaddress
47 import copy
48 import time
49
50
51 LOG = logging.getLogger("5gtango.llcm")
52 LOG.setLevel(logging.INFO)
53
54
55 CORS_HEADER = {'Access-Control-Allow-Origin': '*',
56 'Access-Control-Allow-Methods': 'GET,OPTIONS'}
57
58
59 GK_STORAGE = "/tmp/vim-emu-tango-llcm/"
60 UPLOAD_FOLDER = os.path.join(GK_STORAGE, "uploads/")
61 CATALOG_FOLDER = os.path.join(GK_STORAGE, "catalog/")
62
63 # Enable Dockerfile build functionality
64 BUILD_DOCKERFILE = False
65
66 # flag to indicate that we run without the emulator (only the bare API for
67 # integration testing)
68 GK_STANDALONE_MODE = False
69
70 # should a new version of an image be pulled even if its available
71 FORCE_PULL = False
72
73 # flag to indicate if we use bidirectional forwarding rules in the
74 # automatic chaining process
75 BIDIRECTIONAL_CHAIN = True
76
77 # override the management interfaces in the descriptors with default
78 # docker0 interfaces in the containers
79 USE_DOCKER_MGMT = False
80
81 # automatically deploy uploaded packages (no need to execute son-access
82 # deploy --latest separately)
83 AUTO_DEPLOY = False
84
85 # and also automatically terminate any other running services
86 AUTO_DELETE = False
87
88 # global subnet definitions (see reset_subnets())
89 ELAN_SUBNETS = None
90 ELINE_SUBNETS = None
91
92 # Time in seconds to wait for vnf stop scripts to execute fully
93 VNF_STOP_WAIT_TIME = 5
94
95 # If services are instantiated multiple times, the public port
96 # mappings need to be adapted to avoid colisions. We use this
97 # offset for this: NEW_PORT (SSIID * OFFSET) + ORIGINAL_PORT
98 MULTI_INSTANCE_PORT_OFFSET = 1000
99
100 # Selected Placement Algorithm: Points to the class of the selected
101 # placement algorithm.
102 PLACEMENT_ALGORITHM_OBJ = None
103
104 # Path to folder with <container_name>.env.yml files that contain
105 # environment variables injected into the specific container
106 # when it is started.
107 PER_INSTANCE_ENV_CONFIGURATION_FOLDER = None
108
109
110 class OnBoardingException(BaseException):
111 pass
112
113
114 class Gatekeeper(object):
115
116 def __init__(self):
117 self.services = dict()
118 self.dcs = dict()
119 self.net = None
120 # used to generate short names for VNFs (Mininet limitation)
121 self.vnf_counter = 0
122 reset_subnets()
123 LOG.info("Initialized 5GTANGO LLCM module.")
124
125 def register_service_package(self, service_uuid, service):
126 """
127 register new service package
128 :param service_uuid
129 :param service object
130 """
131 self.services[service_uuid] = service
132 # lets perform all steps needed to onboard the service
133 service.onboard()
134
135
136 class Service(object):
137 """
138 This class represents a NS uploaded as a *.son package to the
139 dummy gatekeeper.
140 Can have multiple running instances of this service.
141 """
142
143 def __init__(self,
144 service_uuid,
145 package_file_hash,
146 package_file_path):
147 self.uuid = service_uuid
148 self.package_file_hash = package_file_hash
149 self.package_file_path = package_file_path
150 self.package_content_path = os.path.join(
151 CATALOG_FOLDER, "services/%s" % self.uuid)
152 self.manifest = None
153 self.nsd = None
154 self.vnfds = dict()
155 self.local_docker_files = dict()
156 self.remote_docker_image_urls = dict()
157 self.instances = dict()
158 self._instance_counter = 0
159 self.created_at = str(datetime.datetime.now())
160
161 def onboard(self):
162 """
163 Do all steps to prepare this service to be instantiated
164 :return:
165 """
166 # 1. extract the contents of the package and store them in our catalog
167 self._unpack_service_package()
168 # 2. read in all descriptor files
169 self._load_package_descriptor()
170 self._load_nsd()
171 self._load_vnfd()
172 if self.nsd is None:
173 raise OnBoardingException("No NSD found.")
174 if len(self.vnfds) < 1:
175 raise OnBoardingException("No VNFDs found.")
176 # 3. prepare container images (e.g. download or build Dockerfile)
177 if BUILD_DOCKERFILE:
178 self._load_docker_files()
179 self._build_images_from_dockerfiles()
180 else:
181 self._load_docker_urls()
182 self._pull_predefined_dockerimages()
183 # 4. reserve subnets
184 eline_fwd_links, elan_fwd_links = self._get_elines_and_elans()
185 self.eline_subnets = [ELINE_SUBNETS.pop(0) for _ in eline_fwd_links]
186 self.elan_subnets = [ELAN_SUBNETS.pop(0) for _ in elan_fwd_links]
187 LOG.debug("Reserved subnets for service '{}': E-Line: {} / E-LAN: {}"
188 .format(self.manifest.get("name"),
189 self.eline_subnets, self.elan_subnets))
190 LOG.info("On-boarded service: {}".format(self.manifest.get("name")))
191
192 def start_service(self):
193 """
194 This methods creates and starts a new service instance.
195 It computes placements, iterates over all VNFDs, and starts
196 each VNFD as a Docker container in the data center selected
197 by the placement algorithm.
198 :return:
199 """
200 LOG.info("Starting service {} ({})"
201 .format(get_triple_id(self.nsd), self.uuid))
202
203 # 1. each service instance gets a new uuid to identify it
204 instance_uuid = str(uuid.uuid4())
205 # build a instances dict (a bit like a NSR :))
206 self.instances[instance_uuid] = dict()
207 self.instances[instance_uuid]["uuid"] = self.uuid
208 # SSIID = short service instance ID (to postfix Container names)
209 self.instances[instance_uuid]["ssiid"] = self._instance_counter
210 self.instances[instance_uuid]["name"] = get_triple_id(self.nsd)
211 self.instances[instance_uuid]["vnf_instances"] = list()
212 self.instances[instance_uuid]["created_at"] = str(datetime.datetime.now())
213 # increase for next instance
214 self._instance_counter += 1
215
216 # 3. start all vnfds that we have in the service
217 for vnf_id in self.vnfds:
218 vnfd = self.vnfds[vnf_id]
219 # attention: returns a list of started deployment units
220 vnfis = self._start_vnfd(
221 vnfd, vnf_id, self.instances[instance_uuid]["ssiid"])
222 # add list of VNFIs to total VNFI list
223 self.instances[instance_uuid]["vnf_instances"].extend(vnfis)
224
225 # 4. Deploy E-Line, E-Tree and E-LAN links
226 # Attention: Only done if ""forwarding_graphs" section in NSD exists,
227 # even if "forwarding_graphs" are not used directly.
228 # Attention2: Do a copy of *_subnets with list() is important here!
229 eline_fwd_links, elan_fwd_links = self._get_elines_and_elans()
230 # 5a. deploy E-Line links
231 GK.net.deployed_elines.extend(eline_fwd_links) # bookkeeping
232 self._connect_elines(eline_fwd_links, instance_uuid, list(self.eline_subnets))
233 # 5b. deploy E-Tree/E-LAN links
234 GK.net.deployed_elans.extend(elan_fwd_links) # bookkeeping
235 self._connect_elans(elan_fwd_links, instance_uuid, list(self.elan_subnets))
236
237 # 6. run the emulator specific entrypoint scripts in the VNFIs of this
238 # service instance
239 self._trigger_emulator_start_scripts_in_vnfis(
240 self.instances[instance_uuid]["vnf_instances"])
241 # done
242 LOG.info("Service '{}' started. Instance id: {} SSIID: {}"
243 .format(self.instances[instance_uuid]["name"],
244 instance_uuid,
245 self.instances[instance_uuid]["ssiid"]))
246 return instance_uuid
247
248 def stop_service(self, instance_uuid):
249 """
250 This method stops a running service instance.
251 It iterates over all VNF instances, stopping them each
252 and removing them from their data center.
253 :param instance_uuid: the uuid of the service instance to be stopped
254 """
255 LOG.info("Stopping service %r" % self.uuid)
256 # get relevant information
257 # instance_uuid = str(self.uuid.uuid4())
258 vnf_instances = self.instances[instance_uuid]["vnf_instances"]
259 # trigger stop skripts in vnf instances and wait a few seconds for
260 # completion
261 self._trigger_emulator_stop_scripts_in_vnfis(vnf_instances)
262 time.sleep(VNF_STOP_WAIT_TIME)
263 # stop all vnfs
264 for v in vnf_instances:
265 self._stop_vnfi(v)
266 # last step: remove the instance from the list of all instances
267 del self.instances[instance_uuid]
268
269 def _get_elines_and_elans(self):
270 """
271 Get the E-Line, E-LAN, E-Tree links from the NSD.
272 """
273 # Attention: Only done if ""forwarding_graphs" section in NSD exists,
274 # even if "forwarding_graphs" are not used directly.
275 eline_fwd_links = list()
276 elan_fwd_links = list()
277 if "virtual_links" in self.nsd and "forwarding_graphs" in self.nsd:
278 vlinks = self.nsd["virtual_links"]
279 # constituent virtual links are not checked
280 eline_fwd_links = [l for l in vlinks if (
281 l["connectivity_type"] == "E-Line")]
282 elan_fwd_links = [l for l in vlinks if (
283 l["connectivity_type"] == "E-LAN" or
284 l["connectivity_type"] == "E-Tree")] # Treat E-Tree as E-LAN
285 return eline_fwd_links, elan_fwd_links
286
287 def _get_resource_limits(self, deployment_unit):
288 """
289 Extract resource limits from deployment units.
290 """
291 # defaults
292 cpu_list = None
293 cpu_period, cpu_quota = self._calculate_cpu_cfs_values(float(1.0))
294 mem_limit = None
295 # update from descriptor
296 if "resource_requirements" in deployment_unit:
297 res_req = deployment_unit.get("resource_requirements")
298 cpu_list = res_req.get("cpu").get("cpuset")
299 if cpu_list is None:
300 cpu_list = res_req.get("cpu").get("vcpus")
301 if cpu_list is not None:
302 # attention: docker expects list as string w/o spaces:
303 cpu_list = str(cpu_list).replace(" ", "").strip()
304 cpu_bw = res_req.get("cpu").get("cpu_bw")
305 if cpu_bw is None:
306 cpu_bw = 1.0
307 cpu_period, cpu_quota = self._calculate_cpu_cfs_values(float(cpu_bw))
308 mem_limit = res_req.get("memory").get("size")
309 mem_unit = str(res_req.get("memory").get("size_unit", "GB"))
310 if mem_limit is not None:
311 mem_limit = int(mem_limit)
312 # to bytes
313 if "G" in mem_unit:
314 mem_limit = mem_limit * 1024 * 1024 * 1024
315 elif "M" in mem_unit:
316 mem_limit = mem_limit * 1024 * 1024
317 elif "K" in mem_unit:
318 mem_limit = mem_limit * 1024
319 return cpu_list, cpu_period, cpu_quota, mem_limit
320
321 def _start_vnfd(self, vnfd, vnf_id, ssiid, **kwargs):
322 """
323 Start a single VNFD of this service
324 :param vnfd: vnfd descriptor dict
325 :param vnf_id: unique id of this vnf in the nsd
326 :return:
327 """
328 vnfis = list()
329 # the vnf_name refers to the container image to be deployed
330 vnf_name = vnfd.get("name")
331 # combine VDUs and CDUs
332 deployment_units = (vnfd.get("virtual_deployment_units", []) +
333 vnfd.get("cloudnative_deployment_units", []))
334 # iterate over all deployment units within each VNFDs
335 for u in deployment_units:
336 # 0. vnf_container_name = vnf_id.vdu_id
337 vnf_container_name = get_container_name(vnf_id, u.get("id"))
338 vnf_container_instance_name = get_container_name(vnf_id, u.get("id"), ssiid)
339 # 1. get the name of the docker image to star
340 if vnf_container_name not in self.remote_docker_image_urls:
341 raise Exception("No image name for %r found. Abort." % vnf_container_name)
342 docker_image_name = self.remote_docker_image_urls.get(vnf_container_name)
343 # 2. select datacenter to start the VNF in
344 target_dc = self._place(vnfd, vnf_id, u, ssiid)
345 # 3. perform some checks to ensure we can start the container
346 assert(docker_image_name is not None)
347 assert(target_dc is not None)
348 if not self._check_docker_image_exists(docker_image_name):
349 raise Exception("Docker image {} not found. Abort."
350 .format(docker_image_name))
351
352 # 4. get the resource limits
353 cpu_list, cpu_period, cpu_quota, mem_limit = self._get_resource_limits(u)
354
355 # get connection points defined for the DU
356 intfs = u.get("connection_points", [])
357 # do some re-naming of fields to be compatible to containernet
358 for i in intfs:
359 if i.get("address"):
360 i["ip"] = i.get("address")
361
362 # get ports and port_bindings from the port and publish fields of CNFD
363 # see: https://github.com/containernet/containernet/wiki/Exposing-and-mapping-network-ports
364 ports = list() # Containernet naming
365 port_bindings = dict()
366 for i in intfs:
367 if i.get("port"): # field with a single port
368 if not isinstance(i.get("port"), int):
369 LOG.info("Field 'port' is no int CP: {}".format(i))
370 else:
371 ports.append(i.get("port")) # collect all ports
372 if i.get("ports"): # list with multiple ports
373 if not isinstance(i.get("ports"), list):
374 LOG.info("Field 'port' is no list CP: {}".format(i))
375 else:
376 for p in i.get("ports"):
377 if not isinstance(p, int):
378 # do some parsing
379 try:
380 if "/udp" in p:
381 p = tuple(p.split("/"))
382 else:
383 p = int(p)
384 ports.append(p) # collect all ports
385 except BaseException as ex:
386 LOG.error(
387 "Could not parse ports list: {}".format(p))
388 LOG.error(ex)
389 else:
390 ports.append(p) # collect all ports
391 if i.get("publish"):
392 if not isinstance(i.get("publish"), dict):
393 LOG.info("Field 'publish' is no dict CP: {}".format(i))
394 else:
395 port_bindings.update(i.get("publish"))
396 # update port mapping for cases where service is deployed > 1 times
397 port_bindings = update_port_mapping_multi_instance(ssiid, port_bindings)
398 if len(ports) > 0:
399 LOG.info("{} exposes ports: {}".format(vnf_container_instance_name, ports))
400 if len(port_bindings) > 0:
401 LOG.info("{} publishes ports: {}".format(vnf_container_instance_name, port_bindings))
402
403 # 5. collect additional information to start container
404 volumes = list()
405 cenv = dict()
406 # 5.1 inject descriptor based start/stop commands into env (overwrite)
407 VNFD_CMD_START = u.get("vm_cmd_start")
408 VNFD_CMD_STOP = u.get("vm_cmd_stop")
409 if VNFD_CMD_START and not VNFD_CMD_START == "None":
410 LOG.info("Found 'vm_cmd_start'='{}' in VNFD.".format(VNFD_CMD_START) +
411 " Overwriting SON_EMU_CMD.")
412 cenv["SON_EMU_CMD"] = VNFD_CMD_START
413 if VNFD_CMD_STOP and not VNFD_CMD_STOP == "None":
414 LOG.info("Found 'vm_cmd_start'='{}' in VNFD.".format(VNFD_CMD_STOP) +
415 " Overwriting SON_EMU_CMD_STOP.")
416 cenv["SON_EMU_CMD_STOP"] = VNFD_CMD_STOP
417
418 # 5.2 inject per instance configurations based on envs
419 conf_envs = self._load_instance_conf_envs(vnf_container_instance_name)
420 cenv.update(conf_envs)
421
422 # 6. Start the container
423 LOG.info("Starting %r as %r in DC %r" %
424 (vnf_name, vnf_container_instance_name, target_dc))
425 LOG.debug("Interfaces for %r: %r" % (vnf_id, intfs))
426 # start the container
427 vnfi = target_dc.startCompute(
428 vnf_container_instance_name,
429 network=intfs,
430 image=docker_image_name,
431 cpu_quota=cpu_quota,
432 cpu_period=cpu_period,
433 cpuset_cpus=cpu_list,
434 mem_limit=mem_limit,
435 volumes=volumes,
436 properties=cenv, # environment
437 ports=ports,
438 port_bindings=port_bindings,
439 # only publish if explicitly stated in descriptor
440 publish_all_ports=False,
441 type=kwargs.get('type', 'docker'))
442 # add vnfd reference to vnfi
443 vnfi.vnfd = vnfd
444 # add container name
445 vnfi.vnf_container_name = vnf_container_name
446 vnfi.vnf_container_instance_name = vnf_container_instance_name
447 vnfi.ssiid = ssiid
448 # store vnfi
449 vnfis.append(vnfi)
450 return vnfis
451
452 def _stop_vnfi(self, vnfi):
453 """
454 Stop a VNF instance.
455 :param vnfi: vnf instance to be stopped
456 """
457 # Find the correct datacenter
458 status = vnfi.getStatus()
459 dc = vnfi.datacenter
460 # stop the vnfi
461 LOG.info("Stopping the vnf instance contained in %r in DC %r" %
462 (status["name"], dc))
463 dc.stopCompute(status["name"])
464
465 def _get_vnf_instance(self, instance_uuid, vnf_id):
466 """
467 Returns VNFI object for a given "vnf_id" or "vnf_container_name" taken from an NSD.
468 :return: single object
469 """
470 for vnfi in self.instances[instance_uuid]["vnf_instances"]:
471 if str(vnfi.name) == str(vnf_id):
472 return vnfi
473 LOG.warning("No container with name: {0} found.".format(vnf_id))
474 return None
475
476 def _get_vnf_instance_units(self, instance_uuid, vnf_id):
477 """
478 Returns a list of VNFI objects (all deployment units) for a given
479 "vnf_id" taken from an NSD.
480 :return: list
481 """
482 if vnf_id is None:
483 return None
484 r = list()
485 for vnfi in self.instances[instance_uuid]["vnf_instances"]:
486 if vnf_id in vnfi.name:
487 r.append(vnfi)
488 if len(r) > 0:
489 LOG.debug("Found units: {} for vnf_id: {}"
490 .format([i.name for i in r], vnf_id))
491 return r
492 LOG.warning("No container(s) with name: {0} found.".format(vnf_id))
493 return None
494
495 @staticmethod
496 def _vnf_reconfigure_network(vnfi, if_name, net_str=None, new_name=None):
497 """
498 Reconfigure the network configuration of a specific interface
499 of a running container.
500 :param vnfi: container instance
501 :param if_name: interface name
502 :param net_str: network configuration string, e.g., 1.2.3.4/24
503 :return:
504 """
505 # assign new ip address
506 if net_str is not None:
507 intf = vnfi.intf(intf=if_name)
508 if intf is not None:
509 intf.setIP(net_str)
510 LOG.debug("Reconfigured network of %s:%s to %r" %
511 (vnfi.name, if_name, net_str))
512 else:
513 LOG.warning("Interface not found: %s:%s. Network reconfiguration skipped." % (
514 vnfi.name, if_name))
515
516 if new_name is not None:
517 vnfi.cmd('ip link set', if_name, 'down')
518 vnfi.cmd('ip link set', if_name, 'name', new_name)
519 vnfi.cmd('ip link set', new_name, 'up')
520 LOG.debug("Reconfigured interface name of %s:%s to %s" %
521 (vnfi.name, if_name, new_name))
522
523 def _trigger_emulator_start_scripts_in_vnfis(self, vnfi_list):
524 for vnfi in vnfi_list:
525 config = vnfi.dcinfo.get("Config", dict())
526 env = config.get("Env", list())
527 for env_var in env:
528 var, cmd = map(str.strip, map(str, env_var.split('=', 1)))
529 if var == "SON_EMU_CMD" or var == "VIM_EMU_CMD":
530 LOG.info("Executing script in '{}': {}={}"
531 .format(vnfi.name, var, cmd))
532 # execute command in new thread to ensure that GK is not
533 # blocked by VNF
534 t = threading.Thread(target=vnfi.cmdPrint, args=(cmd,))
535 t.daemon = True
536 t.start()
537 break # only execute one command
538
539 def _trigger_emulator_stop_scripts_in_vnfis(self, vnfi_list):
540 for vnfi in vnfi_list:
541 config = vnfi.dcinfo.get("Config", dict())
542 env = config.get("Env", list())
543 for env_var in env:
544 var, cmd = map(str.strip, map(str, env_var.split('=', 1)))
545 if var == "SON_EMU_CMD_STOP" or var == "VIM_EMU_CMD_STOP":
546 LOG.info("Executing script in '{}': {}={}"
547 .format(vnfi.name, var, cmd))
548 # execute command in new thread to ensure that GK is not
549 # blocked by VNF
550 t = threading.Thread(target=vnfi.cmdPrint, args=(cmd,))
551 t.daemon = True
552 t.start()
553 break # only execute one command
554
555 def _load_instance_conf_envs(self, cname):
556 """
557 Try to load an instance-specific env file. If not found,
558 just return an empty dict.
559 """
560 if PER_INSTANCE_ENV_CONFIGURATION_FOLDER is None:
561 return dict()
562 try:
563 path = os.path.expanduser(PER_INSTANCE_ENV_CONFIGURATION_FOLDER)
564 path = os.path.join(path, "{}.env.yml".format(cname))
565 res = load_yaml(path)
566 LOG.info("Loaded instance-specific env file for '{}': {}"
567 .format(cname, res))
568 return res
569 except BaseException as ex:
570 LOG.info("No instance-specific env file found for: {}"
571 .format(cname))
572 del ex
573 return dict()
574
575 def _unpack_service_package(self):
576 """
577 unzip *.son file and store contents in CATALOG_FOLDER/services/<service_uuid>/
578 """
579 LOG.info("Unzipping: %r" % self.package_file_path)
580 with zipfile.ZipFile(self.package_file_path, "r") as z:
581 z.extractall(self.package_content_path)
582
583 def _load_package_descriptor(self):
584 """
585 Load the main package descriptor YAML and keep it as dict.
586 :return:
587 """
588 self.manifest = load_yaml(
589 os.path.join(
590 self.package_content_path, "TOSCA-Metadata/NAPD.yaml"))
591
592 def _load_nsd(self):
593 """
594 Load the entry NSD YAML and keep it as dict.
595 :return:
596 """
597 if "package_content" in self.manifest:
598 nsd_path = None
599 for f in self.manifest.get("package_content"):
600 if f.get("content-type") == "application/vnd.5gtango.nsd":
601 nsd_path = os.path.join(
602 self.package_content_path,
603 make_relative_path(f.get("source")))
604 break # always use the first NSD for now
605 if nsd_path is None:
606 raise OnBoardingException("No NSD with type 'application/vnd.5gtango.nsd' found.")
607 self.nsd = load_yaml(nsd_path)
608 GK.net.deployed_nsds.append(self.nsd) # TODO this seems strange (remove?)
609 LOG.debug("Loaded NSD: %r" % self.nsd.get("name"))
610 else:
611 raise OnBoardingException(
612 "No 'package_content' section in package manifest:\n{}"
613 .format(self.manifest))
614
615 def _load_vnfd(self):
616 """
617 Load all VNFD YAML files referenced in MANIFEST.MF and keep them in dict.
618 :return:
619 """
620 # first make a list of all the vnfds in the package
621 vnfd_set = dict()
622 if "package_content" in self.manifest:
623 for pc in self.manifest.get("package_content"):
624 if pc.get(
625 "content-type") == "application/vnd.5gtango.vnfd":
626 vnfd_path = os.path.join(
627 self.package_content_path,
628 make_relative_path(pc.get("source")))
629 vnfd = load_yaml(vnfd_path)
630 vnfd_set[vnfd.get("name")] = vnfd
631 if len(vnfd_set) < 1:
632 raise OnBoardingException("No VNFDs found.")
633 # then link each vnf_id in the nsd to its vnfd
634 for v in self.nsd.get("network_functions"):
635 if v.get("vnf_name") in vnfd_set:
636 self.vnfds[v.get("vnf_id")] = vnfd_set[v.get("vnf_name")]
637 LOG.debug("Loaded VNFD: {0} id: {1}"
638 .format(v.get("vnf_name"), v.get("vnf_id")))
639
640 def _connect_elines(self, eline_fwd_links, instance_uuid, subnets):
641 """
642 Connect all E-LINE links in the NSD
643 Attention: This method DOES NOT support multi V/CDU VNFs!
644 :param eline_fwd_links: list of E-LINE links in the NSD
645 :param: instance_uuid of the service
646 :param: subnets list of subnets to be used
647 :return:
648 """
649 # cookie is used as identifier for the flowrules installed by the dummygatekeeper
650 # eg. different services get a unique cookie for their flowrules
651 cookie = 1
652 for link in eline_fwd_links:
653 LOG.info("Found E-Line: {}".format(link))
654 src_id, src_if_name = parse_interface(
655 link["connection_points_reference"][0])
656 dst_id, dst_if_name = parse_interface(
657 link["connection_points_reference"][1])
658 LOG.info("Searching C/VDU for E-Line: src={}, src_if={}, dst={}, dst_if={}"
659 .format(src_id, src_if_name, dst_id, dst_if_name))
660 # handle C/VDUs (ugly hack, only one V/CDU per VNF for now)
661 src_units = self._get_vnf_instance_units(instance_uuid, src_id)
662 dst_units = self._get_vnf_instance_units(instance_uuid, dst_id)
663 if src_units is None or dst_units is None:
664 LOG.info("No VNF-VNF link. Skipping: src={}, src_if={}, dst={}, dst_if={}"
665 .format(src_id, src_if_name, dst_id, dst_if_name))
666 return
667 # we only support VNFs with one V/CDU right now
668 if len(src_units) != 1 or len(dst_units) != 1:
669 raise BaseException("LLCM does not support E-LINES for multi V/CDU VNFs.")
670 # get the full name from that C/VDU and use it as src_id and dst_id
671 src_id = src_units[0].name
672 dst_id = dst_units[0].name
673 # from here we have all info we need
674 LOG.info("Creating E-Line for C/VDU: src={}, src_if={}, dst={}, dst_if={}"
675 .format(src_id, src_if_name, dst_id, dst_if_name))
676 # get involved vnfis
677 src_vnfi = src_units[0]
678 dst_vnfi = dst_units[0]
679 # proceed with chaining setup
680 setChaining = False
681 if src_vnfi is not None and dst_vnfi is not None:
682 setChaining = True
683 # re-configure the VNFs IP assignment and ensure that a new
684 # subnet is used for each E-Link
685 eline_net = subnets.pop(0)
686 ip1 = "{0}/{1}".format(str(eline_net[1]),
687 eline_net.prefixlen)
688 ip2 = "{0}/{1}".format(str(eline_net[2]),
689 eline_net.prefixlen)
690 # check if VNFs have fixed IPs (ip/address field in VNFDs)
691 if (self._get_vnfd_cp_from_vnfi(
692 src_vnfi, src_if_name).get("ip") is None and
693 self._get_vnfd_cp_from_vnfi(
694 src_vnfi, src_if_name).get("address") is None):
695 self._vnf_reconfigure_network(src_vnfi, src_if_name, ip1)
696 # check if VNFs have fixed IPs (ip field in VNFDs)
697 if (self._get_vnfd_cp_from_vnfi(
698 dst_vnfi, dst_if_name).get("ip") is None and
699 self._get_vnfd_cp_from_vnfi(
700 dst_vnfi, dst_if_name).get("address") is None):
701 self._vnf_reconfigure_network(dst_vnfi, dst_if_name, ip2)
702 # set the chaining
703 if setChaining:
704 GK.net.setChain(
705 src_id, dst_id,
706 vnf_src_interface=src_if_name, vnf_dst_interface=dst_if_name,
707 bidirectional=BIDIRECTIONAL_CHAIN, cmd="add-flow", cookie=cookie, priority=10)
708
709 def _get_vnfd_cp_from_vnfi(self, vnfi, ifname):
710 """
711 Gets the connection point data structure from the VNFD
712 of the given VNFI using ifname.
713 """
714 if vnfi.vnfd is None:
715 return {}
716 cps = vnfi.vnfd.get("connection_points")
717 for cp in cps:
718 if cp.get("id") == ifname:
719 return cp
720
721 def _connect_elans(self, elan_fwd_links, instance_uuid, subnets):
722 """
723 Connect all E-LAN/E-Tree links in the NSD
724 This method supports multi-V/CDU VNFs if the connection
725 point names of the DUs are the same as the ones in the NSD.
726 :param elan_fwd_links: list of E-LAN links in the NSD
727 :param: instance_uuid of the service
728 :param: subnets list of subnets to be used
729 :return:
730 """
731 for link in elan_fwd_links:
732 # a new E-LAN/E-Tree
733 elan_vnf_list = []
734 lan_net = subnets.pop(0)
735 lan_hosts = list(lan_net.hosts())
736
737 # generate lan ip address for all interfaces (of all involved (V/CDUs))
738 for intf in link["connection_points_reference"]:
739 vnf_id, intf_name = parse_interface(intf)
740 if vnf_id is None:
741 continue # skip references to NS connection points
742 units = self._get_vnf_instance_units(instance_uuid, vnf_id)
743 if units is None:
744 continue # skip if no deployment unit is present
745 # iterate over all involved deployment units
746 for uvnfi in units:
747 # Attention: we apply a simplification for multi DU VNFs here:
748 # the connection points of all involved DUs have to have the same
749 # name as the connection points of the surrounding VNF to be mapped.
750 # This is because we do not consider links specified in the VNFds
751 container_name = uvnfi.name
752 ip_address = "{0}/{1}".format(str(lan_hosts.pop(0)),
753 lan_net.prefixlen)
754 LOG.debug(
755 "Setting up E-LAN/E-Tree interface. (%s:%s) -> %s" % (
756 container_name, intf_name, ip_address))
757 # re-configure the VNFs IP assignment and ensure that a new subnet is used for each E-LAN
758 # E-LAN relies on the learning switch capability of Ryu which has to be turned on in the topology
759 # (DCNetwork(controller=RemoteController, enable_learning=True)), so no explicit chaining is
760 # necessary.
761 vnfi = self._get_vnf_instance(instance_uuid, container_name)
762 if vnfi is not None:
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': container_name, 'interface': intf_name})
767 # install the VLAN tags for this E-LAN
768 GK.net.setLAN(elan_vnf_list)
769
770 def _load_docker_files(self):
771 """
772 Get all paths to Dockerfiles from VNFDs and store them in dict.
773 :return:
774 """
775 for vnf_id, v in self.vnfds.iteritems():
776 for vu in v.get("virtual_deployment_units", []):
777 vnf_container_name = get_container_name(vnf_id, vu.get("id"))
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[vnf_container_name] = docker_path
784 LOG.debug("Found Dockerfile (%r): %r" % (vnf_container_name, docker_path))
785 for cu in v.get("cloudnative_deployment_units", []):
786 vnf_container_name = get_container_name(vnf_id, cu.get("id"))
787 image = cu.get("image")
788 docker_path = os.path.join(
789 self.package_content_path,
790 make_relative_path(image))
791 self.local_docker_files[vnf_container_name] = docker_path
792 LOG.debug("Found Dockerfile (%r): %r" % (vnf_container_name, docker_path))
793
794 def _load_docker_urls(self):
795 """
796 Get all URLs to pre-build docker images in some repo.
797 :return:
798 """
799 for vnf_id, v in self.vnfds.iteritems():
800 for vu in v.get("virtual_deployment_units", []):
801 vnf_container_name = get_container_name(vnf_id, vu.get("id"))
802 if vu.get("vm_image_format") == "docker":
803 url = vu.get("vm_image")
804 if url is not None:
805 url = url.replace("http://", "")
806 self.remote_docker_image_urls[vnf_container_name] = url
807 LOG.debug("Found Docker image URL (%r): %r" %
808 (vnf_container_name,
809 self.remote_docker_image_urls[vnf_container_name]))
810 for cu in v.get("cloudnative_deployment_units", []):
811 vnf_container_name = get_container_name(vnf_id, cu.get("id"))
812 url = cu.get("image")
813 if url is not None:
814 url = url.replace("http://", "")
815 self.remote_docker_image_urls[vnf_container_name] = url
816 LOG.debug("Found Docker image URL (%r): %r" %
817 (vnf_container_name,
818 self.remote_docker_image_urls[vnf_container_name]))
819
820 def _build_images_from_dockerfiles(self):
821 """
822 Build Docker images for each local Dockerfile found in the package: self.local_docker_files
823 """
824 if GK_STANDALONE_MODE:
825 return # do not build anything in standalone mode
826 dc = DockerClient()
827 LOG.info("Building %d Docker images (this may take several minutes) ..." % len(
828 self.local_docker_files))
829 for k, v in self.local_docker_files.iteritems():
830 for line in dc.build(path=v.replace(
831 "Dockerfile", ""), tag=k, rm=False, nocache=False):
832 LOG.debug("DOCKER BUILD: %s" % line)
833 LOG.info("Docker image created: %s" % k)
834
835 def _pull_predefined_dockerimages(self):
836 """
837 If the package contains URLs to pre-build Docker images, we download them with this method.
838 """
839 dc = DockerClient()
840 for url in self.remote_docker_image_urls.itervalues():
841 # only pull if not present (speedup for development)
842 if not FORCE_PULL:
843 if len(dc.images.list(name=url)) > 0:
844 LOG.debug("Image %r present. Skipping pull." % url)
845 continue
846 LOG.info("Pulling image: %r" % url)
847 # this seems to fail with latest docker api version 2.0.2
848 # dc.images.pull(url,
849 # insecure_registry=True)
850 # using docker cli instead
851 cmd = ["docker",
852 "pull",
853 url,
854 ]
855 Popen(cmd).wait()
856
857 def _check_docker_image_exists(self, image_name):
858 """
859 Query the docker service and check if the given image exists
860 :param image_name: name of the docker image
861 :return:
862 """
863 return len(DockerClient().images.list(name=image_name)) > 0
864
865 def _place(self, vnfd, vnfid, vdu, ssiid):
866 """
867 Do placement. Return the name of the DC to place
868 the given VDU.
869 """
870 assert(len(self.vnfds) > 0)
871 assert(len(GK.dcs) > 0)
872 if PLACEMENT_ALGORITHM_OBJ is None:
873 LOG.error("No placement algorithm given. Using FirstDcPlacement!")
874 p = FirstDcPlacement()
875 else:
876 p = PLACEMENT_ALGORITHM_OBJ
877 cname = get_container_name(vnfid, vdu.get("id"), ssiid)
878 rdc = p.place(GK.dcs, vnfd, vnfid, vdu, ssiid, cname)
879 LOG.info("Placement: '{}' --> '{}'".format(cname, rdc))
880 return rdc
881
882 def _calculate_cpu_cfs_values(self, cpu_time_percentage):
883 """
884 Calculate cpu period and quota for CFS
885 :param cpu_time_percentage: percentage of overall CPU to be used
886 :return: cpu_period, cpu_quota
887 """
888 if cpu_time_percentage is None:
889 return -1, -1
890 if cpu_time_percentage < 0:
891 return -1, -1
892 # (see: https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt)
893 # Attention minimum cpu_quota is 1ms (micro)
894 cpu_period = 1000000 # lets consider a fixed period of 1000000 microseconds for now
895 LOG.debug("cpu_period is %r, cpu_percentage is %r" %
896 (cpu_period, cpu_time_percentage))
897 # calculate the fraction of cpu time for this container
898 cpu_quota = cpu_period * cpu_time_percentage
899 # ATTENTION >= 1000 to avoid a invalid argument system error ... no
900 # idea why
901 if cpu_quota < 1000:
902 LOG.debug("cpu_quota before correcting: %r" % cpu_quota)
903 cpu_quota = 1000
904 LOG.warning("Increased CPU quota to avoid system error.")
905 LOG.debug("Calculated: cpu_period=%f / cpu_quota=%f" %
906 (cpu_period, cpu_quota))
907 return int(cpu_period), int(cpu_quota)
908
909
910 """
911 Some (simple) placement algorithms
912 """
913
914
915 class FirstDcPlacement(object):
916 """
917 Placement: Always use one and the same data center from the GK.dcs dict.
918 """
919
920 def place(self, dcs, vnfd, vnfid, vdu, ssiid, cname):
921 return list(dcs.itervalues())[0]
922
923
924 class RoundRobinDcPlacement(object):
925 """
926 Placement: Distribute VNFs across all available DCs in a round robin fashion.
927 """
928
929 def __init__(self):
930 self.count = 0
931
932 def place(self, dcs, vnfd, vnfid, vdu, ssiid, cname):
933 dcs_list = list(dcs.itervalues())
934 rdc = dcs_list[self.count % len(dcs_list)]
935 self.count += 1 # inc. count to use next DC
936 return rdc
937
938
939 class StaticConfigPlacement(object):
940 """
941 Placement: Fixed assignment based on config file.
942 """
943
944 def __init__(self, path=None):
945 if path is None:
946 path = "static_placement.yml"
947 path = os.path.expanduser(path)
948 self.static_placement = dict()
949 try:
950 self.static_placement = load_yaml(path)
951 except BaseException as ex:
952 LOG.error(ex)
953 LOG.error("Couldn't load placement from {}"
954 .format(path))
955 LOG.info("Loaded static placement: {}"
956 .format(self.static_placement))
957
958 def place(self, dcs, vnfd, vnfid, vdu, ssiid, cname):
959 # check for container name entry
960 if cname not in self.static_placement:
961 LOG.error("Coudn't find {} in placement".format(cname))
962 LOG.error("Using first DC as fallback!")
963 return list(dcs.itervalues())[0]
964 # lookup
965 candidate_dc = self.static_placement.get(cname)
966 # check if DC exsits
967 if candidate_dc not in dcs:
968 LOG.error("Coudn't find DC {}".format(candidate_dc))
969 LOG.error("Using first DC as fallback!")
970 return list(dcs.itervalues())[0]
971 # return correct DC
972 return dcs.get(candidate_dc)
973
974
975 """
976 Resource definitions and API endpoints
977 """
978
979
980 class Packages(fr.Resource):
981
982 def post(self):
983 """
984 Upload a *.son service package to the dummy gatekeeper.
985
986 We expect request with a *.son file and store it in UPLOAD_FOLDER
987 :return: UUID
988 """
989 try:
990 # get file contents
991 LOG.info("POST /packages called")
992 # lets search for the package in the request
993 is_file_object = False # make API more robust: file can be in data or in files field
994 if "package" in request.files:
995 son_file = request.files["package"]
996 is_file_object = True
997 elif len(request.data) > 0:
998 son_file = request.data
999 else:
1000 return {"service_uuid": None, "size": 0, "sha1": None,
1001 "error": "upload failed. file not found."}, 500
1002 # generate a uuid to reference this package
1003 service_uuid = str(uuid.uuid4())
1004 file_hash = hashlib.sha1(str(son_file)).hexdigest()
1005 # ensure that upload folder exists
1006 ensure_dir(UPLOAD_FOLDER)
1007 upload_path = os.path.join(UPLOAD_FOLDER, "%s.tgo" % service_uuid)
1008 # store *.son file to disk
1009 if is_file_object:
1010 son_file.save(upload_path)
1011 else:
1012 with open(upload_path, 'wb') as f:
1013 f.write(son_file)
1014 size = os.path.getsize(upload_path)
1015
1016 # first stop and delete any other running services
1017 if AUTO_DELETE:
1018 service_list = copy.copy(GK.services)
1019 for service_uuid in service_list:
1020 instances_list = copy.copy(
1021 GK.services[service_uuid].instances)
1022 for instance_uuid in instances_list:
1023 # valid service and instance UUID, stop service
1024 GK.services.get(service_uuid).stop_service(
1025 instance_uuid)
1026 LOG.info("service instance with uuid %r stopped." %
1027 instance_uuid)
1028
1029 # create a service object and register it
1030 s = Service(service_uuid, file_hash, upload_path)
1031 GK.register_service_package(service_uuid, s)
1032
1033 # automatically deploy the service
1034 if AUTO_DEPLOY:
1035 # ok, we have a service uuid, lets start the service
1036 reset_subnets()
1037 GK.services.get(service_uuid).start_service()
1038
1039 # generate the JSON result
1040 return {"service_uuid": service_uuid, "size": size,
1041 "sha1": file_hash, "error": None}, 201
1042 except BaseException:
1043 LOG.exception("Service package upload failed:")
1044 return {"service_uuid": None, "size": 0,
1045 "sha1": None, "error": "upload failed"}, 500
1046
1047 def get(self):
1048 """
1049 Return a list of package descriptor headers.
1050 Fakes the behavior of 5GTANGO's GK API to be
1051 compatible with tng-cli.
1052 :return: list
1053 """
1054 LOG.info("GET /packages")
1055 result = list()
1056 for suuid, sobj in GK.services.iteritems():
1057 pkg = dict()
1058 pkg["pd"] = dict()
1059 pkg["uuid"] = suuid
1060 pkg["pd"]["name"] = sobj.manifest.get("name")
1061 pkg["pd"]["version"] = sobj.manifest.get("version")
1062 pkg["created_at"] = sobj.created_at
1063 result.append(pkg)
1064 return result, 200, CORS_HEADER
1065
1066
1067 class Services(fr.Resource):
1068
1069 def get(self):
1070 """
1071 Return a list of services.
1072 Fakes the behavior of 5GTANGO's GK API to be
1073 compatible with tng-cli.
1074 :return: list
1075 """
1076 LOG.info("GET /services")
1077 result = list()
1078 for suuid, sobj in GK.services.iteritems():
1079 service = dict()
1080 service["nsd"] = dict()
1081 service["uuid"] = suuid
1082 service["nsd"]["name"] = sobj.nsd.get("name")
1083 service["nsd"]["version"] = sobj.nsd.get("version")
1084 service["created_at"] = sobj.created_at
1085 result.append(service)
1086 return result, 200, CORS_HEADER
1087
1088
1089 class Instantiations(fr.Resource):
1090
1091 def post(self):
1092 """
1093 Instantiate a service specified by its UUID.
1094 Will return a new UUID to identify the running service instance.
1095 :return: UUID
1096 """
1097 LOG.info("POST /instantiations (or /requests) called")
1098 # try to extract the service uuid from the request
1099 json_data = request.get_json(force=True)
1100 service_uuid = json_data.get("service_uuid")
1101 service_name = json_data.get("service_name")
1102 if service_name is None:
1103 # lets be fuzzy
1104 service_name = service_uuid
1105 # first try to find by service_name
1106 if service_name is not None:
1107 for s_uuid, s in GK.services.iteritems():
1108 if s.manifest.get("name") == service_name:
1109 LOG.info("Searched for: {}. Found service w. UUID: {}"
1110 .format(service_name, s_uuid))
1111 service_uuid = s_uuid
1112 # lets be a bit fuzzy here to make testing easier
1113 if (service_uuid is None or service_uuid ==
1114 "latest") and len(GK.services) > 0:
1115 # if we don't get a service uuid, we simple start the first service
1116 # in the list
1117 service_uuid = list(GK.services.iterkeys())[0]
1118 if service_uuid in GK.services:
1119 # ok, we have a service uuid, lets start the service
1120 service_instance_uuid = GK.services.get(
1121 service_uuid).start_service()
1122 # multiple ID fields to be compatible with tng-bench and tng-cli
1123 return ({"service_instance_uuid": service_instance_uuid,
1124 "id": service_instance_uuid}, 201)
1125 LOG.error("Service not found: {}/{}".format(service_uuid, service_name))
1126 return "Service not found", 404
1127
1128 def get(self):
1129 """
1130 Returns a list of UUIDs containing all running services.
1131 :return: dict / list
1132 """
1133 LOG.debug("GET /instantiations or /api/v3/records/services")
1134 # return {"service_instantiations_list": [
1135 # list(s.instances.iterkeys()) for s in GK.services.itervalues()]}
1136 result = list()
1137 for suuid, sobj in GK.services.iteritems():
1138 for iuuid, iobj in sobj.instances.iteritems():
1139 inst = dict()
1140 inst["uuid"] = iobj.get("uuid")
1141 inst["instance_name"] = "{}-inst.{}".format(
1142 iobj.get("name"), iobj.get("ssiid"))
1143 inst["status"] = "running"
1144 inst["created_at"] = iobj.get("created_at")
1145 result.append(inst)
1146 return result, 200, CORS_HEADER
1147
1148 def delete(self):
1149 """
1150 Stops a running service specified by its service and instance UUID.
1151 """
1152 # try to extract the service and instance UUID from the request
1153 json_data = request.get_json(force=True)
1154 service_uuid_input = json_data.get("service_uuid")
1155 instance_uuid_input = json_data.get("service_instance_uuid")
1156 if len(GK.services) < 1:
1157 return "No service on-boarded.", 404
1158 # try to be fuzzy
1159 if service_uuid_input is None:
1160 # if we don't get a service uuid we stop all services
1161 service_uuid_list = list(GK.services.iterkeys())
1162 LOG.info("No service_uuid given, stopping all.")
1163 else:
1164 service_uuid_list = [service_uuid_input]
1165 # for each service
1166 for service_uuid in service_uuid_list:
1167 if instance_uuid_input is None:
1168 instance_uuid_list = list(
1169 GK.services[service_uuid].instances.iterkeys())
1170 else:
1171 instance_uuid_list = [instance_uuid_input]
1172 # for all service instances
1173 for instance_uuid in instance_uuid_list:
1174 if (service_uuid in GK.services and
1175 instance_uuid in GK.services[service_uuid].instances):
1176 # valid service and instance UUID, stop service
1177 GK.services.get(service_uuid).stop_service(instance_uuid)
1178 LOG.info("Service instance with uuid %r stopped." % instance_uuid)
1179 return "Service(s) stopped.", 200
1180
1181
1182 class Exit(fr.Resource):
1183
1184 def put(self):
1185 """
1186 Stop the running Containernet instance regardless of data transmitted
1187 """
1188 list(GK.dcs.values())[0].net.stop()
1189
1190
1191 def generate_subnets(prefix, base, subnet_size=50, mask=24):
1192 # Generate a list of ipaddress in subnets
1193 r = list()
1194 for net in range(base, base + subnet_size):
1195 subnet = "{0}.{1}.0/{2}".format(prefix, net, mask)
1196 r.append(ipaddress.ip_network(unicode(subnet)))
1197 return r
1198
1199
1200 def reset_subnets():
1201 global ELINE_SUBNETS
1202 global ELAN_SUBNETS
1203 # private subnet definitions for the generated interfaces
1204 # 30.0.xxx.0/24
1205 ELAN_SUBNETS = generate_subnets('30.0', 0, subnet_size=50, mask=24)
1206 # 20.0.xxx.0/24
1207 ELINE_SUBNETS = generate_subnets('20.0', 0, subnet_size=50, mask=24)
1208
1209
1210 def initialize_GK():
1211 global GK
1212 GK = Gatekeeper()
1213
1214
1215 # create a single, global GK object
1216 GK = None
1217 initialize_GK()
1218 # setup Flask
1219 http_server = None
1220 app = Flask(__name__)
1221 app.config['MAX_CONTENT_LENGTH'] = 512 * 1024 * 1024 # 512 MB max upload
1222 api = fr.Api(app)
1223 # define endpoints
1224 api.add_resource(Packages, '/packages', '/api/v2/packages', '/api/v3/packages')
1225 api.add_resource(Services, '/services', '/api/v2/services', '/api/v3/services')
1226 api.add_resource(Instantiations, '/instantiations',
1227 '/api/v2/instantiations', '/api/v2/requests', '/api/v3/requests',
1228 '/api/v3/records/services')
1229 api.add_resource(Exit, '/emulator/exit')
1230
1231
1232 def start_rest_api(host, port, datacenters=dict()):
1233 global http_server
1234 GK.dcs = datacenters
1235 GK.net = get_dc_network()
1236 # start the Flask server (not the best performance but ok for our use case)
1237 # app.run(host=host,
1238 # port=port,
1239 # debug=True,
1240 # use_reloader=False # this is needed to run Flask in a non-main thread
1241 # )
1242 http_server = WSGIServer((host, port), app, log=open("/dev/null", "w"))
1243 http_server.serve_forever()
1244
1245
1246 def stop_rest_api():
1247 if http_server:
1248 http_server.close()
1249
1250
1251 def ensure_dir(name):
1252 if not os.path.exists(name):
1253 os.makedirs(name)
1254
1255
1256 def load_yaml(path):
1257 with open(path, "r") as f:
1258 try:
1259 r = yaml.load(f)
1260 except yaml.YAMLError as exc:
1261 LOG.exception("YAML parse error: %r" % str(exc))
1262 r = dict()
1263 return r
1264
1265
1266 def make_relative_path(path):
1267 if path.startswith("file://"):
1268 path = path.replace("file://", "", 1)
1269 if path.startswith("/"):
1270 path = path.replace("/", "", 1)
1271 return path
1272
1273
1274 def get_dc_network():
1275 """
1276 retrieve the DCnetwork where this dummygatekeeper (GK) connects to.
1277 Assume at least 1 datacenter is connected to this GK, and that all datacenters belong to the same DCNetwork
1278 :return:
1279 """
1280 assert (len(GK.dcs) > 0)
1281 return GK.dcs.values()[0].net
1282
1283
1284 def parse_interface(interface_name):
1285 """
1286 convert the interface name in the nsd to the according vnf_id, vnf_interface names
1287 :param interface_name:
1288 :return:
1289 """
1290 if ':' in interface_name:
1291 vnf_id, vnf_interface = interface_name.split(':')
1292 else:
1293 vnf_id = None
1294 vnf_interface = interface_name
1295 return vnf_id, vnf_interface
1296
1297
1298 def get_container_name(vnf_id, vdu_id, ssiid=None):
1299 if ssiid is not None:
1300 return "{}.{}.{}".format(vnf_id, vdu_id, ssiid)
1301 return "{}.{}".format(vnf_id, vdu_id)
1302
1303
1304 def get_triple_id(descr):
1305 return "{}.{}.{}".format(
1306 descr.get("vendor"), descr.get("name"), descr.get("version"))
1307
1308
1309 def update_port_mapping_multi_instance(ssiid, port_bindings):
1310 """
1311 Port_bindings are used to expose ports of the deployed containers.
1312 They would collide if we deploy multiple service instances.
1313 This function adds a offset to them which is based on the
1314 short service instance id (SSIID).
1315 MULTI_INSTANCE_PORT_OFFSET
1316 """
1317 def _offset(p):
1318 return p + MULTI_INSTANCE_PORT_OFFSET * ssiid
1319
1320 port_bindings = {k: _offset(v) for k, v in port_bindings.iteritems()}
1321 return port_bindings
1322
1323
1324 if __name__ == '__main__':
1325 """
1326 Lets allow to run the API in standalone mode.
1327 """
1328 GK_STANDALONE_MODE = True
1329 logging.getLogger("werkzeug").setLevel(logging.INFO)
1330 start_rest_api("0.0.0.0", 8000)