Merge pull request #69 from wtaverni/master
[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 print(request.files)
275 son_file = request.files['file']
276 # generate a uuid to reference this package
277 service_uuid = str(uuid.uuid4())
278 file_hash = hashlib.sha1(str(son_file)).hexdigest()
279 # ensure that upload folder exists
280 ensure_dir(UPLOAD_FOLDER)
281 upload_path = os.path.join(UPLOAD_FOLDER, "%s.son" % service_uuid)
282 # store *.son file to disk
283 son_file.save(upload_path)
284 size = os.path.getsize(upload_path)
285 # create a service object and register it
286 s = Service(service_uuid, file_hash, upload_path)
287 GK.register_service_package(service_uuid, s)
288 # generate the JSON result
289 return {"service_uuid": service_uuid, "size": size, "sha1": file_hash, "error": None}
290 except Exception as ex:
291 LOG.exception("Service package upload failed:")
292 return {"service_uuid": None, "size": 0, "sha1": None, "error": "upload failed"}
293
294 def get(self):
295 """
296 Return a list of UUID's of uploaded service packages.
297 :return: dict/list
298 """
299 return {"service_uuid_list": list(GK.services.iterkeys())}
300
301
302 class Instantiations(fr.Resource):
303
304 def post(self):
305 """
306 Instantiate a service specified by its UUID.
307 Will return a new UUID to identify the running service instance.
308 :return: UUID
309 """
310 # try to extract the service uuid from the request
311 json_data = request.get_json(force=True)
312 service_uuid = json_data.get("service_uuid")
313
314 # lets be a bit fuzzy here to make testing easier
315 if service_uuid is None and len(GK.services) > 0:
316 # if we don't get a service uuid, we simple start the first service in the list
317 service_uuid = list(GK.services.iterkeys())[0]
318
319 if service_uuid in GK.services:
320 # ok, we have a service uuid, lets start the service
321 service_instance_uuid = GK.services.get(service_uuid).start_service()
322 return {"service_instance_uuid": service_instance_uuid}
323 return "Service not found", 404
324
325 def get(self):
326 """
327 Returns a list of UUIDs containing all running services.
328 :return: dict / list
329 """
330 return {"service_instance_list": [
331 list(s.instances.iterkeys()) for s in GK.services.itervalues()]}
332
333
334 # create a single, global GK object
335 GK = Gatekeeper()
336 # setup Flask
337 app = Flask(__name__)
338 app.config['MAX_CONTENT_LENGTH'] = 512 * 1024 * 1024 # 512 MB max upload
339 api = fr.Api(app)
340 # define endpoints
341 api.add_resource(Packages, '/api/packages')
342 api.add_resource(Instantiations, '/api/instantiations')
343
344
345 def start_rest_api(host, port, datacenters=dict()):
346 GK.dcs = datacenters
347 # start the Flask server (not the best performance but ok for our use case)
348 app.run(host=host,
349 port=port,
350 debug=True,
351 use_reloader=False # this is needed to run Flask in a non-main thread
352 )
353
354
355 def ensure_dir(name):
356 if not os.path.exists(name):
357 os.makedirs(name)
358
359
360 def load_yaml(path):
361 with open(path, "r") as f:
362 try:
363 r = yaml.load(f)
364 except yaml.YAMLError as exc:
365 LOG.exception("YAML parse error")
366 r = dict()
367 return r
368
369
370 def make_relative_path(path):
371 if path.startswith("file://"):
372 path = path.replace("file://", "", 1)
373 if path.startswith("/"):
374 path = path.replace("/", "", 1)
375 return path
376
377
378 if __name__ == '__main__':
379 """
380 Lets allow to run the API in standalone mode.
381 """
382 GK_STANDALONE_MODE = True
383 logging.getLogger("werkzeug").setLevel(logging.INFO)
384 start_rest_api("0.0.0.0", 8000)
385