Fix bug #1077 slice instantiation with ssh_keys option
[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 try:
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 except ValidationError as e:
171 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
172
173 def upload_content(self, session, _id, indata, kwargs, headers):
174 """
175 Used for receiving content by chunks (with a transaction_id header and/or gzip file. It will store and extract)
176 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
177 :param _id : the nsd,vnfd is already created, this is the id
178 :param indata: http body request
179 :param kwargs: user query string to override parameters. NOT USED
180 :param headers: http request headers
181 :return: True if package is completely uploaded or False if partial content has been uploded
182 Raise exception on error
183 """
184 # Check that _id exists and it is valid
185 current_desc = self.show(session, _id)
186
187 content_range_text = headers.get("Content-Range")
188 expected_md5 = headers.get("Content-File-MD5")
189 compressed = None
190 content_type = headers.get("Content-Type")
191 if content_type and "application/gzip" in content_type or "application/x-gzip" in content_type or \
192 "application/zip" in content_type:
193 compressed = "gzip"
194 filename = headers.get("Content-Filename")
195 if not filename:
196 filename = "package.tar.gz" if compressed else "package"
197 # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266
198 file_pkg = None
199 error_text = ""
200 try:
201 if content_range_text:
202 content_range = content_range_text.replace("-", " ").replace("/", " ").split()
203 if content_range[0] != "bytes": # TODO check x<y not negative < total....
204 raise IndexError()
205 start = int(content_range[1])
206 end = int(content_range[2]) + 1
207 total = int(content_range[3])
208 else:
209 start = 0
210 temp_folder = _id + "_" # all the content is upload here and if ok, it is rename from id_ to is folder
211
212 if start:
213 if not self.fs.file_exists(temp_folder, 'dir'):
214 raise EngineException("invalid Transaction-Id header", HTTPStatus.NOT_FOUND)
215 else:
216 self.fs.file_delete(temp_folder, ignore_non_exist=True)
217 self.fs.mkdir(temp_folder)
218
219 storage = self.fs.get_params()
220 storage["folder"] = _id
221
222 file_path = (temp_folder, filename)
223 if self.fs.file_exists(file_path, 'file'):
224 file_size = self.fs.file_size(file_path)
225 else:
226 file_size = 0
227 if file_size != start:
228 raise EngineException("invalid Content-Range start sequence, expected '{}' but received '{}'".format(
229 file_size, start), HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
230 file_pkg = self.fs.file_open(file_path, 'a+b')
231 if isinstance(indata, dict):
232 indata_text = yaml.safe_dump(indata, indent=4, default_flow_style=False)
233 file_pkg.write(indata_text.encode(encoding="utf-8"))
234 else:
235 indata_len = 0
236 while True:
237 indata_text = indata.read(4096)
238 indata_len += len(indata_text)
239 if not indata_text:
240 break
241 file_pkg.write(indata_text)
242 if content_range_text:
243 if indata_len != end-start:
244 raise EngineException("Mismatch between Content-Range header {}-{} and body length of {}".format(
245 start, end-1, indata_len), HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
246 if end != total:
247 # TODO update to UPLOADING
248 return False
249
250 # PACKAGE UPLOADED
251 if expected_md5:
252 file_pkg.seek(0, 0)
253 file_md5 = md5()
254 chunk_data = file_pkg.read(1024)
255 while chunk_data:
256 file_md5.update(chunk_data)
257 chunk_data = file_pkg.read(1024)
258 if expected_md5 != file_md5.hexdigest():
259 raise EngineException("Error, MD5 mismatch", HTTPStatus.CONFLICT)
260 file_pkg.seek(0, 0)
261 if compressed == "gzip":
262 tar = tarfile.open(mode='r', fileobj=file_pkg)
263 descriptor_file_name = None
264 for tarinfo in tar:
265 tarname = tarinfo.name
266 tarname_path = tarname.split("/")
267 if not tarname_path[0] or ".." in tarname_path: # if start with "/" means absolute path
268 raise EngineException("Absolute path or '..' are not allowed for package descriptor tar.gz")
269 if len(tarname_path) == 1 and not tarinfo.isdir():
270 raise EngineException("All files must be inside a dir for package descriptor tar.gz")
271 if tarname.endswith(".yaml") or tarname.endswith(".json") or tarname.endswith(".yml"):
272 storage["pkg-dir"] = tarname_path[0]
273 if len(tarname_path) == 2:
274 if descriptor_file_name:
275 raise EngineException(
276 "Found more than one descriptor file at package descriptor tar.gz")
277 descriptor_file_name = tarname
278 if not descriptor_file_name:
279 raise EngineException("Not found any descriptor file at package descriptor tar.gz")
280 storage["descriptor"] = descriptor_file_name
281 storage["zipfile"] = filename
282 self.fs.file_extract(tar, temp_folder)
283 with self.fs.file_open((temp_folder, descriptor_file_name), "r") as descriptor_file:
284 content = descriptor_file.read()
285 else:
286 content = file_pkg.read()
287 storage["descriptor"] = descriptor_file_name = filename
288
289 if descriptor_file_name.endswith(".json"):
290 error_text = "Invalid json format "
291 indata = json.load(content)
292 else:
293 error_text = "Invalid yaml format "
294 indata = yaml.load(content, Loader=yaml.SafeLoader)
295
296 current_desc["_admin"]["storage"] = storage
297 current_desc["_admin"]["onboardingState"] = "ONBOARDED"
298 current_desc["_admin"]["operationalState"] = "ENABLED"
299
300 indata = self._remove_envelop(indata)
301
302 # Override descriptor with query string kwargs
303 if kwargs:
304 self._update_input_with_kwargs(indata, kwargs)
305 # it will call overrides method at VnfdTopic or NsdTopic
306 # indata = self._validate_input_edit(indata, force=session["force"])
307
308 deep_update_rfc7396(current_desc, indata)
309 self.check_conflict_on_edit(session, current_desc, indata, _id=_id)
310 current_desc["_admin"]["modified"] = time()
311 self.db.replace(self.topic, _id, current_desc)
312 self.fs.dir_rename(temp_folder, _id)
313
314 indata["_id"] = _id
315 self._send_msg("edited", indata)
316
317 # TODO if descriptor has changed because kwargs update content and remove cached zip
318 # TODO if zip is not present creates one
319 return True
320
321 except EngineException:
322 raise
323 except IndexError:
324 raise EngineException("invalid Content-Range header format. Expected 'bytes start-end/total'",
325 HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
326 except IOError as e:
327 raise EngineException("invalid upload transaction sequence: '{}'".format(e), HTTPStatus.BAD_REQUEST)
328 except tarfile.ReadError as e:
329 raise EngineException("invalid file content {}".format(e), HTTPStatus.BAD_REQUEST)
330 except (ValueError, yaml.YAMLError) as e:
331 raise EngineException(error_text + str(e))
332 except ValidationError as e:
333 raise EngineException(e, HTTPStatus.UNPROCESSABLE_ENTITY)
334 finally:
335 if file_pkg:
336 file_pkg.close()
337
338 def get_file(self, session, _id, path=None, accept_header=None):
339 """
340 Return the file content of a vnfd or nsd
341 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
342 :param _id: Identity of the vnfd, nsd
343 :param path: artifact path or "$DESCRIPTOR" or None
344 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
345 :return: opened file plus Accept format or raises an exception
346 """
347 accept_text = accept_zip = False
348 if accept_header:
349 if 'text/plain' in accept_header or '*/*' in accept_header:
350 accept_text = True
351 if 'application/zip' in accept_header or '*/*' in accept_header:
352 accept_zip = 'application/zip'
353 elif 'application/gzip' in accept_header:
354 accept_zip = 'application/gzip'
355
356 if not accept_text and not accept_zip:
357 raise EngineException("provide request header 'Accept' with 'application/zip' or 'text/plain'",
358 http_code=HTTPStatus.NOT_ACCEPTABLE)
359
360 content = self.show(session, _id)
361 if content["_admin"]["onboardingState"] != "ONBOARDED":
362 raise EngineException("Cannot get content because this resource is not at 'ONBOARDED' state. "
363 "onboardingState is {}".format(content["_admin"]["onboardingState"]),
364 http_code=HTTPStatus.CONFLICT)
365 storage = content["_admin"]["storage"]
366 if path is not None and path != "$DESCRIPTOR": # artifacts
367 if not storage.get('pkg-dir'):
368 raise EngineException("Packages does not contains artifacts", http_code=HTTPStatus.BAD_REQUEST)
369 if self.fs.file_exists((storage['folder'], storage['pkg-dir'], *path), 'dir'):
370 folder_content = self.fs.dir_ls((storage['folder'], storage['pkg-dir'], *path))
371 return folder_content, "text/plain"
372 # TODO manage folders in http
373 else:
374 return self.fs.file_open((storage['folder'], storage['pkg-dir'], *path), "rb"),\
375 "application/octet-stream"
376
377 # pkgtype accept ZIP TEXT -> result
378 # manyfiles yes X -> zip
379 # no yes -> error
380 # onefile yes no -> zip
381 # X yes -> text
382
383 if accept_text and (not storage.get('pkg-dir') or path == "$DESCRIPTOR"):
384 return self.fs.file_open((storage['folder'], storage['descriptor']), "r"), "text/plain"
385 elif storage.get('pkg-dir') and not accept_zip:
386 raise EngineException("Packages that contains several files need to be retrieved with 'application/zip'"
387 "Accept header", http_code=HTTPStatus.NOT_ACCEPTABLE)
388 else:
389 if not storage.get('zipfile'):
390 # TODO generate zipfile if not present
391 raise EngineException("Only allowed 'text/plain' Accept header for this descriptor. To be solved in "
392 "future versions", http_code=HTTPStatus.NOT_ACCEPTABLE)
393 return self.fs.file_open((storage['folder'], storage['zipfile']), "rb"), accept_zip
394
395 def pyangbind_validation(self, item, data, force=False):
396 try:
397 if item == "vnfds":
398 myvnfd = vnfd_im()
399 pybindJSONDecoder.load_ietf_json({'vnfd:vnfd-catalog': {'vnfd': [data]}}, None, None, obj=myvnfd,
400 path_helper=True, skip_unknown=force)
401 out = pybindJSON.dumps(myvnfd, mode="ietf")
402 elif item == "nsds":
403 mynsd = nsd_im()
404 pybindJSONDecoder.load_ietf_json({'nsd:nsd-catalog': {'nsd': [data]}}, None, None, obj=mynsd,
405 path_helper=True, skip_unknown=force)
406 out = pybindJSON.dumps(mynsd, mode="ietf")
407 elif item == "nsts":
408 mynst = nst_im()
409 pybindJSONDecoder.load_ietf_json({'nst': [data]}, None, None, obj=mynst,
410 path_helper=True, skip_unknown=force)
411 out = pybindJSON.dumps(mynst, mode="ietf")
412 else:
413 raise EngineException("Not possible to validate '{}' item".format(item),
414 http_code=HTTPStatus.INTERNAL_SERVER_ERROR)
415
416 desc_out = self._remove_envelop(yaml.safe_load(out))
417 return desc_out
418
419 except Exception as e:
420 raise EngineException("Error in pyangbind validation: {}".format(str(e)),
421 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
422
423
424 class VnfdTopic(DescriptorTopic):
425 topic = "vnfds"
426 topic_msg = "vnfd"
427
428 def __init__(self, db, fs, msg, auth):
429 DescriptorTopic.__init__(self, db, fs, msg, auth)
430
431 @staticmethod
432 def _remove_envelop(indata=None):
433 if not indata:
434 return {}
435 clean_indata = indata
436 if clean_indata.get('vnfd:vnfd-catalog'):
437 clean_indata = clean_indata['vnfd:vnfd-catalog']
438 elif clean_indata.get('vnfd-catalog'):
439 clean_indata = clean_indata['vnfd-catalog']
440 if clean_indata.get('vnfd'):
441 if not isinstance(clean_indata['vnfd'], list) or len(clean_indata['vnfd']) != 1:
442 raise EngineException("'vnfd' must be a list of only one element")
443 clean_indata = clean_indata['vnfd'][0]
444 elif clean_indata.get('vnfd:vnfd'):
445 if not isinstance(clean_indata['vnfd:vnfd'], list) or len(clean_indata['vnfd:vnfd']) != 1:
446 raise EngineException("'vnfd:vnfd' must be a list of only one element")
447 clean_indata = clean_indata['vnfd:vnfd'][0]
448 return clean_indata
449
450 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
451 super().check_conflict_on_edit(session, final_content, edit_content, _id)
452
453 # set type of vnfd
454 contains_pdu = False
455 contains_vdu = False
456 for vdu in get_iterable(final_content.get("vdu")):
457 if vdu.get("pdu-type"):
458 contains_pdu = True
459 else:
460 contains_vdu = True
461 if contains_pdu:
462 final_content["_admin"]["type"] = "hnfd" if contains_vdu else "pnfd"
463 elif contains_vdu:
464 final_content["_admin"]["type"] = "vnfd"
465 # if neither vud nor pdu do not fill type
466
467 def check_conflict_on_del(self, session, _id, db_content):
468 """
469 Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note
470 that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr
471 that uses this vnfd
472 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
473 :param _id: vnfd internal id
474 :param db_content: The database content of the _id.
475 :return: None or raises EngineException with the conflict
476 """
477 if session["force"]:
478 return
479 descriptor = db_content
480 descriptor_id = descriptor.get("id")
481 if not descriptor_id: # empty vnfd not uploaded
482 return
483
484 _filter = self._get_project_filter(session)
485
486 # check vnfrs using this vnfd
487 _filter["vnfd-id"] = _id
488 if self.db.get_list("vnfrs", _filter):
489 raise EngineException("There is at least one VNF using this descriptor", http_code=HTTPStatus.CONFLICT)
490
491 # check NSD referencing this VNFD
492 del _filter["vnfd-id"]
493 _filter["constituent-vnfd.ANYINDEX.vnfd-id-ref"] = descriptor_id
494 if self.db.get_list("nsds", _filter):
495 raise EngineException("There is at least one NSD referencing this descriptor",
496 http_code=HTTPStatus.CONFLICT)
497
498 def _validate_input_new(self, indata, storage_params, force=False):
499 indata = self.pyangbind_validation("vnfds", indata, force)
500 # Cross references validation in the descriptor
501 if indata.get("vdu"):
502 if not indata.get("mgmt-interface"):
503 raise EngineException("'mgmt-interface' is a mandatory field and it is not defined",
504 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
505 if indata["mgmt-interface"].get("cp"):
506 for cp in get_iterable(indata.get("connection-point")):
507 if cp["name"] == indata["mgmt-interface"]["cp"]:
508 break
509 else:
510 raise EngineException("mgmt-interface:cp='{}' must match an existing connection-point"
511 .format(indata["mgmt-interface"]["cp"]),
512 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
513
514 for vdu in get_iterable(indata.get("vdu")):
515 icp_refs = []
516 ecp_refs = []
517 for interface in get_iterable(vdu.get("interface")):
518 if interface.get("external-connection-point-ref"):
519 if interface.get("external-connection-point-ref") in ecp_refs:
520 raise EngineException("vdu[id='{}']:interface[name='{}']:external-connection-point-ref='{}' "
521 "is referenced by other interface"
522 .format(vdu["id"], interface["name"],
523 interface["external-connection-point-ref"]),
524 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
525 ecp_refs.append(interface.get("external-connection-point-ref"))
526 for cp in get_iterable(indata.get("connection-point")):
527 if cp["name"] == interface["external-connection-point-ref"]:
528 break
529 else:
530 raise EngineException("vdu[id='{}']:interface[name='{}']:external-connection-point-ref='{}' "
531 "must match an existing connection-point"
532 .format(vdu["id"], interface["name"],
533 interface["external-connection-point-ref"]),
534 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
535 elif interface.get("internal-connection-point-ref"):
536 if interface.get("internal-connection-point-ref") in icp_refs:
537 raise EngineException("vdu[id='{}']:interface[name='{}']:internal-connection-point-ref='{}' "
538 "is referenced by other interface"
539 .format(vdu["id"], interface["name"],
540 interface["internal-connection-point-ref"]),
541 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
542 icp_refs.append(interface.get("internal-connection-point-ref"))
543 for internal_cp in get_iterable(vdu.get("internal-connection-point")):
544 if interface["internal-connection-point-ref"] == internal_cp.get("id"):
545 break
546 else:
547 raise EngineException("vdu[id='{}']:interface[name='{}']:internal-connection-point-ref='{}' "
548 "must match an existing vdu:internal-connection-point"
549 .format(vdu["id"], interface["name"],
550 interface["internal-connection-point-ref"]),
551 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
552 # Validate that if descriptor contains charms, artifacts _admin.storage."pkg-dir" is not none
553 if vdu.get("vdu-configuration"):
554 if vdu["vdu-configuration"].get("juju"):
555 if not self._validate_package_folders(storage_params, 'charms'):
556 raise EngineException("Charm defined in vnf[id={}]:vdu[id={}] but not present in "
557 "package".format(indata["id"], vdu["id"]))
558 # Validate that if descriptor contains cloud-init, artifacts _admin.storage."pkg-dir" is not none
559 if vdu.get("cloud-init-file"):
560 if not self._validate_package_folders(storage_params, 'cloud_init', vdu["cloud-init-file"]):
561 raise EngineException("Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
562 "package".format(indata["id"], vdu["id"]))
563 # Validate that if descriptor contains charms, artifacts _admin.storage."pkg-dir" is not none
564 if indata.get("vnf-configuration"):
565 if indata["vnf-configuration"].get("juju"):
566 if not self._validate_package_folders(storage_params, 'charms'):
567 raise EngineException("Charm defined in vnf[id={}] but not present in "
568 "package".format(indata["id"]))
569 vld_names = [] # For detection of duplicated VLD names
570 for ivld in get_iterable(indata.get("internal-vld")):
571 # BEGIN Detection of duplicated VLD names
572 ivld_name = ivld.get("name")
573 if ivld_name:
574 if ivld_name in vld_names:
575 raise EngineException("Duplicated VLD name '{}' in vnfd[id={}]:internal-vld[id={}]"
576 .format(ivld["name"], indata["id"], ivld["id"]),
577 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
578 else:
579 vld_names.append(ivld_name)
580 # END Detection of duplicated VLD names
581 for icp in get_iterable(ivld.get("internal-connection-point")):
582 icp_mark = False
583 for vdu in get_iterable(indata.get("vdu")):
584 for internal_cp in get_iterable(vdu.get("internal-connection-point")):
585 if icp["id-ref"] == internal_cp["id"]:
586 icp_mark = True
587 break
588 if icp_mark:
589 break
590 else:
591 raise EngineException("internal-vld[id='{}']:internal-connection-point='{}' must match an existing "
592 "vdu:internal-connection-point".format(ivld["id"], icp["id-ref"]),
593 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
594 if ivld.get("ip-profile-ref"):
595 for ip_prof in get_iterable(indata.get("ip-profiles")):
596 if ip_prof["name"] == get_iterable(ivld.get("ip-profile-ref")):
597 break
598 else:
599 raise EngineException("internal-vld[id='{}']:ip-profile-ref='{}' does not exist".format(
600 ivld["id"], ivld["ip-profile-ref"]),
601 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
602 for mp in get_iterable(indata.get("monitoring-param")):
603 if mp.get("vdu-monitoring-param"):
604 mp_vmp_mark = False
605 for vdu in get_iterable(indata.get("vdu")):
606 for vmp in get_iterable(vdu.get("monitoring-param")):
607 if vmp["id"] == mp["vdu-monitoring-param"].get("vdu-monitoring-param-ref") and vdu["id"] ==\
608 mp["vdu-monitoring-param"]["vdu-ref"]:
609 mp_vmp_mark = True
610 break
611 if mp_vmp_mark:
612 break
613 else:
614 raise EngineException("monitoring-param:vdu-monitoring-param:vdu-monitoring-param-ref='{}' not "
615 "defined at vdu[id='{}'] or vdu does not exist"
616 .format(mp["vdu-monitoring-param"]["vdu-monitoring-param-ref"],
617 mp["vdu-monitoring-param"]["vdu-ref"]),
618 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
619 elif mp.get("vdu-metric"):
620 mp_vm_mark = False
621 for vdu in get_iterable(indata.get("vdu")):
622 if vdu.get("vdu-configuration"):
623 for metric in get_iterable(vdu["vdu-configuration"].get("metrics")):
624 if metric["name"] == mp["vdu-metric"]["vdu-metric-name-ref"] and vdu["id"] == \
625 mp["vdu-metric"]["vdu-ref"]:
626 mp_vm_mark = True
627 break
628 if mp_vm_mark:
629 break
630 else:
631 raise EngineException("monitoring-param:vdu-metric:vdu-metric-name-ref='{}' not defined at "
632 "vdu[id='{}'] or vdu does not exist"
633 .format(mp["vdu-metric"]["vdu-metric-name-ref"],
634 mp["vdu-metric"]["vdu-ref"]),
635 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
636
637 for sgd in get_iterable(indata.get("scaling-group-descriptor")):
638 for sp in get_iterable(sgd.get("scaling-policy")):
639 for sc in get_iterable(sp.get("scaling-criteria")):
640 for mp in get_iterable(indata.get("monitoring-param")):
641 if mp["id"] == get_iterable(sc.get("vnf-monitoring-param-ref")):
642 break
643 else:
644 raise EngineException("scaling-group-descriptor[name='{}']:scaling-criteria[name='{}']:"
645 "vnf-monitoring-param-ref='{}' not defined in any monitoring-param"
646 .format(sgd["name"], sc["name"], sc["vnf-monitoring-param-ref"]),
647 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
648 for sgd_vdu in get_iterable(sgd.get("vdu")):
649 sgd_vdu_mark = False
650 for vdu in get_iterable(indata.get("vdu")):
651 if vdu["id"] == sgd_vdu["vdu-id-ref"]:
652 sgd_vdu_mark = True
653 break
654 if sgd_vdu_mark:
655 break
656 else:
657 raise EngineException("scaling-group-descriptor[name='{}']:vdu-id-ref={} does not match any vdu"
658 .format(sgd["name"], sgd_vdu["vdu-id-ref"]),
659 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
660 for sca in get_iterable(sgd.get("scaling-config-action")):
661 if not indata.get("vnf-configuration"):
662 raise EngineException("'vnf-configuration' not defined in the descriptor but it is referenced by "
663 "scaling-group-descriptor[name='{}']:scaling-config-action"
664 .format(sgd["name"]),
665 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
666 for primitive in get_iterable(indata["vnf-configuration"].get("config-primitive")):
667 if primitive["name"] == sca["vnf-config-primitive-name-ref"]:
668 break
669 else:
670 raise EngineException("scaling-group-descriptor[name='{}']:scaling-config-action:vnf-config-"
671 "primitive-name-ref='{}' does not match any "
672 "vnf-configuration:config-primitive:name"
673 .format(sgd["name"], sca["vnf-config-primitive-name-ref"]),
674 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
675 return indata
676
677 def _validate_input_edit(self, indata, force=False):
678 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
679 return indata
680
681 def _validate_package_folders(self, storage_params, folder, file=None):
682 if not storage_params or not storage_params.get("pkg-dir"):
683 return False
684 else:
685 if self.fs.file_exists("{}_".format(storage_params["folder"]), 'dir'):
686 f = "{}_/{}/{}".format(storage_params["folder"], storage_params["pkg-dir"], folder)
687 else:
688 f = "{}/{}/{}".format(storage_params["folder"], storage_params["pkg-dir"], folder)
689 if file:
690 return self.fs.file_exists("{}/{}".format(f, file), 'file')
691 else:
692 if self.fs.file_exists(f, 'dir'):
693 if self.fs.dir_ls(f):
694 return True
695 return False
696
697 def delete_extra(self, session, _id, db_content, not_send_msg=None):
698 """
699 Deletes associate file system storage (via super)
700 Deletes associated vnfpkgops from database.
701 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
702 :param _id: server internal id
703 :param db_content: The database content of the descriptor
704 :return: None
705 :raises: FsException in case of error while deleting associated storage
706 """
707 super().delete_extra(session, _id, db_content, not_send_msg)
708 self.db.del_list("vnfpkgops", {"vnfPkgId": _id})
709
710
711 class NsdTopic(DescriptorTopic):
712 topic = "nsds"
713 topic_msg = "nsd"
714
715 def __init__(self, db, fs, msg, auth):
716 DescriptorTopic.__init__(self, db, fs, msg, auth)
717
718 @staticmethod
719 def _remove_envelop(indata=None):
720 if not indata:
721 return {}
722 clean_indata = indata
723
724 if clean_indata.get('nsd:nsd-catalog'):
725 clean_indata = clean_indata['nsd:nsd-catalog']
726 elif clean_indata.get('nsd-catalog'):
727 clean_indata = clean_indata['nsd-catalog']
728 if clean_indata.get('nsd'):
729 if not isinstance(clean_indata['nsd'], list) or len(clean_indata['nsd']) != 1:
730 raise EngineException("'nsd' must be a list of only one element")
731 clean_indata = clean_indata['nsd'][0]
732 elif clean_indata.get('nsd:nsd'):
733 if not isinstance(clean_indata['nsd:nsd'], list) or len(clean_indata['nsd:nsd']) != 1:
734 raise EngineException("'nsd:nsd' must be a list of only one element")
735 clean_indata = clean_indata['nsd:nsd'][0]
736 return clean_indata
737
738 def _validate_input_new(self, indata, storage_params, force=False):
739 indata = self.pyangbind_validation("nsds", indata, force)
740 # Cross references validation in the descriptor
741 # TODO validata that if contains cloud-init-file or charms, have artifacts _admin.storage."pkg-dir" is not none
742 for vld in get_iterable(indata.get("vld")):
743 if vld.get("mgmt-network") and vld.get("ip-profile-ref"):
744 raise EngineException("Error at vld[id='{}']:ip-profile-ref"
745 " You cannot set an ip-profile when mgmt-network is True"
746 .format(vld["id"]), http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
747 for vnfd_cp in get_iterable(vld.get("vnfd-connection-point-ref")):
748 for constituent_vnfd in get_iterable(indata.get("constituent-vnfd")):
749 if vnfd_cp["member-vnf-index-ref"] == constituent_vnfd["member-vnf-index"]:
750 if vnfd_cp.get("vnfd-id-ref") and vnfd_cp["vnfd-id-ref"] != constituent_vnfd["vnfd-id-ref"]:
751 raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[vnfd-id-ref='{}'] "
752 "does not match constituent-vnfd[member-vnf-index='{}']:vnfd-id-ref"
753 " '{}'".format(vld["id"], vnfd_cp["vnfd-id-ref"],
754 constituent_vnfd["member-vnf-index"],
755 constituent_vnfd["vnfd-id-ref"]),
756 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
757 break
758 else:
759 raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}'] "
760 "does not match any constituent-vnfd:member-vnf-index"
761 .format(vld["id"], vnfd_cp["member-vnf-index-ref"]),
762 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
763 # Check VNFFGD
764 for fgd in get_iterable(indata.get("vnffgd")):
765 for cls in get_iterable(fgd.get("classifier")):
766 rspref = cls.get("rsp-id-ref")
767 for rsp in get_iterable(fgd.get("rsp")):
768 rspid = rsp.get("id")
769 if rspid and rspref and rspid == rspref:
770 break
771 else:
772 raise EngineException(
773 "Error at vnffgd[id='{}']:classifier[id='{}']:rsp-id-ref '{}' does not match any rsp:id"
774 .format(fgd["id"], cls["id"], rspref),
775 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
776 return indata
777
778 def _validate_input_edit(self, indata, force=False):
779 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
780 return indata
781
782 def _check_descriptor_dependencies(self, session, descriptor):
783 """
784 Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd
785 connection points are ok
786 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
787 :param descriptor: descriptor to be inserted or edit
788 :return: None or raises exception
789 """
790 if session["force"]:
791 return
792 member_vnfd_index = {}
793 if descriptor.get("constituent-vnfd") and not session["force"]:
794 for vnf in descriptor["constituent-vnfd"]:
795 vnfd_id = vnf["vnfd-id-ref"]
796 filter_q = self._get_project_filter(session)
797 filter_q["id"] = vnfd_id
798 vnf_list = self.db.get_list("vnfds", filter_q)
799 if not vnf_list:
800 raise EngineException("Descriptor error at 'constituent-vnfd':'vnfd-id-ref'='{}' references a non "
801 "existing vnfd".format(vnfd_id), http_code=HTTPStatus.CONFLICT)
802 # elif len(vnf_list) > 1:
803 # raise EngineException("More than one vnfd found for id='{}'".format(vnfd_id),
804 # http_code=HTTPStatus.CONFLICT)
805 member_vnfd_index[vnf["member-vnf-index"]] = vnf_list[0]
806
807 # Cross references validation in the descriptor and vnfd connection point validation
808 for vld in get_iterable(descriptor.get("vld")):
809 for referenced_vnfd_cp in get_iterable(vld.get("vnfd-connection-point-ref")):
810 # look if this vnfd contains this connection point
811 vnfd = member_vnfd_index.get(referenced_vnfd_cp["member-vnf-index-ref"])
812 for vnfd_cp in get_iterable(vnfd.get("connection-point")):
813 if referenced_vnfd_cp.get("vnfd-connection-point-ref") == vnfd_cp["name"]:
814 break
815 else:
816 raise EngineException(
817 "Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}']:vnfd-"
818 "connection-point-ref='{}' references a non existing conection-point:name inside vnfd '{}'"
819 .format(vld["id"], referenced_vnfd_cp["member-vnf-index-ref"],
820 referenced_vnfd_cp["vnfd-connection-point-ref"], vnfd["id"]),
821 http_code=HTTPStatus.UNPROCESSABLE_ENTITY)
822
823 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
824 super().check_conflict_on_edit(session, final_content, edit_content, _id)
825
826 self._check_descriptor_dependencies(session, final_content)
827
828 def check_conflict_on_del(self, session, _id, db_content):
829 """
830 Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note
831 that NSD can be public and be used by other projects.
832 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
833 :param _id: nsd internal id
834 :param db_content: The database content of the _id
835 :return: None or raises EngineException with the conflict
836 """
837 if session["force"]:
838 return
839 descriptor = db_content
840 descriptor_id = descriptor.get("id")
841 if not descriptor_id: # empty nsd not uploaded
842 return
843
844 # check NSD used by NS
845 _filter = self._get_project_filter(session)
846 _filter["nsd-id"] = _id
847 if self.db.get_list("nsrs", _filter):
848 raise EngineException("There is at least one NS using this descriptor", http_code=HTTPStatus.CONFLICT)
849
850 # check NSD referenced by NST
851 del _filter["nsd-id"]
852 _filter["netslice-subnet.ANYINDEX.nsd-ref"] = descriptor_id
853 if self.db.get_list("nsts", _filter):
854 raise EngineException("There is at least one NetSlice Template referencing this descriptor",
855 http_code=HTTPStatus.CONFLICT)
856
857
858 class NstTopic(DescriptorTopic):
859 topic = "nsts"
860 topic_msg = "nst"
861
862 def __init__(self, db, fs, msg, auth):
863 DescriptorTopic.__init__(self, db, fs, msg, auth)
864
865 @staticmethod
866 def _remove_envelop(indata=None):
867 if not indata:
868 return {}
869 clean_indata = indata
870
871 if clean_indata.get('nst'):
872 if not isinstance(clean_indata['nst'], list) or len(clean_indata['nst']) != 1:
873 raise EngineException("'nst' must be a list only one element")
874 clean_indata = clean_indata['nst'][0]
875 elif clean_indata.get('nst:nst'):
876 if not isinstance(clean_indata['nst:nst'], list) or len(clean_indata['nst:nst']) != 1:
877 raise EngineException("'nst:nst' must be a list only one element")
878 clean_indata = clean_indata['nst:nst'][0]
879 return clean_indata
880
881 def _validate_input_edit(self, indata, force=False):
882 # TODO validate with pyangbind, serialize
883 return indata
884
885 def _validate_input_new(self, indata, storage_params, force=False):
886 indata = self.pyangbind_validation("nsts", indata, force)
887 return indata.copy()
888
889 def _check_descriptor_dependencies(self, session, descriptor):
890 """
891 Check that the dependent descriptors exist on a new descriptor or edition
892 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
893 :param descriptor: descriptor to be inserted or edit
894 :return: None or raises exception
895 """
896 if not descriptor.get("netslice-subnet"):
897 return
898 for nsd in descriptor["netslice-subnet"]:
899 nsd_id = nsd["nsd-ref"]
900 filter_q = self._get_project_filter(session)
901 filter_q["id"] = nsd_id
902 if not self.db.get_list("nsds", filter_q):
903 raise EngineException("Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non "
904 "existing nsd".format(nsd_id), http_code=HTTPStatus.CONFLICT)
905
906 def check_conflict_on_edit(self, session, final_content, edit_content, _id):
907 super().check_conflict_on_edit(session, final_content, edit_content, _id)
908
909 self._check_descriptor_dependencies(session, final_content)
910
911 def check_conflict_on_del(self, session, _id, db_content):
912 """
913 Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note
914 that NST can be public and be used by other projects.
915 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
916 :param _id: nst internal id
917 :param db_content: The database content of the _id.
918 :return: None or raises EngineException with the conflict
919 """
920 # TODO: Check this method
921 if session["force"]:
922 return
923 # Get Network Slice Template from Database
924 _filter = self._get_project_filter(session)
925 _filter["_admin.nst-id"] = _id
926 if self.db.get_list("nsis", _filter):
927 raise EngineException("there is at least one Netslice Instance using this descriptor",
928 http_code=HTTPStatus.CONFLICT)
929
930
931 class PduTopic(BaseTopic):
932 topic = "pdus"
933 topic_msg = "pdu"
934 schema_new = pdu_new_schema
935 schema_edit = pdu_edit_schema
936
937 def __init__(self, db, fs, msg, auth):
938 BaseTopic.__init__(self, db, fs, msg, auth)
939
940 @staticmethod
941 def format_on_new(content, project_id=None, make_public=False):
942 BaseTopic.format_on_new(content, project_id=project_id, make_public=make_public)
943 content["_admin"]["onboardingState"] = "CREATED"
944 content["_admin"]["operationalState"] = "ENABLED"
945 content["_admin"]["usageState"] = "NOT_IN_USE"
946
947 def check_conflict_on_del(self, session, _id, db_content):
948 """
949 Check that there is not any vnfr that uses this PDU
950 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
951 :param _id: pdu internal id
952 :param db_content: The database content of the _id.
953 :return: None or raises EngineException with the conflict
954 """
955 if session["force"]:
956 return
957
958 _filter = self._get_project_filter(session)
959 _filter["vdur.pdu-id"] = _id
960 if self.db.get_list("vnfrs", _filter):
961 raise EngineException("There is at least one VNF using this PDU", http_code=HTTPStatus.CONFLICT)
962
963
964 class VnfPkgOpTopic(BaseTopic):
965 topic = "vnfpkgops"
966 topic_msg = "vnfd"
967 schema_new = vnfpkgop_new_schema
968 schema_edit = None
969
970 def __init__(self, db, fs, msg, auth):
971 BaseTopic.__init__(self, db, fs, msg, auth)
972
973 def edit(self, session, _id, indata=None, kwargs=None, content=None):
974 raise EngineException("Method 'edit' not allowed for topic '{}'".format(self.topic),
975 HTTPStatus.METHOD_NOT_ALLOWED)
976
977 def delete(self, session, _id, dry_run=False):
978 raise EngineException("Method 'delete' not allowed for topic '{}'".format(self.topic),
979 HTTPStatus.METHOD_NOT_ALLOWED)
980
981 def delete_list(self, session, filter_q=None):
982 raise EngineException("Method 'delete_list' not allowed for topic '{}'".format(self.topic),
983 HTTPStatus.METHOD_NOT_ALLOWED)
984
985 def new(self, rollback, session, indata=None, kwargs=None, headers=None):
986 """
987 Creates a new entry into database.
988 :param rollback: list to append created items at database in case a rollback may to be done
989 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
990 :param indata: data to be inserted
991 :param kwargs: used to override the indata descriptor
992 :param headers: http request headers
993 :return: _id, op_id:
994 _id: identity of the inserted data.
995 op_id: None
996 """
997 self._update_input_with_kwargs(indata, kwargs)
998 validate_input(indata, self.schema_new)
999 vnfpkg_id = indata["vnfPkgId"]
1000 filter_q = BaseTopic._get_project_filter(session)
1001 filter_q["_id"] = vnfpkg_id
1002 vnfd = self.db.get_one("vnfds", filter_q)
1003 operation = indata["lcmOperationType"]
1004 kdu_name = indata["kdu_name"]
1005 for kdu in vnfd.get("kdu", []):
1006 if kdu["name"] == kdu_name:
1007 helm_chart = kdu.get("helm-chart")
1008 juju_bundle = kdu.get("juju-bundle")
1009 break
1010 else:
1011 raise EngineException("Not found vnfd[id='{}']:kdu[name='{}']".format(vnfpkg_id, kdu_name))
1012 if helm_chart:
1013 indata["helm-chart"] = helm_chart
1014 match = fullmatch(r"([^/]*)/([^/]*)", helm_chart)
1015 repo_name = match.group(1) if match else None
1016 elif juju_bundle:
1017 indata["juju-bundle"] = juju_bundle
1018 match = fullmatch(r"([^/]*)/([^/]*)", juju_bundle)
1019 repo_name = match.group(1) if match else None
1020 else:
1021 raise EngineException("Found neither 'helm-chart' nor 'juju-bundle' in vnfd[id='{}']:kdu[name='{}']"
1022 .format(vnfpkg_id, kdu_name))
1023 if repo_name:
1024 del filter_q["_id"]
1025 filter_q["name"] = repo_name
1026 repo = self.db.get_one("k8srepos", filter_q)
1027 k8srepo_id = repo.get("_id")
1028 k8srepo_url = repo.get("url")
1029 else:
1030 k8srepo_id = None
1031 k8srepo_url = None
1032 indata["k8srepoId"] = k8srepo_id
1033 indata["k8srepo_url"] = k8srepo_url
1034 vnfpkgop_id = str(uuid4())
1035 vnfpkgop_desc = {
1036 "_id": vnfpkgop_id,
1037 "operationState": "PROCESSING",
1038 "vnfPkgId": vnfpkg_id,
1039 "lcmOperationType": operation,
1040 "isAutomaticInvocation": False,
1041 "isCancelPending": False,
1042 "operationParams": indata,
1043 "links": {
1044 "self": "/osm/vnfpkgm/v1/vnfpkg_op_occs/" + vnfpkgop_id,
1045 "vnfpkg": "/osm/vnfpkgm/v1/vnf_packages/" + vnfpkg_id,
1046 }
1047 }
1048 self.format_on_new(vnfpkgop_desc, session["project_id"], make_public=session["public"])
1049 ctime = vnfpkgop_desc["_admin"]["created"]
1050 vnfpkgop_desc["statusEnteredTime"] = ctime
1051 vnfpkgop_desc["startTime"] = ctime
1052 self.db.create(self.topic, vnfpkgop_desc)
1053 rollback.append({"topic": self.topic, "_id": vnfpkgop_id})
1054 self.msg.write(self.topic_msg, operation, vnfpkgop_desc)
1055 return vnfpkgop_id, None