Made fake GK compatible with latest VNFD format.
[osm/vim-emu.git] / src / emuvim / api / sonata / dummygatekeeper.py
1 """
2 This module implements a simple REST API that behaves like SONATA's gatekeeper.
3
4 It is only used to support the development of SONATA's SDK tools and to demonstrate
5 the year 1 version of the emulator until the integration with WP4's orchestrator is done.
6 """
7
8 import logging
9 import os
10 import uuid
11 import hashlib
12 import zipfile
13 import yaml
14 from docker import Client as DockerClient
15 from flask import Flask, request
16 import flask_restful as fr
17
18 logging.basicConfig()
19 LOG = logging.getLogger("sonata-dummy-gatekeeper")
20 LOG.setLevel(logging.DEBUG)
21 logging.getLogger("werkzeug").setLevel(logging.WARNING)
22
23 GK_STORAGE = "/tmp/son-dummy-gk/"
24 UPLOAD_FOLDER = os.path.join(GK_STORAGE, "uploads/")
25 CATALOG_FOLDER = os.path.join(GK_STORAGE, "catalog/")
26
27 # flag to indicate that we run without the emulator (only the bare API for integration testing)
28 GK_STANDALONE_MODE = False
29
30
31 class Gatekeeper(object):
32
33 def __init__(self):
34 self.services = dict()
35 self.dcs = dict()
36 self.vnf_counter = 0 # used to generate short names for VNFs (Mininet limitation)
37 LOG.info("Create SONATA dummy gatekeeper.")
38
39 def register_service_package(self, service_uuid, service):
40 """
41 register new service package
42 :param service_uuid
43 :param service object
44 """
45 self.services[service_uuid] = service
46 # lets perform all steps needed to onboard the service
47 service.onboard()
48
49 def get_next_vnf_name(self):
50 self.vnf_counter += 1
51 return "vnf%d" % self.vnf_counter
52
53
54 class Service(object):
55 """
56 This class represents a NS uploaded as a *.son package to the
57 dummy gatekeeper.
58 Can have multiple running instances of this service.
59 """
60
61 def __init__(self,
62 service_uuid,
63 package_file_hash,
64 package_file_path):
65 self.uuid = service_uuid
66 self.package_file_hash = package_file_hash
67 self.package_file_path = package_file_path
68 self.package_content_path = os.path.join(CATALOG_FOLDER, "services/%s" % self.uuid)
69 self.manifest = None
70 self.nsd = None
71 self.vnfds = dict()
72 self.local_docker_files = dict()
73 self.instances = dict()
74
75 def onboard(self):
76 """
77 Do all steps to prepare this service to be instantiated
78 :return:
79 """
80 # 1. extract the contents of the package and store them in our catalog
81 self._unpack_service_package()
82 # 2. read in all descriptor files
83 self._load_package_descriptor()
84 self._load_nsd()
85 self._load_vnfd()
86 self._load_docker_files()
87 # 3. prepare container images (e.g. download or build Dockerfile)
88 self._build_images_from_dockerfiles()
89 self._download_predefined_dockerimages()
90
91 LOG.info("On-boarded service: %r" % self.manifest.get("package_name"))
92
93 def start_service(self):
94 """
95 This methods creates and starts a new service instance.
96 It computes placements, iterates over all VNFDs, and starts
97 each VNFD as a Docker container in the data center selected
98 by the placement algorithm.
99 :return:
100 """
101 LOG.info("Starting service %r" % self.uuid)
102 # 1. each service instance gets a new uuid to identify it
103 instance_uuid = str(uuid.uuid4())
104 # build a instances dict (a bit like a NSR :))
105 self.instances[instance_uuid] = dict()
106 self.instances[instance_uuid]["vnf_instances"] = list()
107 # 2. compute placement of this service instance (adds DC names to VNFDs)
108 if not GK_STANDALONE_MODE:
109 self._calculate_placement(FirstDcPlacement)
110 # iterate over all vnfds that we have to start
111 for vnfd in self.vnfds.itervalues():
112 vnfi = None
113 if not GK_STANDALONE_MODE:
114 vnfi = self._start_vnfd(vnfd)
115 self.instances[instance_uuid]["vnf_instances"].append(vnfi)
116 LOG.info("Service started. Instance id: %r" % instance_uuid)
117 return instance_uuid
118
119 def _start_vnfd(self, vnfd):
120 """
121 Start a single VNFD of this service
122 :param vnfd: vnfd descriptor dict
123 :return:
124 """
125 # iterate over all deployment units within each VNFDs
126 for u in vnfd.get("virtual_deployment_units"):
127 # 1. get the name of the docker image to start and the assigned DC
128 docker_name = vnfd.get("vnf_name")
129 target_dc = vnfd.get("dc")
130 # 2. perform some checks to ensure we can start the container
131 assert(docker_name is not None)
132 assert(target_dc is not None)
133 if not self._check_docker_image_exists(docker_name):
134 raise Exception("Docker image %r not found. Abort." % docker_name)
135 # 3. do the dc.startCompute(name="foobar") call to run the container
136 # TODO consider flavors, and other annotations
137 vnfi = target_dc.startCompute(GK.get_next_vnf_name(), image=docker_name, flavor_name="small")
138 # 6. store references to the compute objects in self.instances
139 return vnfi
140
141 def _unpack_service_package(self):
142 """
143 unzip *.son file and store contents in CATALOG_FOLDER/services/<service_uuid>/
144 """
145 with zipfile.ZipFile(self.package_file_path, "r") as z:
146 z.extractall(self.package_content_path)
147
148 def _load_package_descriptor(self):
149 """
150 Load the main package descriptor YAML and keep it as dict.
151 :return:
152 """
153 self.manifest = load_yaml(
154 os.path.join(
155 self.package_content_path, "META-INF/MANIFEST.MF"))
156
157 def _load_nsd(self):
158 """
159 Load the entry NSD YAML and keep it as dict.
160 :return:
161 """
162 if "entry_service_template" in self.manifest:
163 nsd_path = os.path.join(
164 self.package_content_path,
165 make_relative_path(self.manifest.get("entry_service_template")))
166 self.nsd = load_yaml(nsd_path)
167 LOG.debug("Loaded NSD: %r" % self.nsd.get("ns_name"))
168
169 def _load_vnfd(self):
170 """
171 Load all VNFD YAML files referenced in MANIFEST.MF and keep them in dict.
172 :return:
173 """
174 if "package_content" in self.manifest:
175 for pc in self.manifest.get("package_content"):
176 if pc.get("content-type") == "application/sonata.function_descriptor":
177 vnfd_path = os.path.join(
178 self.package_content_path,
179 make_relative_path(pc.get("name")))
180 vnfd = load_yaml(vnfd_path)
181 self.vnfds[vnfd.get("vnf_name")] = vnfd
182 LOG.debug("Loaded VNFD: %r" % vnfd.get("vnf_name"))
183
184 def _load_docker_files(self):
185 """
186 Get all paths to Dockerfiles from VNFDs and store them in dict.
187 :return:
188 """
189 for k, v in self.vnfds.iteritems():
190 for vu in v.get("virtual_deployment_units"):
191 if vu.get("vm_image_format") == "docker":
192 vm_image = vu.get("vm_image")
193 docker_path = os.path.join(
194 self.package_content_path,
195 make_relative_path(vm_image))
196 self.local_docker_files[k] = docker_path
197 LOG.debug("Found Dockerfile: %r" % docker_path)
198
199 def _build_images_from_dockerfiles(self):
200 """
201 Build Docker images for each local Dockerfile found in the package: self.local_docker_files
202 """
203 if GK_STANDALONE_MODE:
204 return # do not build anything in standalone mode
205 dc = DockerClient()
206 LOG.info("Building %d Docker images (this may take several minutes) ..." % len(self.local_docker_files))
207 for k, v in self.local_docker_files.iteritems():
208 for line in dc.build(path=v.replace("Dockerfile", ""), tag=k, rm=False, nocache=False):
209 LOG.debug("DOCKER BUILD: %s" % line)
210 LOG.info("Docker image created: %s" % k)
211
212 def _download_predefined_dockerimages(self):
213 """
214 If the package contains URLs to pre-build Docker images, we download them with this method.
215 """
216 # TODO implement this if we want to be able to download docker images instead of building them
217 pass
218
219 def _check_docker_image_exists(self, image_name):
220 """
221 Query the docker service and check if the given image exists
222 :param image_name: name of the docker image
223 :return:
224 """
225 return len(DockerClient().images(image_name)) > 0
226
227 def _calculate_placement(self, algorithm):
228 """
229 Do placement by adding the a field "dc" to
230 each VNFD that points to one of our
231 data center objects known to the gatekeeper.
232 """
233 assert(len(self.vnfds) > 0)
234 assert(len(GK.dcs) > 0)
235 # instantiate algorithm an place
236 p = algorithm()
237 p.place(self.nsd, self.vnfds, GK.dcs)
238 LOG.info("Using placement algorithm: %r" % p.__class__.__name__)
239 # lets print the placement result
240 for name, vnfd in self.vnfds.iteritems():
241 LOG.info("Placed VNF %r on DC %r" % (name, str(vnfd.get("dc"))))
242
243
244 """
245 Some (simple) placement algorithms
246 """
247
248
249 class FirstDcPlacement(object):
250 """
251 Placement: Always use one and the same data center from the GK.dcs dict.
252 """
253 def place(self, nsd, vnfds, dcs):
254 for name, vnfd in vnfds.iteritems():
255 vnfd["dc"] = list(dcs.itervalues())[0]
256
257
258 """
259 Resource definitions and API endpoints
260 """
261
262
263 class Packages(fr.Resource):
264
265 def post(self):
266 """
267 Upload a *.son service package to the dummy gatekeeper.
268
269 We expect request with a *.son file and store it in UPLOAD_FOLDER
270 :return: UUID
271 """
272 try:
273 # get file contents
274 son_file = request.files['file']
275 # generate a uuid to reference this package
276 service_uuid = str(uuid.uuid4())
277 file_hash = hashlib.sha1(str(son_file)).hexdigest()
278 # ensure that upload folder exists
279 ensure_dir(UPLOAD_FOLDER)
280 upload_path = os.path.join(UPLOAD_FOLDER, "%s.son" % service_uuid)
281 # store *.son file to disk
282 son_file.save(upload_path)
283 size = os.path.getsize(upload_path)
284 # create a service object and register it
285 s = Service(service_uuid, file_hash, upload_path)
286 GK.register_service_package(service_uuid, s)
287 # generate the JSON result
288 return {"service_uuid": service_uuid, "size": size, "sha1": file_hash, "error": None}
289 except Exception as ex:
290 LOG.exception("Service package upload failed:")
291 return {"service_uuid": None, "size": 0, "sha1": None, "error": "upload failed"}
292
293 def get(self):
294 """
295 Return a list of UUID's of uploaded service packages.
296 :return: dict/list
297 """
298 return {"service_uuid_list": list(GK.services.iterkeys())}
299
300
301 class Instantiations(fr.Resource):
302
303 def post(self):
304 """
305 Instantiate a service specified by its UUID.
306 Will return a new UUID to identify the running service instance.
307 :return: UUID
308 """
309 # try to extract the service uuid from the request
310 json_data = request.get_json(force=True)
311 service_uuid = json_data.get("service_uuid")
312
313 # lets be a bit fuzzy here to make testing easier
314 if service_uuid is None and len(GK.services) > 0:
315 # if we don't get a service uuid, we simple start the first service in the list
316 service_uuid = list(GK.services.iterkeys())[0]
317
318 if service_uuid in GK.services:
319 # ok, we have a service uuid, lets start the service
320 service_instance_uuid = GK.services.get(service_uuid).start_service()
321 return {"service_instance_uuid": service_instance_uuid}
322 return "Service not found", 404
323
324 def get(self):
325 """
326 Returns a list of UUIDs containing all running services.
327 :return: dict / list
328 """
329 return {"service_instance_list": [
330 list(s.instances.iterkeys()) for s in GK.services.itervalues()]}
331
332
333 # create a single, global GK object
334 GK = Gatekeeper()
335 # setup Flask
336 app = Flask(__name__)
337 app.config['MAX_CONTENT_LENGTH'] = 512 * 1024 * 1024 # 512 MB max upload
338 api = fr.Api(app)
339 # define endpoints
340 api.add_resource(Packages, '/api/packages')
341 api.add_resource(Instantiations, '/api/instantiations')
342
343
344 def start_rest_api(host, port, datacenters=dict()):
345 GK.dcs = datacenters
346 # start the Flask server (not the best performance but ok for our use case)
347 app.run(host=host,
348 port=port,
349 debug=True,
350 use_reloader=False # this is needed to run Flask in a non-main thread
351 )
352
353
354 def ensure_dir(name):
355 if not os.path.exists(name):
356 os.makedirs(name)
357
358
359 def load_yaml(path):
360 with open(path, "r") as f:
361 try:
362 r = yaml.load(f)
363 except yaml.YAMLError as exc:
364 LOG.exception("YAML parse error")
365 r = dict()
366 return r
367
368
369 def make_relative_path(path):
370 if path.startswith("file://"):
371 path = path.replace("file://", "", 1)
372 if path.startswith("/"):
373 path = path.replace("/", "", 1)
374 return path
375
376
377 if __name__ == '__main__':
378 """
379 Lets allow to run the API in standalone mode.
380 """
381 GK_STANDALONE_MODE = True
382 logging.getLogger("werkzeug").setLevel(logging.INFO)
383 start_rest_api("0.0.0.0", 8000)
384