fix 1107 allow getting single file package with text/plain
[osm/NBI.git] / osm_nbi / descriptor_topics.py
1 # -*- coding: utf-8 -*-
2
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12 # implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 import tarfile
17 import yaml
18 import json
19 # import logging
20 from hashlib import md5
21 from osm_common.dbbase import DbException, deep_update_rfc7396
22 from http import HTTPStatus
23 from time import time
24 from uuid import uuid4
25 from re import fullmatch
26 from osm_nbi.validation import ValidationError, pdu_new_schema, pdu_edit_schema, \
27 validate_input, vnfpkgop_new_schema
28 from osm_nbi.base_topic import BaseTopic, EngineException, get_iterable
29 from osm_im.vnfd import vnfd as vnfd_im
30 from osm_im.nsd import nsd as nsd_im
31 from osm_im.nst import nst as nst_im
32 from pyangbind.lib.serialise import pybindJSONDecoder
33 import pyangbind.lib.pybindJSON as pybindJSON
34
35 __author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
36
37
38 class DescriptorTopic(BaseTopic):
39
40 def __init__(self, db, fs, msg, auth):
41 BaseTopic.__init__(self, db, fs, msg, auth)
42
43 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
44 super().check_conflict_on_edit(session, final_content, edit_content, _id)
45
46 def _check_unique_id_name(descriptor, position=""):
47 for desc_key, desc_item in descriptor.items():
48 if isinstance(desc_item, list) and desc_item:
49 used_ids = []
50 desc_item_id = None
51 for index, list_item in enumerate(desc_item):
52 if isinstance(list_item, dict):
53 _check_unique_id_name(list_item, "{}.{}[{}]"
54 .format(position, desc_key, index))
55 # Base case
56 if index == 0 and (list_item.get("id") or list_item.get("name")):
57 desc_item_id = "id" if list_item.get("id") else "name"
58 if desc_item_id and list_item.get(desc_item_id):
59 if list_item[desc_item_id] in used_ids:
60 position = "{}.{}[{}]".format(position, desc_key, index)
61 raise EngineException("Error: identifier {} '{}' is not unique and repeats at '{}'"
62 .format(desc_item_id, list_item[desc_item_id],
63 position), HTTPStatus.UNPROCESSABLE_ENTITY)
64 used_ids.append(list_item[desc_item_id])
65 _check_unique_id_name(final_content)
66 # 1. validate again with pyangbind
67 # 1.1. remove internal keys
68 internal_keys = {}
69 for k in ("_id", "_admin"):
70 if k in final_content:
71 internal_keys[k] = final_content.pop(k)
72 storage_params = internal_keys["_admin"].get("storage")
73 serialized = self._validate_input_new(final_content, storage_params, session["force"])
74 # 1.2. modify final_content with a serialized version
75 final_content.clear()
76 final_content.update(serialized)
77 # 1.3. restore internal keys
78 for k, v in internal_keys.items():
79 final_content[k] = v
80
81 if session["force"]:
82 return
83 # 2. check that this id is not present
84 if "id" in edit_content:
85 _filter = self._get_project_filter(session)
86 _filter["id"] = final_content["id"]
87 _filter["_id.neq"] = _id
88 if self.db.get_one(self.topic, _filter, fail_on_empty=False):
89 raise EngineException("{} with id '{}' already exists for this project".format(self.topic[:-1],
90 final_content["id"]),
91 HTTPStatus.CONFLICT)
92
93 @staticmethod
94 def format_on_new(content, project_id=None, make_public=False):
95 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
96 content["_admin"]["onboardingState"] = "CREATED"
97 content["_admin"]["operationalState"] = "DISABLED"
98 content["_admin"]["usageState"] = "NOT_IN_USE"
99
100 def delete_extra(self, session, _id, db_content, not_send_msg=None):
101 """
102 Deletes file system storage associated with the descriptor
103 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
104 :param _id: server internal id
105 :param db_content: The database content of the descriptor
106 :param not_send_msg: To not send message (False) or store content (list) instead
107 :return: None if ok or raises EngineException with the problem
108 """
109 self.fs.file_delete(_id, ignore_non_exist=True)
110 self.fs.file_delete(_id + "_", ignore_non_exist=True) # remove temp folder
111
112 @staticmethod
113 def get_one_by_id(db, session, topic, id):
114 # find owned by this project
115 _filter = BaseTopic._get_project_filter(session)
116 _filter["id"] = id
117 desc_list = db.get_list(topic, _filter)
118 if len(desc_list) == 1:
119 return desc_list[0]
120 elif len(desc_list) > 1:
121 raise DbException("Found more than one {} with id='{}' belonging to this project".format(topic[:-1], id),
122 HTTPStatus.CONFLICT)
123
124 # not found any: try to find public
125 _filter = BaseTopic._get_project_filter(session)
126 _filter["id"] = id
127 desc_list = db.get_list(topic, _filter)
128 if not desc_list:
129 raise DbException("Not found any {} with id='{}'".format(topic[:-1], id), HTTPStatus.NOT_FOUND)
130 elif len(desc_list) == 1:
131 return desc_list[0]
132 else:
133 raise DbException("Found more than one public {} with id='{}'; and no one belonging to this project".format(
134 topic[:-1], id), HTTPStatus.CONFLICT)
135
136 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
137 """
138 Creates a new almost empty DISABLED entry into database. Due to SOL005, it does not follow normal procedure.
139 Creating a VNFD or NSD is done in two steps: 1. Creates an empty descriptor (this step) and 2) upload content
140 (self.upload_content)
141 :param rollback: list to append created items at database in case a rollback may to be done
142 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
143 :param indata: data to be inserted
144 :param kwargs: used to override the indata descriptor
145 :param headers: http request headers
146 :return: _id, None: identity of the inserted data; and None as there is not any operation
147 """
148
149 # No needed to capture exceptions
150 # Check Quota
151 self.check_quota(session)
152
153 # _remove_envelop
154 if indata:
155 if "userDefinedData" in indata:
156 indata = indata['userDefinedData']
157
158 # Override descriptor with query string kwargs
159 self._update_input_with_kwargs(indata, kwargs)
160 # uncomment when this method is implemented.
161 # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors
162 # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"])
163
164 content = {"_admin": {"userDefinedData": indata}}
165 self.format_on_new(content, session["project_id"], make_public=session["public"])
166 _id = self.db.create(self.topic, content)
167 rollback.append({"topic": self.topic, "_id": _id})
168 self._send_msg("created", {"_id": _id})
169 return _id, None
170
171 def upload_content(self, session, _id, indata, kwargs, headers):
172 """
173 Used for receiving content by chunks (with a transaction_id header and/or gzip file. It will store and extract)
174 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
175 :param _id : the nsd,vnfd is already created, this is the id
176 :param indata: http body request
177 :param kwargs: user query string to override parameters. NOT USED
178 :param headers: http request headers
179 :return: True if package is completely uploaded or False if partial content has been uploded
180 Raise exception on error
181 """
182 # Check that _id exists and it is valid
183 current_desc = self.show(session, _id)
184
185 content_range_text = headers.get("Content-Range")
186 expected_md5 = headers.get("Content-File-MD5")
187 compressed = None
188 content_type = headers.get("Content-Type")
189 if content_type and "application/gzip" in content_type or "application/x-gzip" in content_type or \
190 "application/zip" in content_type:
191 compressed = "gzip"
192 filename = headers.get("Content-Filename")
193 if not filename:
194 filename = "package.tar.gz" if compressed else "package"
195 # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266
196 file_pkg = None
197 error_text = ""
198 try:
199 if content_range_text:
200 content_range = content_range_text.replace("-", " ").replace("/", " ").split()
201 if content_range[0] != "bytes": # TODO check x<y not negative < total....
202 raise IndexError()
203 start = int(content_range[1])
204 end = int(content_range[2]) + 1
205 total = int(content_range[3])
206 else:
207 start = 0
208 temp_folder = _id + "_" # all the content is upload here and if ok, it is rename from id_ to is folder
209
210 if start:
211 if not self.fs.file_exists(temp_folder, 'dir'):
212 raise EngineException("invalid Transaction-Id header", HTTPStatus.NOT_FOUND)
213 else:
214 self.fs.file_delete(temp_folder, ignore_non_exist=True)
215 self.fs.mkdir(temp_folder)
216
217 storage = self.fs.get_params()
218 storage["folder"] = _id
219
220 file_path = (temp_folder, filename)
221 if self.fs.file_exists(file_path, 'file'):
222 file_size = self.fs.file_size(file_path)
223 else:
224 file_size = 0
225 if file_size != start:
226 raise EngineException("invalid Content-Range start sequence, expected '{}' but received '{}'".format(
227 file_size, start), HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
228 file_pkg = self.fs.file_open(file_path, 'a+b')
229 if isinstance(indata, dict):
230 indata_text = yaml.safe_dump(indata, indent=4, default_flow_style=False)
231 file_pkg.write(indata_text.encode(encoding="utf-8"))
232 else:
233 indata_len = 0
234 while True:
235 indata_text = indata.read(4096)
236 indata_len += len(indata_text)
237 if not indata_text:
238 break
239 file_pkg.write(indata_text)
240 if content_range_text:
241 if indata_len != end-start:
242 raise EngineException("Mismatch between Content-Range header {}-{} and body length of {}".format(
243 start, end-1, indata_len), HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
244 if end != total:
245 # TODO update to UPLOADING
246 return False
247
248 # PACKAGE UPLOADED
249 if expected_md5:
250 file_pkg.seek(0, 0)
251 file_md5 = md5()
252 chunk_data = file_pkg.read(1024)
253 while chunk_data:
254 file_md5.update(chunk_data)
255 chunk_data = file_pkg.read(1024)
256 if expected_md5 != file_md5.hexdigest():
257 raise EngineException("Error, MD5 mismatch", HTTPStatus.CONFLICT)
258 file_pkg.seek(0, 0)
259 if compressed == "gzip":
260 tar = tarfile.open(mode='r', fileobj=file_pkg)
261 descriptor_file_name = None
262 for tarinfo in tar:
263 tarname = tarinfo.name
264 tarname_path = tarname.split("/")
265 if not tarname_path[0] or ".." in tarname_path: # if start with "/" means absolute path
266 raise EngineException("Absolute path or '..' are not allowed for package descriptor tar.gz")
267 if len(tarname_path) == 1 and not tarinfo.isdir():
268 raise EngineException("All files must be inside a dir for package descriptor tar.gz")
269 if tarname.endswith(".yaml") or tarname.endswith(".json") or tarname.endswith(".yml"):
270 storage["pkg-dir"] = tarname_path[0]
271 if len(tarname_path) == 2:
272 if descriptor_file_name:
273 raise EngineException(
274 "Found more than one descriptor file at package descriptor tar.gz")
275 descriptor_file_name = tarname
276 if not descriptor_file_name:
277 raise EngineException("Not found any descriptor file at package descriptor tar.gz")
278 storage["descriptor"] = descriptor_file_name
279 storage["zipfile"] = filename
280 self.fs.file_extract(tar, temp_folder)
281 with self.fs.file_open((temp_folder, descriptor_file_name), "r") as descriptor_file:
282 content = descriptor_file.read()
283 else:
284 content = file_pkg.read()
285 storage["descriptor"] = descriptor_file_name = filename
286
287 if descriptor_file_name.endswith(".json"):
288 error_text = "Invalid json format "
289 indata = json.load(content)
290 else:
291 error_text = "Invalid yaml format "
292 indata = yaml.load(content, Loader=yaml.SafeLoader)
293
294 current_desc["_admin"]["storage"] = storage
295 current_desc["_admin"]["onboardingState"] = "ONBOARDED"
296 current_desc["_admin"]["operationalState"] = "ENABLED"
297
298 indata = self._remove_envelop(indata)
299
300 # Override descriptor with query string kwargs
301 if kwargs:
302 self._update_input_with_kwargs(indata, kwargs)
303 # it will call overrides method at VnfdTopic or NsdTopic
304 # indata = self._validate_input_edit(indata, force=session["force"])
305
306 deep_update_rfc7396(current_desc, indata)
307 self.check_conflict_on_edit(session, current_desc, indata, _id=_id)
308 current_desc["_admin"]["modified"] = time()
309 self.db.replace(self.topic, _id, current_desc)
310 self.fs.dir_rename(temp_folder, _id)
311
312 indata["_id"] = _id
313 self._send_msg("edited", indata)
314
315 # TODO if descriptor has changed because kwargs update content and remove cached zip
316 # TODO if zip is not present creates one
317 return True
318
319 except EngineException:
320 raise
321 except IndexError:
322 raise EngineException("invalid Content-Range header format. Expected 'bytes start-end/total'",
323 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
324 except IOError as e:
325 raise EngineException("invalid upload transaction sequence: '{}'".format(e), HTTPStatus.BAD_REQUEST)
326 except tarfile.ReadError as e:
327 raise EngineException("invalid file content {}".format(e), HTTPStatus.BAD_REQUEST)
328 except (ValueError, yaml.YAMLError) as e:
329 raise EngineException(error_text + str(e))
330 except ValidationError as e:
331 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
332 finally:
333 if file_pkg:
334 file_pkg.close()
335
336 def get_file(self, session, _id, path=None, accept_header=None):
337 """
338 Return the file content of a vnfd or nsd
339 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
340 :param _id: Identity of the vnfd, nsd
341 :param path: artifact path or "$DESCRIPTOR" or None
342 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
343 :return: opened file plus Accept format or raises an exception
344 """
345 accept_text = accept_zip = False
346 if accept_header:
347 if 'text/plain' in accept_header or '*/*' in accept_header:
348 accept_text = True
349 if 'application/zip' in accept_header or '*/*' in accept_header:
350 accept_zip = 'application/zip'
351 elif 'application/gzip' in accept_header:
352 accept_zip = 'application/gzip'
353
354 if not accept_text and not accept_zip:
355 raise EngineException("provide request header 'Accept' with 'application/zip' or 'text/plain'",
356 http_code=HTTPStatus.NOT_ACCEPTABLE)
357
358 content = self.show(session, _id)
359 if content["_admin"]["onboardingState"] != "ONBOARDED":
360 raise EngineException("Cannot get content because this resource is not at 'ONBOARDED' state. "
361 "onboardingState is {}".format(content["_admin"]["onboardingState"]),
362 http_code=HTTPStatus.CONFLICT)
363 storage = content["_admin"]["storage"]
364 if path is not None and path != "$DESCRIPTOR": # artifacts
365 if not storage.get('pkg-dir'):
366 raise EngineException("Packages does not contains artifacts", http_code=HTTPStatus.BAD_REQUEST)
367 if self.fs.file_exists((storage['folder'], storage['pkg-dir'], *path), 'dir'):
368 folder_content = self.fs.dir_ls((storage['folder'], storage['pkg-dir'], *path))
369 return folder_content, "text/plain"
370 # TODO manage folders in http
371 else:
372 return self.fs.file_open((storage['folder'], storage['pkg-dir'], *path), "rb"),\
373 "application/octet-stream"
374
375 # pkgtype accept ZIP TEXT -> result
376 # manyfiles yes X -> zip
377 # no yes -> error
378 # onefile yes no -> zip
379 # X yes -> text
380 contain_many_files = False
381 if storage.get('pkg-dir'):
382 # check if there are more than one file in the package, ignoring checksums.txt.
383 pkg_files = self.fs.dir_ls((storage['folder'], storage['pkg-dir']))
384 if len(pkg_files) >= 3 or (len(pkg_files) == 2 and 'checksums.txt' not in pkg_files):
385 contain_many_files = True
386 if accept_text and (not contain_many_files or path == "$DESCRIPTOR"):
387 return self.fs.file_open((storage['folder'], storage['descriptor']), "r"), "text/plain"
388 elif contain_many_files and not accept_zip:
389 raise EngineException("Packages that contains several files need to be retrieved with 'application/zip'"
390 "Accept header", http_code=HTTPStatus.NOT_ACCEPTABLE)
391 else:
392 if not storage.get('zipfile'):
393 # TODO generate zipfile if not present
394 raise EngineException("Only allowed 'text/plain' Accept header for this descriptor. To be solved in "
395 "future versions", http_code=HTTPStatus.NOT_ACCEPTABLE)
396 return self.fs.file_open((storage['folder'], storage['zipfile']), "rb"), accept_zip
397
398 def pyangbind_validation(self, item, data, force=False):
399 try:
400 if item == "vnfds":
401 myvnfd = vnfd_im()
402 pybindJSONDecoder.load_ietf_json({'vnfd:vnfd-catalog': {'vnfd': [data]}}, None, None, obj=myvnfd,
403 path_helper=True, skip_unknown=force)
404 out = pybindJSON.dumps(myvnfd, mode="ietf")
405 elif item == "nsds":
406 mynsd = nsd_im()
407 pybindJSONDecoder.load_ietf_json({'nsd:nsd-catalog': {'nsd': [data]}}, None, None, obj=mynsd,
408 path_helper=True, skip_unknown=force)
409 out = pybindJSON.dumps(mynsd, mode="ietf")
410 elif item == "nsts":
411 mynst = nst_im()
412 pybindJSONDecoder.load_ietf_json({'nst': [data]}, None, None, obj=mynst,
413 path_helper=True, skip_unknown=force)
414 out = pybindJSON.dumps(mynst, mode="ietf")
415 else:
416 raise EngineException("Not possible to validate '{}' item".format(item),
417 http_code=HTTPStatus.INTERNAL_SERVER_ERROR)
418
419 desc_out = self._remove_envelop(yaml.safe_load(out))
420 return desc_out
421
422 except Exception as e:
423 raise EngineException("Error in pyangbind validation: {}".format(str(e)),
424 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
425
426 def _validate_input_edit(self, indata, content, force=False):
427 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
428 if "_id" in indata:
429 indata.pop("_id")
430 if "_admin" not in indata:
431 indata["_admin"] = {}
432
433 if "operationalState" in indata:
434 if indata["operationalState"] in ("ENABLED", "DISABLED"):
435 indata["_admin"]["operationalState"] = indata.pop("operationalState")
436 else:
437 raise EngineException("State '{}' is not a valid operational state"
438 .format(indata["operationalState"]),
439 http_code=HTTPStatus.BAD_REQUEST)
440
441 # In the case of user defined data, we need to put the data in the root of the object
442 # to preserve current expected behaviour
443 if "userDefinedData" in indata:
444 data = indata.pop("userDefinedData")
445 if type(data) == dict:
446 indata["_admin"]["userDefinedData"] = data
447 else:
448 raise EngineException("userDefinedData should be an object, but is '{}' instead"
449 .format(type(data)),
450 http_code=HTTPStatus.BAD_REQUEST)
451
452 if ("operationalState" in indata["_admin"] and
453 content["_admin"]["operationalState"] == indata["_admin"]["operationalState"]):
454 raise EngineException("operationalState already {}".format(content["_admin"]["operationalState"]),
455 http_code=HTTPStatus.CONFLICT)
456
457 return indata
458
459
460 class VnfdTopic(DescriptorTopic):
461 topic = "vnfds"
462 topic_msg = "vnfd"
463
464 def __init__(self, db, fs, msg, auth):
465 DescriptorTopic.__init__(self, db, fs, msg, auth)
466
467 @staticmethod
468 def _remove_envelop(indata=None):
469 if not indata:
470 return {}
471 clean_indata = indata
472 if clean_indata.get('vnfd:vnfd-catalog'):
473 clean_indata = clean_indata['vnfd:vnfd-catalog']
474 elif clean_indata.get('vnfd-catalog'):
475 clean_indata = clean_indata['vnfd-catalog']
476 if clean_indata.get('vnfd'):
477 if not isinstance(clean_indata['vnfd'], list) or len(clean_indata['vnfd']) != 1:
478 raise EngineException("'vnfd' must be a list of only one element")
479 clean_indata = clean_indata['vnfd'][0]
480 elif clean_indata.get('vnfd:vnfd'):
481 if not isinstance(clean_indata['vnfd:vnfd'], list) or len(clean_indata['vnfd:vnfd']) != 1:
482 raise EngineException("'vnfd:vnfd' must be a list of only one element")
483 clean_indata = clean_indata['vnfd:vnfd'][0]
484 return clean_indata
485
486 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
487 super().check_conflict_on_edit(session, final_content, edit_content, _id)
488
489 # set type of vnfd
490 contains_pdu = False
491 contains_vdu = False
492 for vdu in get_iterable(final_content.get("vdu")):
493 if vdu.get("pdu-type"):
494 contains_pdu = True
495 else:
496 contains_vdu = True
497 if contains_pdu:
498 final_content["_admin"]["type"] = "hnfd" if contains_vdu else "pnfd"
499 elif contains_vdu:
500 final_content["_admin"]["type"] = "vnfd"
501 # if neither vud nor pdu do not fill type
502
503 def check_conflict_on_del(self, session, _id, db_content):
504 """
505 Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note
506 that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr
507 that uses this vnfd
508 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
509 :param _id: vnfd internal id
510 :param db_content: The database content of the _id.
511 :return: None or raises EngineException with the conflict
512 """
513 if session["force"]:
514 return
515 descriptor = db_content
516 descriptor_id = descriptor.get("id")
517 if not descriptor_id: # empty vnfd not uploaded
518 return
519
520 _filter = self._get_project_filter(session)
521
522 # check vnfrs using this vnfd
523 _filter["vnfd-id"] = _id
524 if self.db.get_list("vnfrs", _filter):
525 raise EngineException("There is at least one VNF using this descriptor", http_code=HTTPStatus.CONFLICT)
526
527 # check NSD referencing this VNFD
528 del _filter["vnfd-id"]
529 _filter["constituent-vnfd.ANYINDEX.vnfd-id-ref"] = descriptor_id
530 if self.db.get_list("nsds", _filter):
531 raise EngineException("There is at least one NSD referencing this descriptor",
532 http_code=HTTPStatus.CONFLICT)
533
534 def _validate_input_new(self, indata, storage_params, force=False):
535 indata.pop("onboardingState", None)
536 indata.pop("operationalState", None)
537 indata.pop("usageState", None)
538
539 indata.pop("links", None)
540
541 indata = self.pyangbind_validation("vnfds", indata, force)
542 # Cross references validation in the descriptor
543 if indata.get("vdu"):
544 if not indata.get("mgmt-interface"):
545 raise EngineException("'mgmt-interface' is a mandatory field and it is not defined",
546 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
547 if indata["mgmt-interface"].get("cp"):
548 for cp in get_iterable(indata.get("connection-point")):
549 if cp["name"] == indata["mgmt-interface"]["cp"]:
550 break
551 else:
552 raise EngineException("mgmt-interface:cp='{}' must match an existing connection-point"
553 .format(indata["mgmt-interface"]["cp"]),
554 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
555
556 for vdu in get_iterable(indata.get("vdu")):
557 icp_refs = []
558 ecp_refs = []
559 for interface in get_iterable(vdu.get("interface")):
560 if interface.get("external-connection-point-ref"):
561 if interface.get("external-connection-point-ref") in ecp_refs:
562 raise EngineException("vdu[id='{}']:interface[name='{}']:external-connection-point-ref='{}' "
563 "is referenced by other interface"
564 .format(vdu["id"], interface["name"],
565 interface["external-connection-point-ref"]),
566 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
567 ecp_refs.append(interface.get("external-connection-point-ref"))
568 for cp in get_iterable(indata.get("connection-point")):
569 if cp["name"] == interface["external-connection-point-ref"]:
570 break
571 else:
572 raise EngineException("vdu[id='{}']:interface[name='{}']:external-connection-point-ref='{}' "
573 "must match an existing connection-point"
574 .format(vdu["id"], interface["name"],
575 interface["external-connection-point-ref"]),
576 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
577 elif interface.get("internal-connection-point-ref"):
578 if interface.get("internal-connection-point-ref") in icp_refs:
579 raise EngineException("vdu[id='{}']:interface[name='{}']:internal-connection-point-ref='{}' "
580 "is referenced by other interface"
581 .format(vdu["id"], interface["name"],
582 interface["internal-connection-point-ref"]),
583 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
584 icp_refs.append(interface.get("internal-connection-point-ref"))
585 for internal_cp in get_iterable(vdu.get("internal-connection-point")):
586 if interface["internal-connection-point-ref"] == internal_cp.get("id"):
587 break
588 else:
589 raise EngineException("vdu[id='{}']:interface[name='{}']:internal-connection-point-ref='{}' "
590 "must match an existing vdu:internal-connection-point"
591 .format(vdu["id"], interface["name"],
592 interface["internal-connection-point-ref"]),
593 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
594 # Validate that if descriptor contains charms, artifacts _admin.storage."pkg-dir" is not none
595 if vdu.get("vdu-configuration"):
596 if vdu["vdu-configuration"].get("juju"):
597 if not self._validate_package_folders(storage_params, 'charms'):
598 raise EngineException("Charm defined in vnf[id={}]:vdu[id={}] but not present in "
599 "package".format(indata["id"], vdu["id"]))
600 # Validate that if descriptor contains cloud-init, artifacts _admin.storage."pkg-dir" is not none
601 if vdu.get("cloud-init-file"):
602 if not self._validate_package_folders(storage_params, 'cloud_init', vdu["cloud-init-file"]):
603 raise EngineException("Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
604 "package".format(indata["id"], vdu["id"]))
605 # Validate that if descriptor contains charms, artifacts _admin.storage."pkg-dir" is not none
606 if indata.get("vnf-configuration"):
607 if indata["vnf-configuration"].get("juju"):
608 if not self._validate_package_folders(storage_params, 'charms'):
609 raise EngineException("Charm defined in vnf[id={}] but not present in "
610 "package".format(indata["id"]))
611 vld_names = [] # For detection of duplicated VLD names
612 for ivld in get_iterable(indata.get("internal-vld")):
613 # BEGIN Detection of duplicated VLD names
614 ivld_name = ivld.get("name")
615 if ivld_name:
616 if ivld_name in vld_names:
617 raise EngineException("Duplicated VLD name '{}' in vnfd[id={}]:internal-vld[id={}]"
618 .format(ivld["name"], indata["id"], ivld["id"]),
619 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
620 else:
621 vld_names.append(ivld_name)
622 # END Detection of duplicated VLD names
623 for icp in get_iterable(ivld.get("internal-connection-point")):
624 icp_mark = False
625 for vdu in get_iterable(indata.get("vdu")):
626 for internal_cp in get_iterable(vdu.get("internal-connection-point")):
627 if icp["id-ref"] == internal_cp["id"]:
628 icp_mark = True
629 break
630 if icp_mark:
631 break
632 else:
633 raise EngineException("internal-vld[id='{}']:internal-connection-point='{}' must match an existing "
634 "vdu:internal-connection-point".format(ivld["id"], icp["id-ref"]),
635 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
636 if ivld.get("ip-profile-ref"):
637 for ip_prof in get_iterable(indata.get("ip-profiles")):
638 if ip_prof["name"] == get_iterable(ivld.get("ip-profile-ref")):
639 break
640 else:
641 raise EngineException("internal-vld[id='{}']:ip-profile-ref='{}' does not exist".format(
642 ivld["id"], ivld["ip-profile-ref"]),
643 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
644 for mp in get_iterable(indata.get("monitoring-param")):
645 if mp.get("vdu-monitoring-param"):
646 mp_vmp_mark = False
647 for vdu in get_iterable(indata.get("vdu")):
648 for vmp in get_iterable(vdu.get("monitoring-param")):
649 if vmp["id"] == mp["vdu-monitoring-param"].get("vdu-monitoring-param-ref") and vdu["id"] ==\
650 mp["vdu-monitoring-param"]["vdu-ref"]:
651 mp_vmp_mark = True
652 break
653 if mp_vmp_mark:
654 break
655 else:
656 raise EngineException("monitoring-param:vdu-monitoring-param:vdu-monitoring-param-ref='{}' not "
657 "defined at vdu[id='{}'] or vdu does not exist"
658 .format(mp["vdu-monitoring-param"]["vdu-monitoring-param-ref"],
659 mp["vdu-monitoring-param"]["vdu-ref"]),
660 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
661 elif mp.get("vdu-metric"):
662 mp_vm_mark = False
663 for vdu in get_iterable(indata.get("vdu")):
664 if vdu.get("vdu-configuration"):
665 for metric in get_iterable(vdu["vdu-configuration"].get("metrics")):
666 if metric["name"] == mp["vdu-metric"]["vdu-metric-name-ref"] and vdu["id"] == \
667 mp["vdu-metric"]["vdu-ref"]:
668 mp_vm_mark = True
669 break
670 if mp_vm_mark:
671 break
672 else:
673 raise EngineException("monitoring-param:vdu-metric:vdu-metric-name-ref='{}' not defined at "
674 "vdu[id='{}'] or vdu does not exist"
675 .format(mp["vdu-metric"]["vdu-metric-name-ref"],
676 mp["vdu-metric"]["vdu-ref"]),
677 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
678
679 for sgd in get_iterable(indata.get("scaling-group-descriptor")):
680 for sp in get_iterable(sgd.get("scaling-policy")):
681 for sc in get_iterable(sp.get("scaling-criteria")):
682 for mp in get_iterable(indata.get("monitoring-param")):
683 if mp["id"] == get_iterable(sc.get("vnf-monitoring-param-ref")):
684 break
685 else:
686 raise EngineException("scaling-group-descriptor[name='{}']:scaling-criteria[name='{}']:"
687 "vnf-monitoring-param-ref='{}' not defined in any monitoring-param"
688 .format(sgd["name"], sc["name"], sc["vnf-monitoring-param-ref"]),
689 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
690 for sgd_vdu in get_iterable(sgd.get("vdu")):
691 sgd_vdu_mark = False
692 for vdu in get_iterable(indata.get("vdu")):
693 if vdu["id"] == sgd_vdu["vdu-id-ref"]:
694 sgd_vdu_mark = True
695 break
696 if sgd_vdu_mark:
697 break
698 else:
699 raise EngineException("scaling-group-descriptor[name='{}']:vdu-id-ref={} does not match any vdu"
700 .format(sgd["name"], sgd_vdu["vdu-id-ref"]),
701 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
702 for sca in get_iterable(sgd.get("scaling-config-action")):
703 if not indata.get("vnf-configuration"):
704 raise EngineException("'vnf-configuration' not defined in the descriptor but it is referenced by "
705 "scaling-group-descriptor[name='{}']:scaling-config-action"
706 .format(sgd["name"]),
707 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
708 for primitive in get_iterable(indata["vnf-configuration"].get("config-primitive")):
709 if primitive["name"] == sca["vnf-config-primitive-name-ref"]:
710 break
711 else:
712 raise EngineException("scaling-group-descriptor[name='{}']:scaling-config-action:vnf-config-"
713 "primitive-name-ref='{}' does not match any "
714 "vnf-configuration:config-primitive:name"
715 .format(sgd["name"], sca["vnf-config-primitive-name-ref"]),
716 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
717 return indata
718
719 def _validate_package_folders(self, storage_params, folder, file=None):
720 if not storage_params or not storage_params.get("pkg-dir"):
721 return False
722 else:
723 if self.fs.file_exists("{}_".format(storage_params["folder"]), 'dir'):
724 f = "{}_/{}/{}".format(storage_params["folder"], storage_params["pkg-dir"], folder)
725 else:
726 f = "{}/{}/{}".format(storage_params["folder"], storage_params["pkg-dir"], folder)
727 if file:
728 return self.fs.file_exists("{}/{}".format(f, file), 'file')
729 else:
730 if self.fs.file_exists(f, 'dir'):
731 if self.fs.dir_ls(f):
732 return True
733 return False
734
735 def delete_extra(self, session, _id, db_content, not_send_msg=None):
736 """
737 Deletes associate file system storage (via super)
738 Deletes associated vnfpkgops from database.
739 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
740 :param _id: server internal id
741 :param db_content: The database content of the descriptor
742 :return: None
743 :raises: FsException in case of error while deleting associated storage
744 """
745 super().delete_extra(session, _id, db_content, not_send_msg)
746 self.db.del_list("vnfpkgops", {"vnfPkgId": _id})
747
748 def sol005_projection(self, data):
749 data["onboardingState"] = data["_admin"]["onboardingState"]
750 data["operationalState"] = data["_admin"]["operationalState"]
751 data["usageState"] = data["_admin"]["usageState"]
752
753 links = {}
754 links["self"] = {"href": "/vnfpkgm/v1/vnf_packages/{}".format(data["_id"])}
755 links["vnfd"] = {"href": "/vnfpkgm/v1/vnf_packages/{}/vnfd".format(data["_id"])}
756 links["packageContent"] = {"href": "/vnfpkgm/v1/vnf_packages/{}/package_content".format(data["_id"])}
757 data["_links"] = links
758
759 return super().sol005_projection(data)
760
761
762 class NsdTopic(DescriptorTopic):
763 topic = "nsds"
764 topic_msg = "nsd"
765
766 def __init__(self, db, fs, msg, auth):
767 DescriptorTopic.__init__(self, db, fs, msg, auth)
768
769 @staticmethod
770 def _remove_envelop(indata=None):
771 if not indata:
772 return {}
773 clean_indata = indata
774
775 if clean_indata.get('nsd:nsd-catalog'):
776 clean_indata = clean_indata['nsd:nsd-catalog']
777 elif clean_indata.get('nsd-catalog'):
778 clean_indata = clean_indata['nsd-catalog']
779 if clean_indata.get('nsd'):
780 if not isinstance(clean_indata['nsd'], list) or len(clean_indata['nsd']) != 1:
781 raise EngineException("'nsd' must be a list of only one element")
782 clean_indata = clean_indata['nsd'][0]
783 elif clean_indata.get('nsd:nsd'):
784 if not isinstance(clean_indata['nsd:nsd'], list) or len(clean_indata['nsd:nsd']) != 1:
785 raise EngineException("'nsd:nsd' must be a list of only one element")
786 clean_indata = clean_indata['nsd:nsd'][0]
787 return clean_indata
788
789 def _validate_input_new(self, indata, storage_params, force=False):
790 indata.pop("nsdOnboardingState", None)
791 indata.pop("nsdOperationalState", None)
792 indata.pop("nsdUsageState", None)
793
794 indata.pop("links", None)
795
796 indata = self.pyangbind_validation("nsds", indata, force)
797 # Cross references validation in the descriptor
798 # TODO validata that if contains cloud-init-file or charms, have artifacts _admin.storage."pkg-dir" is not none
799 for vld in get_iterable(indata.get("vld")):
800 if vld.get("mgmt-network") and vld.get("ip-profile-ref"):
801 raise EngineException("Error at vld[id='{}']:ip-profile-ref"
802 " You cannot set an ip-profile when mgmt-network is True"
803 .format(vld["id"]), http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
804 for vnfd_cp in get_iterable(vld.get("vnfd-connection-point-ref")):
805 for constituent_vnfd in get_iterable(indata.get("constituent-vnfd")):
806 if vnfd_cp["member-vnf-index-ref"] == constituent_vnfd["member-vnf-index"]:
807 if vnfd_cp.get("vnfd-id-ref") and vnfd_cp["vnfd-id-ref"] != constituent_vnfd["vnfd-id-ref"]:
808 raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[vnfd-id-ref='{}'] "
809 "does not match constituent-vnfd[member-vnf-index='{}']:vnfd-id-ref"
810 " '{}'".format(vld["id"], vnfd_cp["vnfd-id-ref"],
811 constituent_vnfd["member-vnf-index"],
812 constituent_vnfd["vnfd-id-ref"]),
813 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
814 break
815 else:
816 raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}'] "
817 "does not match any constituent-vnfd:member-vnf-index"
818 .format(vld["id"], vnfd_cp["member-vnf-index-ref"]),
819 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
820 # Check VNFFGD
821 for fgd in get_iterable(indata.get("vnffgd")):
822 for cls in get_iterable(fgd.get("classifier")):
823 rspref = cls.get("rsp-id-ref")
824 for rsp in get_iterable(fgd.get("rsp")):
825 rspid = rsp.get("id")
826 if rspid and rspref and rspid == rspref:
827 break
828 else:
829 raise EngineException(
830 "Error at vnffgd[id='{}']:classifier[id='{}']:rsp-id-ref '{}' does not match any rsp:id"
831 .format(fgd["id"], cls["id"], rspref),
832 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
833 return indata
834
835 def _validate_input_edit(self, indata, content, force=False):
836 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
837 """
838 indata looks as follows:
839 - In the new case (conformant)
840 {'nsdOperationalState': 'DISABLED', 'userDefinedData': {'id': 'string23',
841 '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}}
842 - In the old case (backwards-compatible)
843 {'id': 'string23', '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}
844 """
845 if "_admin" not in indata:
846 indata["_admin"] = {}
847
848 if "nsdOperationalState" in indata:
849 if indata["nsdOperationalState"] in ("ENABLED", "DISABLED"):
850 indata["_admin"]["operationalState"] = indata.pop("nsdOperationalState")
851 else:
852 raise EngineException("State '{}' is not a valid operational state"
853 .format(indata["nsdOperationalState"]),
854 http_code=HTTPStatus.BAD_REQUEST)
855
856 # In the case of user defined data, we need to put the data in the root of the object
857 # to preserve current expected behaviour
858 if "userDefinedData" in indata:
859 data = indata.pop("userDefinedData")
860 if type(data) == dict:
861 indata["_admin"]["userDefinedData"] = data
862 else:
863 raise EngineException("userDefinedData should be an object, but is '{}' instead"
864 .format(type(data)),
865 http_code=HTTPStatus.BAD_REQUEST)
866 if ("operationalState" in indata["_admin"] and
867 content["_admin"]["operationalState"] == indata["_admin"]["operationalState"]):
868 raise EngineException("nsdOperationalState already {}".format(content["_admin"]["operationalState"]),
869 http_code=HTTPStatus.CONFLICT)
870 return indata
871
872 def _check_descriptor_dependencies(self, session, descriptor):
873 """
874 Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd
875 connection points are ok
876 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
877 :param descriptor: descriptor to be inserted or edit
878 :return: None or raises exception
879 """
880 if session["force"]:
881 return
882 member_vnfd_index = {}
883 if descriptor.get("constituent-vnfd") and not session["force"]:
884 for vnf in descriptor["constituent-vnfd"]:
885 vnfd_id = vnf["vnfd-id-ref"]
886 filter_q = self._get_project_filter(session)
887 filter_q["id"] = vnfd_id
888 vnf_list = self.db.get_list("vnfds", filter_q)
889 if not vnf_list:
890 raise EngineException("Descriptor error at 'constituent-vnfd':'vnfd-id-ref'='{}' references a non "
891 "existing vnfd".format(vnfd_id), http_code=HTTPStatus.CONFLICT)
892 # elif len(vnf_list) > 1:
893 # raise EngineException("More than one vnfd found for id='{}'".format(vnfd_id),
894 # http_code=HTTPStatus.CONFLICT)
895 member_vnfd_index[vnf["member-vnf-index"]] = vnf_list[0]
896
897 # Cross references validation in the descriptor and vnfd connection point validation
898 for vld in get_iterable(descriptor.get("vld")):
899 for referenced_vnfd_cp in get_iterable(vld.get("vnfd-connection-point-ref")):
900 # look if this vnfd contains this connection point
901 vnfd = member_vnfd_index.get(referenced_vnfd_cp["member-vnf-index-ref"])
902 for vnfd_cp in get_iterable(vnfd.get("connection-point")):
903 if referenced_vnfd_cp.get("vnfd-connection-point-ref") == vnfd_cp["name"]:
904 break
905 else:
906 raise EngineException(
907 "Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}']:vnfd-"
908 "connection-point-ref='{}' references a non existing conection-point:name inside vnfd '{}'"
909 .format(vld["id"], referenced_vnfd_cp["member-vnf-index-ref"],
910 referenced_vnfd_cp["vnfd-connection-point-ref"], vnfd["id"]),
911 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
912
913 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
914 super().check_conflict_on_edit(session, final_content, edit_content, _id)
915
916 self._check_descriptor_dependencies(session, final_content)
917
918 def check_conflict_on_del(self, session, _id, db_content):
919 """
920 Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note
921 that NSD can be public and be used by other projects.
922 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
923 :param _id: nsd internal id
924 :param db_content: The database content of the _id
925 :return: None or raises EngineException with the conflict
926 """
927 if session["force"]:
928 return
929 descriptor = db_content
930 descriptor_id = descriptor.get("id")
931 if not descriptor_id: # empty nsd not uploaded
932 return
933
934 # check NSD used by NS
935 _filter = self._get_project_filter(session)
936 _filter["nsd-id"] = _id
937 if self.db.get_list("nsrs", _filter):
938 raise EngineException("There is at least one NS using this descriptor", http_code=HTTPStatus.CONFLICT)
939
940 # check NSD referenced by NST
941 del _filter["nsd-id"]
942 _filter["netslice-subnet.ANYINDEX.nsd-ref"] = descriptor_id
943 if self.db.get_list("nsts", _filter):
944 raise EngineException("There is at least one NetSlice Template referencing this descriptor",
945 http_code=HTTPStatus.CONFLICT)
946
947 def sol005_projection(self, data):
948 data["nsdOnboardingState"] = data["_admin"]["onboardingState"]
949 data["nsdOperationalState"] = data["_admin"]["operationalState"]
950 data["nsdUsageState"] = data["_admin"]["usageState"]
951
952 links = {}
953 links["self"] = {"href": "/nsd/v1/ns_descriptors/{}".format(data["_id"])}
954 links["nsd_content"] = {"href": "/nsd/v1/ns_descriptors/{}/nsd_content".format(data["_id"])}
955 data["_links"] = links
956
957 return super().sol005_projection(data)
958
959
960 class NstTopic(DescriptorTopic):
961 topic = "nsts"
962 topic_msg = "nst"
963 quota_name = "slice_templates"
964
965 def __init__(self, db, fs, msg, auth):
966 DescriptorTopic.__init__(self, db, fs, msg, auth)
967
968 @staticmethod
969 def _remove_envelop(indata=None):
970 if not indata:
971 return {}
972 clean_indata = indata
973
974 if clean_indata.get('nst'):
975 if not isinstance(clean_indata['nst'], list) or len(clean_indata['nst']) != 1:
976 raise EngineException("'nst' must be a list only one element")
977 clean_indata = clean_indata['nst'][0]
978 elif clean_indata.get('nst:nst'):
979 if not isinstance(clean_indata['nst:nst'], list) or len(clean_indata['nst:nst']) != 1:
980 raise EngineException("'nst:nst' must be a list only one element")
981 clean_indata = clean_indata['nst:nst'][0]
982 return clean_indata
983
984 def _validate_input_new(self, indata, storage_params, force=False):
985 indata.pop("onboardingState", None)
986 indata.pop("operationalState", None)
987 indata.pop("usageState", None)
988 indata = self.pyangbind_validation("nsts", indata, force)
989 return indata.copy()
990
991 def _check_descriptor_dependencies(self, session, descriptor):
992 """
993 Check that the dependent descriptors exist on a new descriptor or edition
994 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
995 :param descriptor: descriptor to be inserted or edit
996 :return: None or raises exception
997 """
998 if not descriptor.get("netslice-subnet"):
999 return
1000 for nsd in descriptor["netslice-subnet"]:
1001 nsd_id = nsd["nsd-ref"]
1002 filter_q = self._get_project_filter(session)
1003 filter_q["id"] = nsd_id
1004 if not self.db.get_list("nsds", filter_q):
1005 raise EngineException("Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non "
1006 "existing nsd".format(nsd_id), http_code=HTTPStatus.CONFLICT)
1007
1008 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
1009 super().check_conflict_on_edit(session, final_content, edit_content, _id)
1010
1011 self._check_descriptor_dependencies(session, final_content)
1012
1013 def check_conflict_on_del(self, session, _id, db_content):
1014 """
1015 Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note
1016 that NST can be public and be used by other projects.
1017 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1018 :param _id: nst internal id
1019 :param db_content: The database content of the _id.
1020 :return: None or raises EngineException with the conflict
1021 """
1022 # TODO: Check this method
1023 if session["force"]:
1024 return
1025 # Get Network Slice Template from Database
1026 _filter = self._get_project_filter(session)
1027 _filter["_admin.nst-id"] = _id
1028 if self.db.get_list("nsis", _filter):
1029 raise EngineException("there is at least one Netslice Instance using this descriptor",
1030 http_code=HTTPStatus.CONFLICT)
1031
1032 def sol005_projection(self, data):
1033 data["onboardingState"] = data["_admin"]["onboardingState"]
1034 data["operationalState"] = data["_admin"]["operationalState"]
1035 data["usageState"] = data["_admin"]["usageState"]
1036
1037 links = {}
1038 links["self"] = {"href": "/nst/v1/netslice_templates/{}".format(data["_id"])}
1039 links["nst"] = {"href": "/nst/v1/netslice_templates/{}/nst".format(data["_id"])}
1040 data["_links"] = links
1041
1042 return super().sol005_projection(data)
1043
1044
1045 class PduTopic(BaseTopic):
1046 topic = "pdus"
1047 topic_msg = "pdu"
1048 quota_name = "pduds"
1049 schema_new = pdu_new_schema
1050 schema_edit = pdu_edit_schema
1051
1052 def __init__(self, db, fs, msg, auth):
1053 BaseTopic.__init__(self, db, fs, msg, auth)
1054
1055 @staticmethod
1056 def format_on_new(content, project_id=None, make_public=False):
1057 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
1058 content["_admin"]["onboardingState"] = "CREATED"
1059 content["_admin"]["operationalState"] = "ENABLED"
1060 content["_admin"]["usageState"] = "NOT_IN_USE"
1061
1062 def check_conflict_on_del(self, session, _id, db_content):
1063 """
1064 Check that there is not any vnfr that uses this PDU
1065 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1066 :param _id: pdu internal id
1067 :param db_content: The database content of the _id.
1068 :return: None or raises EngineException with the conflict
1069 """
1070 if session["force"]:
1071 return
1072
1073 _filter = self._get_project_filter(session)
1074 _filter["vdur.pdu-id"] = _id
1075 if self.db.get_list("vnfrs", _filter):
1076 raise EngineException("There is at least one VNF using this PDU", http_code=HTTPStatus.CONFLICT)
1077
1078
1079 class VnfPkgOpTopic(BaseTopic):
1080 topic = "vnfpkgops"
1081 topic_msg = "vnfd"
1082 schema_new = vnfpkgop_new_schema
1083 schema_edit = None
1084
1085 def __init__(self, db, fs, msg, auth):
1086 BaseTopic.__init__(self, db, fs, msg, auth)
1087
1088 def edit(self, session, _id, indata=None, kwargs=None, content=None):
1089 raise EngineException("Method 'edit' not allowed for topic '{}'".format(self.topic),
1090 HTTPStatus.METHOD_NOT_ALLOWED)
1091
1092 def delete(self, session, _id, dry_run=False):
1093 raise EngineException("Method 'delete' not allowed for topic '{}'".format(self.topic),
1094 HTTPStatus.METHOD_NOT_ALLOWED)
1095
1096 def delete_list(self, session, filter_q=None):
1097 raise EngineException("Method 'delete_list' not allowed for topic '{}'".format(self.topic),
1098 HTTPStatus.METHOD_NOT_ALLOWED)
1099
1100 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
1101 """
1102 Creates a new entry into database.
1103 :param rollback: list to append created items at database in case a rollback may to be done
1104 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1105 :param indata: data to be inserted
1106 :param kwargs: used to override the indata descriptor
1107 :param headers: http request headers
1108 :return: _id, op_id:
1109 _id: identity of the inserted data.
1110 op_id: None
1111 """
1112 self._update_input_with_kwargs(indata, kwargs)
1113 validate_input(indata, self.schema_new)
1114 vnfpkg_id = indata["vnfPkgId"]
1115 filter_q = BaseTopic._get_project_filter(session)
1116 filter_q["_id"] = vnfpkg_id
1117 vnfd = self.db.get_one("vnfds", filter_q)
1118 operation = indata["lcmOperationType"]
1119 kdu_name = indata["kdu_name"]
1120 for kdu in vnfd.get("kdu", []):
1121 if kdu["name"] == kdu_name:
1122 helm_chart = kdu.get("helm-chart")
1123 juju_bundle = kdu.get("juju-bundle")
1124 break
1125 else:
1126 raise EngineException("Not found vnfd[id='{}']:kdu[name='{}']".format(vnfpkg_id, kdu_name))
1127 if helm_chart:
1128 indata["helm-chart"] = helm_chart
1129 match = fullmatch(r"([^/]*)/([^/]*)", helm_chart)
1130 repo_name = match.group(1) if match else None
1131 elif juju_bundle:
1132 indata["juju-bundle"] = juju_bundle
1133 match = fullmatch(r"([^/]*)/([^/]*)", juju_bundle)
1134 repo_name = match.group(1) if match else None
1135 else:
1136 raise EngineException("Found neither 'helm-chart' nor 'juju-bundle' in vnfd[id='{}']:kdu[name='{}']"
1137 .format(vnfpkg_id, kdu_name))
1138 if repo_name:
1139 del filter_q["_id"]
1140 filter_q["name"] = repo_name
1141 repo = self.db.get_one("k8srepos", filter_q)
1142 k8srepo_id = repo.get("_id")
1143 k8srepo_url = repo.get("url")
1144 else:
1145 k8srepo_id = None
1146 k8srepo_url = None
1147 indata["k8srepoId"] = k8srepo_id
1148 indata["k8srepo_url"] = k8srepo_url
1149 vnfpkgop_id = str(uuid4())
1150 vnfpkgop_desc = {
1151 "_id": vnfpkgop_id,
1152 "operationState": "PROCESSING",
1153 "vnfPkgId": vnfpkg_id,
1154 "lcmOperationType": operation,
1155 "isAutomaticInvocation": False,
1156 "isCancelPending": False,
1157 "operationParams": indata,
1158 "links": {
1159 "self": "/osm/vnfpkgm/v1/vnfpkg_op_occs/" + vnfpkgop_id,
1160 "vnfpkg": "/osm/vnfpkgm/v1/vnf_packages/" + vnfpkg_id,
1161 }
1162 }
1163 self.format_on_new(vnfpkgop_desc, session["project_id"], make_public=session["public"])
1164 ctime = vnfpkgop_desc["_admin"]["created"]
1165 vnfpkgop_desc["statusEnteredTime"] = ctime
1166 vnfpkgop_desc["startTime"] = ctime
1167 self.db.create(self.topic, vnfpkgop_desc)
1168 rollback.append({"topic": self.topic, "_id": vnfpkgop_id})
1169 self.msg.write(self.topic_msg, operation, vnfpkgop_desc)
1170 return vnfpkgop_id, None