1 # -*- coding: utf-8 -*-
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
23 from hashlib
import md5
24 from osm_common
.dbbase
import DbException
, deep_update_rfc7396
25 from http
import HTTPStatus
27 from uuid
import uuid4
28 from re
import fullmatch
29 from osm_nbi
.validation
import (
36 from osm_nbi
.base_topic
import BaseTopic
, EngineException
, get_iterable
37 from osm_im
import etsi_nfv_vnfd
, etsi_nfv_nsd
38 from osm_im
.nst
import nst
as nst_im
39 from pyangbind
.lib
.serialise
import pybindJSONDecoder
40 import pyangbind
.lib
.pybindJSON
as pybindJSON
41 from osm_nbi
import utils
43 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
46 class DescriptorTopic(BaseTopic
):
47 def __init__(self
, db
, fs
, msg
, auth
):
48 BaseTopic
.__init
__(self
, db
, fs
, msg
, auth
)
50 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
51 final_content
= super().check_conflict_on_edit(
52 session
, final_content
, edit_content
, _id
55 def _check_unique_id_name(descriptor
, position
=""):
56 for desc_key
, desc_item
in descriptor
.items():
57 if isinstance(desc_item
, list) and desc_item
:
60 for index
, list_item
in enumerate(desc_item
):
61 if isinstance(list_item
, dict):
62 _check_unique_id_name(
63 list_item
, "{}.{}[{}]".format(position
, desc_key
, index
)
67 list_item
.get("id") or list_item
.get("name")
69 desc_item_id
= "id" if list_item
.get("id") else "name"
70 if desc_item_id
and list_item
.get(desc_item_id
):
71 if list_item
[desc_item_id
] in used_ids
:
72 position
= "{}.{}[{}]".format(
73 position
, desc_key
, index
75 raise EngineException(
76 "Error: identifier {} '{}' is not unique and repeats at '{}'".format(
78 list_item
[desc_item_id
],
81 HTTPStatus
.UNPROCESSABLE_ENTITY
,
83 used_ids
.append(list_item
[desc_item_id
])
85 _check_unique_id_name(final_content
)
86 # 1. validate again with pyangbind
87 # 1.1. remove internal keys
89 for k
in ("_id", "_admin"):
90 if k
in final_content
:
91 internal_keys
[k
] = final_content
.pop(k
)
92 storage_params
= internal_keys
["_admin"].get("storage")
93 serialized
= self
._validate
_input
_new
(
94 final_content
, storage_params
, session
["force"]
97 # 1.2. modify final_content with a serialized version
98 final_content
= copy
.deepcopy(serialized
)
99 # 1.3. restore internal keys
100 for k
, v
in internal_keys
.items():
105 # 2. check that this id is not present
106 if "id" in edit_content
:
107 _filter
= self
._get
_project
_filter
(session
)
109 _filter
["id"] = final_content
["id"]
110 _filter
["_id.neq"] = _id
112 if self
.db
.get_one(self
.topic
, _filter
, fail_on_empty
=False):
113 raise EngineException(
114 "{} with id '{}' already exists for this project".format(
115 self
.topic
[:-1], final_content
["id"]
123 def format_on_new(content
, project_id
=None, make_public
=False):
124 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
125 content
["_admin"]["onboardingState"] = "CREATED"
126 content
["_admin"]["operationalState"] = "DISABLED"
127 content
["_admin"]["usageState"] = "NOT_IN_USE"
129 def delete_extra(self
, session
, _id
, db_content
, not_send_msg
=None):
131 Deletes file system storage associated with the descriptor
132 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
133 :param _id: server internal id
134 :param db_content: The database content of the descriptor
135 :param not_send_msg: To not send message (False) or store content (list) instead
136 :return: None if ok or raises EngineException with the problem
138 self
.fs
.file_delete(_id
, ignore_non_exist
=True)
139 self
.fs
.file_delete(_id
+ "_", ignore_non_exist
=True) # remove temp folder
142 def get_one_by_id(db
, session
, topic
, id):
143 # find owned by this project
144 _filter
= BaseTopic
._get
_project
_filter
(session
)
146 desc_list
= db
.get_list(topic
, _filter
)
147 if len(desc_list
) == 1:
149 elif len(desc_list
) > 1:
151 "Found more than one {} with id='{}' belonging to this project".format(
157 # not found any: try to find public
158 _filter
= BaseTopic
._get
_project
_filter
(session
)
160 desc_list
= db
.get_list(topic
, _filter
)
163 "Not found any {} with id='{}'".format(topic
[:-1], id),
164 HTTPStatus
.NOT_FOUND
,
166 elif len(desc_list
) == 1:
170 "Found more than one public {} with id='{}'; and no one belonging to this project".format(
176 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
178 Creates a new almost empty DISABLED entry into database. Due to SOL005, it does not follow normal procedure.
179 Creating a VNFD or NSD is done in two steps: 1. Creates an empty descriptor (this step) and 2) upload content
180 (self.upload_content)
181 :param rollback: list to append created items at database in case a rollback may to be done
182 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
183 :param indata: data to be inserted
184 :param kwargs: used to override the indata descriptor
185 :param headers: http request headers
186 :return: _id, None: identity of the inserted data; and None as there is not any operation
189 # No needed to capture exceptions
191 self
.check_quota(session
)
195 if "userDefinedData" in indata
:
196 indata
= indata
["userDefinedData"]
198 # Override descriptor with query string kwargs
199 self
._update
_input
_with
_kwargs
(indata
, kwargs
)
200 # uncomment when this method is implemented.
201 # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors
202 # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"])
204 content
= {"_admin": {"userDefinedData": indata
}}
206 content
, session
["project_id"], make_public
=session
["public"]
208 _id
= self
.db
.create(self
.topic
, content
)
209 rollback
.append({"topic": self
.topic
, "_id": _id
})
210 self
._send
_msg
("created", {"_id": _id
})
213 def upload_content(self
, session
, _id
, indata
, kwargs
, headers
):
215 Used for receiving content by chunks (with a transaction_id header and/or gzip file. It will store and extract)
216 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
217 :param _id : the nsd,vnfd is already created, this is the id
218 :param indata: http body request
219 :param kwargs: user query string to override parameters. NOT USED
220 :param headers: http request headers
221 :return: True if package is completely uploaded or False if partial content has been uploded
222 Raise exception on error
224 # Check that _id exists and it is valid
225 current_desc
= self
.show(session
, _id
)
227 content_range_text
= headers
.get("Content-Range")
228 expected_md5
= headers
.get("Content-File-MD5")
230 content_type
= headers
.get("Content-Type")
233 and "application/gzip" in content_type
234 or "application/x-gzip" in content_type
235 or "application/zip" in content_type
238 filename
= headers
.get("Content-Filename")
240 filename
= "package.tar.gz" if compressed
else "package"
241 # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266
246 if content_range_text
:
248 content_range_text
.replace("-", " ").replace("/", " ").split()
251 content_range
[0] != "bytes"
252 ): # TODO check x<y not negative < total....
254 start
= int(content_range
[1])
255 end
= int(content_range
[2]) + 1
256 total
= int(content_range
[3])
261 ) # all the content is upload here and if ok, it is rename from id_ to is folder
264 if not self
.fs
.file_exists(temp_folder
, "dir"):
265 raise EngineException(
266 "invalid Transaction-Id header", HTTPStatus
.NOT_FOUND
269 self
.fs
.file_delete(temp_folder
, ignore_non_exist
=True)
270 self
.fs
.mkdir(temp_folder
)
271 fs_rollback
.append(temp_folder
)
273 storage
= self
.fs
.get_params()
274 storage
["folder"] = _id
276 file_path
= (temp_folder
, filename
)
277 if self
.fs
.file_exists(file_path
, "file"):
278 file_size
= self
.fs
.file_size(file_path
)
281 if file_size
!= start
:
282 raise EngineException(
283 "invalid Content-Range start sequence, expected '{}' but received '{}'".format(
286 HTTPStatus
.REQUESTED_RANGE_NOT_SATISFIABLE
,
288 file_pkg
= self
.fs
.file_open(file_path
, "a+b")
289 if isinstance(indata
, dict):
290 indata_text
= yaml
.safe_dump(indata
, indent
=4, default_flow_style
=False)
291 file_pkg
.write(indata_text
.encode(encoding
="utf-8"))
295 indata_text
= indata
.read(4096)
296 indata_len
+= len(indata_text
)
299 file_pkg
.write(indata_text
)
300 if content_range_text
:
301 if indata_len
!= end
- start
:
302 raise EngineException(
303 "Mismatch between Content-Range header {}-{} and body length of {}".format(
304 start
, end
- 1, indata_len
306 HTTPStatus
.REQUESTED_RANGE_NOT_SATISFIABLE
,
309 # TODO update to UPLOADING
316 chunk_data
= file_pkg
.read(1024)
318 file_md5
.update(chunk_data
)
319 chunk_data
= file_pkg
.read(1024)
320 if expected_md5
!= file_md5
.hexdigest():
321 raise EngineException("Error, MD5 mismatch", HTTPStatus
.CONFLICT
)
323 if compressed
== "gzip":
324 tar
= tarfile
.open(mode
="r", fileobj
=file_pkg
)
325 descriptor_file_name
= None
327 tarname
= tarinfo
.name
328 tarname_path
= tarname
.split("/")
330 not tarname_path
[0] or ".." in tarname_path
331 ): # if start with "/" means absolute path
332 raise EngineException(
333 "Absolute path or '..' are not allowed for package descriptor tar.gz"
335 if len(tarname_path
) == 1 and not tarinfo
.isdir():
336 raise EngineException(
337 "All files must be inside a dir for package descriptor tar.gz"
340 tarname
.endswith(".yaml")
341 or tarname
.endswith(".json")
342 or tarname
.endswith(".yml")
344 storage
["pkg-dir"] = tarname_path
[0]
345 if len(tarname_path
) == 2:
346 if descriptor_file_name
:
347 raise EngineException(
348 "Found more than one descriptor file at package descriptor tar.gz"
350 descriptor_file_name
= tarname
351 if not descriptor_file_name
:
352 raise EngineException(
353 "Not found any descriptor file at package descriptor tar.gz"
355 storage
["descriptor"] = descriptor_file_name
356 storage
["zipfile"] = filename
357 self
.fs
.file_extract(tar
, temp_folder
)
358 with self
.fs
.file_open(
359 (temp_folder
, descriptor_file_name
), "r"
360 ) as descriptor_file
:
361 content
= descriptor_file
.read()
363 content
= file_pkg
.read()
364 storage
["descriptor"] = descriptor_file_name
= filename
366 if descriptor_file_name
.endswith(".json"):
367 error_text
= "Invalid json format "
368 indata
= json
.load(content
)
370 error_text
= "Invalid yaml format "
371 indata
= yaml
.load(content
, Loader
=yaml
.SafeLoader
)
373 current_desc
["_admin"]["storage"] = storage
374 current_desc
["_admin"]["onboardingState"] = "ONBOARDED"
375 current_desc
["_admin"]["operationalState"] = "ENABLED"
377 indata
= self
._remove
_envelop
(indata
)
379 # Override descriptor with query string kwargs
381 self
._update
_input
_with
_kwargs
(indata
, kwargs
)
383 deep_update_rfc7396(current_desc
, indata
)
384 current_desc
= self
.check_conflict_on_edit(
385 session
, current_desc
, indata
, _id
=_id
387 current_desc
["_admin"]["modified"] = time()
388 self
.db
.replace(self
.topic
, _id
, current_desc
)
389 self
.fs
.dir_rename(temp_folder
, _id
)
393 self
._send
_msg
("edited", indata
)
395 # TODO if descriptor has changed because kwargs update content and remove cached zip
396 # TODO if zip is not present creates one
399 except EngineException
:
402 raise EngineException(
403 "invalid Content-Range header format. Expected 'bytes start-end/total'",
404 HTTPStatus
.REQUESTED_RANGE_NOT_SATISFIABLE
,
407 raise EngineException(
408 "invalid upload transaction sequence: '{}'".format(e
),
409 HTTPStatus
.BAD_REQUEST
,
411 except tarfile
.ReadError
as e
:
412 raise EngineException(
413 "invalid file content {}".format(e
), HTTPStatus
.BAD_REQUEST
415 except (ValueError, yaml
.YAMLError
) as e
:
416 raise EngineException(error_text
+ str(e
))
417 except ValidationError
as e
:
418 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
422 for file in fs_rollback
:
423 self
.fs
.file_delete(file, ignore_non_exist
=True)
425 def get_file(self
, session
, _id
, path
=None, accept_header
=None):
427 Return the file content of a vnfd or nsd
428 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
429 :param _id: Identity of the vnfd, nsd
430 :param path: artifact path or "$DESCRIPTOR" or None
431 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
432 :return: opened file plus Accept format or raises an exception
434 accept_text
= accept_zip
= False
436 if "text/plain" in accept_header
or "*/*" in accept_header
:
438 if "application/zip" in accept_header
or "*/*" in accept_header
:
439 accept_zip
= "application/zip"
440 elif "application/gzip" in accept_header
:
441 accept_zip
= "application/gzip"
443 if not accept_text
and not accept_zip
:
444 raise EngineException(
445 "provide request header 'Accept' with 'application/zip' or 'text/plain'",
446 http_code
=HTTPStatus
.NOT_ACCEPTABLE
,
449 content
= self
.show(session
, _id
)
450 if content
["_admin"]["onboardingState"] != "ONBOARDED":
451 raise EngineException(
452 "Cannot get content because this resource is not at 'ONBOARDED' state. "
453 "onboardingState is {}".format(content
["_admin"]["onboardingState"]),
454 http_code
=HTTPStatus
.CONFLICT
,
456 storage
= content
["_admin"]["storage"]
457 if path
is not None and path
!= "$DESCRIPTOR": # artifacts
458 if not storage
.get("pkg-dir"):
459 raise EngineException(
460 "Packages does not contains artifacts",
461 http_code
=HTTPStatus
.BAD_REQUEST
,
463 if self
.fs
.file_exists(
464 (storage
["folder"], storage
["pkg-dir"], *path
), "dir"
466 folder_content
= self
.fs
.dir_ls(
467 (storage
["folder"], storage
["pkg-dir"], *path
)
469 return folder_content
, "text/plain"
470 # TODO manage folders in http
474 (storage
["folder"], storage
["pkg-dir"], *path
), "rb"
476 "application/octet-stream",
479 # pkgtype accept ZIP TEXT -> result
480 # manyfiles yes X -> zip
482 # onefile yes no -> zip
484 contain_many_files
= False
485 if storage
.get("pkg-dir"):
486 # check if there are more than one file in the package, ignoring checksums.txt.
487 pkg_files
= self
.fs
.dir_ls((storage
["folder"], storage
["pkg-dir"]))
488 if len(pkg_files
) >= 3 or (
489 len(pkg_files
) == 2 and "checksums.txt" not in pkg_files
491 contain_many_files
= True
492 if accept_text
and (not contain_many_files
or path
== "$DESCRIPTOR"):
494 self
.fs
.file_open((storage
["folder"], storage
["descriptor"]), "r"),
497 elif contain_many_files
and not accept_zip
:
498 raise EngineException(
499 "Packages that contains several files need to be retrieved with 'application/zip'"
501 http_code
=HTTPStatus
.NOT_ACCEPTABLE
,
504 if not storage
.get("zipfile"):
505 # TODO generate zipfile if not present
506 raise EngineException(
507 "Only allowed 'text/plain' Accept header for this descriptor. To be solved in "
509 http_code
=HTTPStatus
.NOT_ACCEPTABLE
,
512 self
.fs
.file_open((storage
["folder"], storage
["zipfile"]), "rb"),
516 def _remove_yang_prefixes_from_descriptor(self
, descriptor
):
518 for k
, v
in descriptor
.items():
520 if isinstance(v
, dict):
521 new_v
= self
._remove
_yang
_prefixes
_from
_descriptor
(v
)
522 elif isinstance(v
, list):
525 if isinstance(x
, dict):
526 new_v
.append(self
._remove
_yang
_prefixes
_from
_descriptor
(x
))
529 new_descriptor
[k
.split(":")[-1]] = new_v
530 return new_descriptor
532 def pyangbind_validation(self
, item
, data
, force
=False):
533 raise EngineException(
534 "Not possible to validate '{}' item".format(item
),
535 http_code
=HTTPStatus
.INTERNAL_SERVER_ERROR
,
538 def _validate_input_edit(self
, indata
, content
, force
=False):
539 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
542 if "_admin" not in indata
:
543 indata
["_admin"] = {}
545 if "operationalState" in indata
:
546 if indata
["operationalState"] in ("ENABLED", "DISABLED"):
547 indata
["_admin"]["operationalState"] = indata
.pop("operationalState")
549 raise EngineException(
550 "State '{}' is not a valid operational state".format(
551 indata
["operationalState"]
553 http_code
=HTTPStatus
.BAD_REQUEST
,
556 # In the case of user defined data, we need to put the data in the root of the object
557 # to preserve current expected behaviour
558 if "userDefinedData" in indata
:
559 data
= indata
.pop("userDefinedData")
560 if type(data
) == dict:
561 indata
["_admin"]["userDefinedData"] = data
563 raise EngineException(
564 "userDefinedData should be an object, but is '{}' instead".format(
567 http_code
=HTTPStatus
.BAD_REQUEST
,
571 "operationalState" in indata
["_admin"]
572 and content
["_admin"]["operationalState"]
573 == indata
["_admin"]["operationalState"]
575 raise EngineException(
576 "operationalState already {}".format(
577 content
["_admin"]["operationalState"]
579 http_code
=HTTPStatus
.CONFLICT
,
585 class VnfdTopic(DescriptorTopic
):
589 def __init__(self
, db
, fs
, msg
, auth
):
590 DescriptorTopic
.__init
__(self
, db
, fs
, msg
, auth
)
592 def pyangbind_validation(self
, item
, data
, force
=False):
593 if self
._descriptor
_data
_is
_in
_old
_format
(data
):
594 raise EngineException(
595 "ERROR: Unsupported descriptor format. Please, use an ETSI SOL006 descriptor.",
596 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
599 myvnfd
= etsi_nfv_vnfd
.etsi_nfv_vnfd()
600 pybindJSONDecoder
.load_ietf_json(
601 {"etsi-nfv-vnfd:vnfd": data
},
608 out
= pybindJSON
.dumps(myvnfd
, mode
="ietf")
609 desc_out
= self
._remove
_envelop
(yaml
.safe_load(out
))
610 desc_out
= self
._remove
_yang
_prefixes
_from
_descriptor
(desc_out
)
611 return utils
.deep_update_dict(data
, desc_out
)
612 except Exception as e
:
613 raise EngineException(
614 "Error in pyangbind validation: {}".format(str(e
)),
615 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
619 def _descriptor_data_is_in_old_format(data
):
620 return ("vnfd-catalog" in data
) or ("vnfd:vnfd-catalog" in data
)
623 def _remove_envelop(indata
=None):
626 clean_indata
= indata
628 if clean_indata
.get("etsi-nfv-vnfd:vnfd"):
629 if not isinstance(clean_indata
["etsi-nfv-vnfd:vnfd"], dict):
630 raise EngineException("'etsi-nfv-vnfd:vnfd' must be a dict")
631 clean_indata
= clean_indata
["etsi-nfv-vnfd:vnfd"]
632 elif clean_indata
.get("vnfd"):
633 if not isinstance(clean_indata
["vnfd"], dict):
634 raise EngineException("'vnfd' must be dict")
635 clean_indata
= clean_indata
["vnfd"]
639 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
640 final_content
= super().check_conflict_on_edit(
641 session
, final_content
, edit_content
, _id
647 for vdu
in get_iterable(final_content
.get("vdu")):
648 if vdu
.get("pdu-type"):
653 final_content
["_admin"]["type"] = "hnfd" if contains_vdu
else "pnfd"
655 final_content
["_admin"]["type"] = "vnfd"
656 # if neither vud nor pdu do not fill type
659 def check_conflict_on_del(self
, session
, _id
, db_content
):
661 Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note
662 that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr
664 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
665 :param _id: vnfd internal id
666 :param db_content: The database content of the _id.
667 :return: None or raises EngineException with the conflict
671 descriptor
= db_content
672 descriptor_id
= descriptor
.get("id")
673 if not descriptor_id
: # empty vnfd not uploaded
676 _filter
= self
._get
_project
_filter
(session
)
678 # check vnfrs using this vnfd
679 _filter
["vnfd-id"] = _id
680 if self
.db
.get_list("vnfrs", _filter
):
681 raise EngineException(
682 "There is at least one VNF instance using this descriptor",
683 http_code
=HTTPStatus
.CONFLICT
,
686 # check NSD referencing this VNFD
687 del _filter
["vnfd-id"]
688 _filter
["vnfd-id"] = descriptor_id
689 if self
.db
.get_list("nsds", _filter
):
690 raise EngineException(
691 "There is at least one NS package referencing this descriptor",
692 http_code
=HTTPStatus
.CONFLICT
,
695 def _validate_input_new(self
, indata
, storage_params
, force
=False):
696 indata
.pop("onboardingState", None)
697 indata
.pop("operationalState", None)
698 indata
.pop("usageState", None)
699 indata
.pop("links", None)
701 indata
= self
.pyangbind_validation("vnfds", indata
, force
)
702 # Cross references validation in the descriptor
704 self
.validate_mgmt_interface_connection_point(indata
)
706 for vdu
in get_iterable(indata
.get("vdu")):
707 self
.validate_vdu_internal_connection_points(vdu
)
708 self
._validate
_vdu
_cloud
_init
_in
_package
(storage_params
, vdu
, indata
)
709 self
._validate
_vdu
_charms
_in
_package
(storage_params
, indata
)
711 self
._validate
_vnf
_charms
_in
_package
(storage_params
, indata
)
713 self
.validate_external_connection_points(indata
)
714 self
.validate_internal_virtual_links(indata
)
715 self
.validate_monitoring_params(indata
)
716 self
.validate_scaling_group_descriptor(indata
)
721 def validate_mgmt_interface_connection_point(indata
):
722 if not indata
.get("vdu"):
724 if not indata
.get("mgmt-cp"):
725 raise EngineException(
726 "'mgmt-cp' is a mandatory field and it is not defined",
727 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
730 for cp
in get_iterable(indata
.get("ext-cpd")):
731 if cp
["id"] == indata
["mgmt-cp"]:
734 raise EngineException(
735 "mgmt-cp='{}' must match an existing ext-cpd".format(indata
["mgmt-cp"]),
736 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
740 def validate_vdu_internal_connection_points(vdu
):
742 for cpd
in get_iterable(vdu
.get("int-cpd")):
743 cpd_id
= cpd
.get("id")
744 if cpd_id
and cpd_id
in int_cpds
:
745 raise EngineException(
746 "vdu[id='{}']:int-cpd[id='{}'] is already used by other int-cpd".format(
749 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
754 def validate_external_connection_points(indata
):
755 all_vdus_int_cpds
= set()
756 for vdu
in get_iterable(indata
.get("vdu")):
757 for int_cpd
in get_iterable(vdu
.get("int-cpd")):
758 all_vdus_int_cpds
.add((vdu
.get("id"), int_cpd
.get("id")))
761 for cpd
in get_iterable(indata
.get("ext-cpd")):
762 cpd_id
= cpd
.get("id")
763 if cpd_id
and cpd_id
in ext_cpds
:
764 raise EngineException(
765 "ext-cpd[id='{}'] is already used by other ext-cpd".format(cpd_id
),
766 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
770 int_cpd
= cpd
.get("int-cpd")
772 if (int_cpd
.get("vdu-id"), int_cpd
.get("cpd")) not in all_vdus_int_cpds
:
773 raise EngineException(
774 "ext-cpd[id='{}']:int-cpd must match an existing vdu int-cpd".format(
777 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
779 # TODO: Validate k8s-cluster-net points to a valid k8s-cluster:nets ?
781 def _validate_vdu_charms_in_package(self
, storage_params
, indata
):
782 for df
in indata
["df"]:
784 "lcm-operations-configuration" in df
785 and "operate-vnf-op-config" in df
["lcm-operations-configuration"]
787 configs
= df
["lcm-operations-configuration"][
788 "operate-vnf-op-config"
790 vdus
= df
.get("vdu-profile", [])
792 for config
in configs
:
793 if config
["id"] == vdu
["id"] and utils
.find_in_list(
794 config
.get("execution-environment-list", []),
795 lambda ee
: "juju" in ee
,
797 if not self
._validate
_package
_folders
(
798 storage_params
, "charms"
800 raise EngineException(
801 "Charm defined in vnf[id={}] but not present in "
802 "package".format(indata
["id"])
805 def _validate_vdu_cloud_init_in_package(self
, storage_params
, vdu
, indata
):
806 if not vdu
.get("cloud-init-file"):
808 if not self
._validate
_package
_folders
(
809 storage_params
, "cloud_init", vdu
["cloud-init-file"]
811 raise EngineException(
812 "Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
813 "package".format(indata
["id"], vdu
["id"])
816 def _validate_vnf_charms_in_package(self
, storage_params
, indata
):
817 # Get VNF configuration through new container
818 for deployment_flavor
in indata
.get("df", []):
819 if "lcm-operations-configuration" not in deployment_flavor
:
822 "operate-vnf-op-config"
823 not in deployment_flavor
["lcm-operations-configuration"]
826 for day_1_2_config
in deployment_flavor
["lcm-operations-configuration"][
827 "operate-vnf-op-config"
829 if day_1_2_config
["id"] == indata
["id"]:
830 if utils
.find_in_list(
831 day_1_2_config
.get("execution-environment-list", []),
832 lambda ee
: "juju" in ee
,
834 if not self
._validate
_package
_folders
(storage_params
, "charms"):
835 raise EngineException(
836 "Charm defined in vnf[id={}] but not present in "
837 "package".format(indata
["id"])
840 def _validate_package_folders(self
, storage_params
, folder
, file=None):
841 if not storage_params
or not storage_params
.get("pkg-dir"):
844 if self
.fs
.file_exists("{}_".format(storage_params
["folder"]), "dir"):
845 f
= "{}_/{}/{}".format(
846 storage_params
["folder"], storage_params
["pkg-dir"], folder
849 f
= "{}/{}/{}".format(
850 storage_params
["folder"], storage_params
["pkg-dir"], folder
853 return self
.fs
.file_exists("{}/{}".format(f
, file), "file")
855 if self
.fs
.file_exists(f
, "dir"):
856 if self
.fs
.dir_ls(f
):
861 def validate_internal_virtual_links(indata
):
863 for ivld
in get_iterable(indata
.get("int-virtual-link-desc")):
864 ivld_id
= ivld
.get("id")
865 if ivld_id
and ivld_id
in all_ivld_ids
:
866 raise EngineException(
867 "Duplicated VLD id in int-virtual-link-desc[id={}]".format(ivld_id
),
868 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
871 all_ivld_ids
.add(ivld_id
)
873 for vdu
in get_iterable(indata
.get("vdu")):
874 for int_cpd
in get_iterable(vdu
.get("int-cpd")):
875 int_cpd_ivld_id
= int_cpd
.get("int-virtual-link-desc")
876 if int_cpd_ivld_id
and int_cpd_ivld_id
not in all_ivld_ids
:
877 raise EngineException(
878 "vdu[id='{}']:int-cpd[id='{}']:int-virtual-link-desc='{}' must match an existing "
879 "int-virtual-link-desc".format(
880 vdu
["id"], int_cpd
["id"], int_cpd_ivld_id
882 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
885 for df
in get_iterable(indata
.get("df")):
886 for vlp
in get_iterable(df
.get("virtual-link-profile")):
887 vlp_ivld_id
= vlp
.get("id")
888 if vlp_ivld_id
and vlp_ivld_id
not in all_ivld_ids
:
889 raise EngineException(
890 "df[id='{}']:virtual-link-profile='{}' must match an existing "
891 "int-virtual-link-desc".format(df
["id"], vlp_ivld_id
),
892 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
896 def validate_monitoring_params(indata
):
897 all_monitoring_params
= set()
898 for ivld
in get_iterable(indata
.get("int-virtual-link-desc")):
899 for mp
in get_iterable(ivld
.get("monitoring-parameters")):
901 if mp_id
and mp_id
in all_monitoring_params
:
902 raise EngineException(
903 "Duplicated monitoring-parameter id in "
904 "int-virtual-link-desc[id='{}']:monitoring-parameters[id='{}']".format(
907 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
910 all_monitoring_params
.add(mp_id
)
912 for vdu
in get_iterable(indata
.get("vdu")):
913 for mp
in get_iterable(vdu
.get("monitoring-parameter")):
915 if mp_id
and mp_id
in all_monitoring_params
:
916 raise EngineException(
917 "Duplicated monitoring-parameter id in "
918 "vdu[id='{}']:monitoring-parameter[id='{}']".format(
921 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
924 all_monitoring_params
.add(mp_id
)
926 for df
in get_iterable(indata
.get("df")):
927 for mp
in get_iterable(df
.get("monitoring-parameter")):
929 if mp_id
and mp_id
in all_monitoring_params
:
930 raise EngineException(
931 "Duplicated monitoring-parameter id in "
932 "df[id='{}']:monitoring-parameter[id='{}']".format(
935 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
938 all_monitoring_params
.add(mp_id
)
941 def validate_scaling_group_descriptor(indata
):
942 all_monitoring_params
= set()
943 for ivld
in get_iterable(indata
.get("int-virtual-link-desc")):
944 for mp
in get_iterable(ivld
.get("monitoring-parameters")):
945 all_monitoring_params
.add(mp
.get("id"))
947 for vdu
in get_iterable(indata
.get("vdu")):
948 for mp
in get_iterable(vdu
.get("monitoring-parameter")):
949 all_monitoring_params
.add(mp
.get("id"))
951 for df
in get_iterable(indata
.get("df")):
952 for mp
in get_iterable(df
.get("monitoring-parameter")):
953 all_monitoring_params
.add(mp
.get("id"))
955 for df
in get_iterable(indata
.get("df")):
956 for sa
in get_iterable(df
.get("scaling-aspect")):
957 for sp
in get_iterable(sa
.get("scaling-policy")):
958 for sc
in get_iterable(sp
.get("scaling-criteria")):
959 sc_monitoring_param
= sc
.get("vnf-monitoring-param-ref")
962 and sc_monitoring_param
not in all_monitoring_params
964 raise EngineException(
965 "df[id='{}']:scaling-aspect[id='{}']:scaling-policy"
966 "[name='{}']:scaling-criteria[name='{}']: "
967 "vnf-monitoring-param-ref='{}' not defined in any monitoring-param".format(
974 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
977 for sca
in get_iterable(sa
.get("scaling-config-action")):
979 "lcm-operations-configuration" not in df
980 or "operate-vnf-op-config"
981 not in df
["lcm-operations-configuration"]
982 or not utils
.find_in_list(
983 df
["lcm-operations-configuration"][
984 "operate-vnf-op-config"
986 lambda config
: config
["id"] == indata
["id"],
989 raise EngineException(
990 "'day1-2 configuration' not defined in the descriptor but it is "
991 "referenced by df[id='{}']:scaling-aspect[id='{}']:scaling-config-action".format(
994 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
996 for configuration
in get_iterable(
997 df
["lcm-operations-configuration"]["operate-vnf-op-config"].get(
1001 for primitive
in get_iterable(
1002 configuration
.get("config-primitive")
1006 == sca
["vnf-config-primitive-name-ref"]
1010 raise EngineException(
1011 "df[id='{}']:scaling-aspect[id='{}']:scaling-config-action:vnf-"
1012 "config-primitive-name-ref='{}' does not match any "
1013 "day1-2 configuration:config-primitive:name".format(
1016 sca
["vnf-config-primitive-name-ref"],
1018 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
1021 def delete_extra(self
, session
, _id
, db_content
, not_send_msg
=None):
1023 Deletes associate file system storage (via super)
1024 Deletes associated vnfpkgops from database.
1025 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1026 :param _id: server internal id
1027 :param db_content: The database content of the descriptor
1029 :raises: FsException in case of error while deleting associated storage
1031 super().delete_extra(session
, _id
, db_content
, not_send_msg
)
1032 self
.db
.del_list("vnfpkgops", {"vnfPkgId": _id
})
1034 def sol005_projection(self
, data
):
1035 data
["onboardingState"] = data
["_admin"]["onboardingState"]
1036 data
["operationalState"] = data
["_admin"]["operationalState"]
1037 data
["usageState"] = data
["_admin"]["usageState"]
1040 links
["self"] = {"href": "/vnfpkgm/v1/vnf_packages/{}".format(data
["_id"])}
1041 links
["vnfd"] = {"href": "/vnfpkgm/v1/vnf_packages/{}/vnfd".format(data
["_id"])}
1042 links
["packageContent"] = {
1043 "href": "/vnfpkgm/v1/vnf_packages/{}/package_content".format(data
["_id"])
1045 data
["_links"] = links
1047 return super().sol005_projection(data
)
1050 class NsdTopic(DescriptorTopic
):
1054 def __init__(self
, db
, fs
, msg
, auth
):
1055 DescriptorTopic
.__init
__(self
, db
, fs
, msg
, auth
)
1057 def pyangbind_validation(self
, item
, data
, force
=False):
1058 if self
._descriptor
_data
_is
_in
_old
_format
(data
):
1059 raise EngineException(
1060 "ERROR: Unsupported descriptor format. Please, use an ETSI SOL006 descriptor.",
1061 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
1064 nsd_vnf_profiles
= data
.get("df", [{}])[0].get("vnf-profile", [])
1065 mynsd
= etsi_nfv_nsd
.etsi_nfv_nsd()
1066 pybindJSONDecoder
.load_ietf_json(
1067 {"nsd": {"nsd": [data
]}},
1074 out
= pybindJSON
.dumps(mynsd
, mode
="ietf")
1075 desc_out
= self
._remove
_envelop
(yaml
.safe_load(out
))
1076 desc_out
= self
._remove
_yang
_prefixes
_from
_descriptor
(desc_out
)
1077 if nsd_vnf_profiles
:
1078 desc_out
["df"][0]["vnf-profile"] = nsd_vnf_profiles
1080 except Exception as e
:
1081 raise EngineException(
1082 "Error in pyangbind validation: {}".format(str(e
)),
1083 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
1087 def _descriptor_data_is_in_old_format(data
):
1088 return ("nsd-catalog" in data
) or ("nsd:nsd-catalog" in data
)
1091 def _remove_envelop(indata
=None):
1094 clean_indata
= indata
1096 if clean_indata
.get("nsd"):
1097 clean_indata
= clean_indata
["nsd"]
1098 elif clean_indata
.get("etsi-nfv-nsd:nsd"):
1099 clean_indata
= clean_indata
["etsi-nfv-nsd:nsd"]
1100 if clean_indata
.get("nsd"):
1102 not isinstance(clean_indata
["nsd"], list)
1103 or len(clean_indata
["nsd"]) != 1
1105 raise EngineException("'nsd' must be a list of only one element")
1106 clean_indata
= clean_indata
["nsd"][0]
1109 def _validate_input_new(self
, indata
, storage_params
, force
=False):
1110 indata
.pop("nsdOnboardingState", None)
1111 indata
.pop("nsdOperationalState", None)
1112 indata
.pop("nsdUsageState", None)
1114 indata
.pop("links", None)
1116 indata
= self
.pyangbind_validation("nsds", indata
, force
)
1117 # Cross references validation in the descriptor
1118 # TODO validata that if contains cloud-init-file or charms, have artifacts _admin.storage."pkg-dir" is not none
1119 for vld
in get_iterable(indata
.get("virtual-link-desc")):
1120 self
.validate_vld_mgmt_network_with_virtual_link_protocol_data(vld
, indata
)
1122 self
.validate_vnf_profiles_vnfd_id(indata
)
1127 def validate_vld_mgmt_network_with_virtual_link_protocol_data(vld
, indata
):
1128 if not vld
.get("mgmt-network"):
1130 vld_id
= vld
.get("id")
1131 for df
in get_iterable(indata
.get("df")):
1132 for vlp
in get_iterable(df
.get("virtual-link-profile")):
1133 if vld_id
and vld_id
== vlp
.get("virtual-link-desc-id"):
1134 if vlp
.get("virtual-link-protocol-data"):
1135 raise EngineException(
1136 "Error at df[id='{}']:virtual-link-profile[id='{}']:virtual-link-"
1137 "protocol-data You cannot set a virtual-link-protocol-data "
1138 "when mgmt-network is True".format(df
["id"], vlp
["id"]),
1139 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
1143 def validate_vnf_profiles_vnfd_id(indata
):
1144 all_vnfd_ids
= set(get_iterable(indata
.get("vnfd-id")))
1145 for df
in get_iterable(indata
.get("df")):
1146 for vnf_profile
in get_iterable(df
.get("vnf-profile")):
1147 vnfd_id
= vnf_profile
.get("vnfd-id")
1148 if vnfd_id
and vnfd_id
not in all_vnfd_ids
:
1149 raise EngineException(
1150 "Error at df[id='{}']:vnf_profile[id='{}']:vnfd-id='{}' "
1151 "does not match any vnfd-id".format(
1152 df
["id"], vnf_profile
["id"], vnfd_id
1154 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
1157 def _validate_input_edit(self
, indata
, content
, force
=False):
1158 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
1160 indata looks as follows:
1161 - In the new case (conformant)
1162 {'nsdOperationalState': 'DISABLED', 'userDefinedData': {'id': 'string23',
1163 '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}}
1164 - In the old case (backwards-compatible)
1165 {'id': 'string23', '_id': 'c6ddc544-cede-4b94-9ebe-be07b298a3c1', 'name': 'simon46'}
1167 if "_admin" not in indata
:
1168 indata
["_admin"] = {}
1170 if "nsdOperationalState" in indata
:
1171 if indata
["nsdOperationalState"] in ("ENABLED", "DISABLED"):
1172 indata
["_admin"]["operationalState"] = indata
.pop("nsdOperationalState")
1174 raise EngineException(
1175 "State '{}' is not a valid operational state".format(
1176 indata
["nsdOperationalState"]
1178 http_code
=HTTPStatus
.BAD_REQUEST
,
1181 # In the case of user defined data, we need to put the data in the root of the object
1182 # to preserve current expected behaviour
1183 if "userDefinedData" in indata
:
1184 data
= indata
.pop("userDefinedData")
1185 if type(data
) == dict:
1186 indata
["_admin"]["userDefinedData"] = data
1188 raise EngineException(
1189 "userDefinedData should be an object, but is '{}' instead".format(
1192 http_code
=HTTPStatus
.BAD_REQUEST
,
1195 "operationalState" in indata
["_admin"]
1196 and content
["_admin"]["operationalState"]
1197 == indata
["_admin"]["operationalState"]
1199 raise EngineException(
1200 "nsdOperationalState already {}".format(
1201 content
["_admin"]["operationalState"]
1203 http_code
=HTTPStatus
.CONFLICT
,
1207 def _check_descriptor_dependencies(self
, session
, descriptor
):
1209 Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd
1210 connection points are ok
1211 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1212 :param descriptor: descriptor to be inserted or edit
1213 :return: None or raises exception
1215 if session
["force"]:
1217 vnfds_index
= self
._get
_descriptor
_constituent
_vnfds
_index
(session
, descriptor
)
1219 # Cross references validation in the descriptor and vnfd connection point validation
1220 for df
in get_iterable(descriptor
.get("df")):
1221 self
.validate_df_vnf_profiles_constituent_connection_points(df
, vnfds_index
)
1223 def _get_descriptor_constituent_vnfds_index(self
, session
, descriptor
):
1225 if descriptor
.get("vnfd-id") and not session
["force"]:
1226 for vnfd_id
in get_iterable(descriptor
.get("vnfd-id")):
1227 query_filter
= self
._get
_project
_filter
(session
)
1228 query_filter
["id"] = vnfd_id
1229 vnf_list
= self
.db
.get_list("vnfds", query_filter
)
1231 raise EngineException(
1232 "Descriptor error at 'vnfd-id'='{}' references a non "
1233 "existing vnfd".format(vnfd_id
),
1234 http_code
=HTTPStatus
.CONFLICT
,
1236 vnfds_index
[vnfd_id
] = vnf_list
[0]
1240 def validate_df_vnf_profiles_constituent_connection_points(df
, vnfds_index
):
1241 for vnf_profile
in get_iterable(df
.get("vnf-profile")):
1242 vnfd
= vnfds_index
.get(vnf_profile
["vnfd-id"])
1243 all_vnfd_ext_cpds
= set()
1244 for ext_cpd
in get_iterable(vnfd
.get("ext-cpd")):
1245 if ext_cpd
.get("id"):
1246 all_vnfd_ext_cpds
.add(ext_cpd
.get("id"))
1248 for virtual_link
in get_iterable(
1249 vnf_profile
.get("virtual-link-connectivity")
1251 for vl_cpd
in get_iterable(virtual_link
.get("constituent-cpd-id")):
1252 vl_cpd_id
= vl_cpd
.get("constituent-cpd-id")
1253 if vl_cpd_id
and vl_cpd_id
not in all_vnfd_ext_cpds
:
1254 raise EngineException(
1255 "Error at df[id='{}']:vnf-profile[id='{}']:virtual-link-connectivity"
1256 "[virtual-link-profile-id='{}']:constituent-cpd-id='{}' references a "
1257 "non existing ext-cpd:id inside vnfd '{}'".format(
1260 virtual_link
["virtual-link-profile-id"],
1264 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
1267 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
1268 final_content
= super().check_conflict_on_edit(
1269 session
, final_content
, edit_content
, _id
1272 self
._check
_descriptor
_dependencies
(session
, final_content
)
1274 return final_content
1276 def check_conflict_on_del(self
, session
, _id
, db_content
):
1278 Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note
1279 that NSD can be public and be used by other projects.
1280 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1281 :param _id: nsd internal id
1282 :param db_content: The database content of the _id
1283 :return: None or raises EngineException with the conflict
1285 if session
["force"]:
1287 descriptor
= db_content
1288 descriptor_id
= descriptor
.get("id")
1289 if not descriptor_id
: # empty nsd not uploaded
1292 # check NSD used by NS
1293 _filter
= self
._get
_project
_filter
(session
)
1294 _filter
["nsd-id"] = _id
1295 if self
.db
.get_list("nsrs", _filter
):
1296 raise EngineException(
1297 "There is at least one NS instance using this descriptor",
1298 http_code
=HTTPStatus
.CONFLICT
,
1301 # check NSD referenced by NST
1302 del _filter
["nsd-id"]
1303 _filter
["netslice-subnet.ANYINDEX.nsd-ref"] = descriptor_id
1304 if self
.db
.get_list("nsts", _filter
):
1305 raise EngineException(
1306 "There is at least one NetSlice Template referencing this descriptor",
1307 http_code
=HTTPStatus
.CONFLICT
,
1310 def sol005_projection(self
, data
):
1311 data
["nsdOnboardingState"] = data
["_admin"]["onboardingState"]
1312 data
["nsdOperationalState"] = data
["_admin"]["operationalState"]
1313 data
["nsdUsageState"] = data
["_admin"]["usageState"]
1316 links
["self"] = {"href": "/nsd/v1/ns_descriptors/{}".format(data
["_id"])}
1317 links
["nsd_content"] = {
1318 "href": "/nsd/v1/ns_descriptors/{}/nsd_content".format(data
["_id"])
1320 data
["_links"] = links
1322 return super().sol005_projection(data
)
1325 class NstTopic(DescriptorTopic
):
1328 quota_name
= "slice_templates"
1330 def __init__(self
, db
, fs
, msg
, auth
):
1331 DescriptorTopic
.__init
__(self
, db
, fs
, msg
, auth
)
1333 def pyangbind_validation(self
, item
, data
, force
=False):
1336 pybindJSONDecoder
.load_ietf_json(
1344 out
= pybindJSON
.dumps(mynst
, mode
="ietf")
1345 desc_out
= self
._remove
_envelop
(yaml
.safe_load(out
))
1347 except Exception as e
:
1348 raise EngineException(
1349 "Error in pyangbind validation: {}".format(str(e
)),
1350 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
,
1354 def _remove_envelop(indata
=None):
1357 clean_indata
= indata
1359 if clean_indata
.get("nst"):
1361 not isinstance(clean_indata
["nst"], list)
1362 or len(clean_indata
["nst"]) != 1
1364 raise EngineException("'nst' must be a list only one element")
1365 clean_indata
= clean_indata
["nst"][0]
1366 elif clean_indata
.get("nst:nst"):
1368 not isinstance(clean_indata
["nst:nst"], list)
1369 or len(clean_indata
["nst:nst"]) != 1
1371 raise EngineException("'nst:nst' must be a list only one element")
1372 clean_indata
= clean_indata
["nst:nst"][0]
1375 def _validate_input_new(self
, indata
, storage_params
, force
=False):
1376 indata
.pop("onboardingState", None)
1377 indata
.pop("operationalState", None)
1378 indata
.pop("usageState", None)
1379 indata
= self
.pyangbind_validation("nsts", indata
, force
)
1380 return indata
.copy()
1382 def _check_descriptor_dependencies(self
, session
, descriptor
):
1384 Check that the dependent descriptors exist on a new descriptor or edition
1385 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1386 :param descriptor: descriptor to be inserted or edit
1387 :return: None or raises exception
1389 if not descriptor
.get("netslice-subnet"):
1391 for nsd
in descriptor
["netslice-subnet"]:
1392 nsd_id
= nsd
["nsd-ref"]
1393 filter_q
= self
._get
_project
_filter
(session
)
1394 filter_q
["id"] = nsd_id
1395 if not self
.db
.get_list("nsds", filter_q
):
1396 raise EngineException(
1397 "Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non "
1398 "existing nsd".format(nsd_id
),
1399 http_code
=HTTPStatus
.CONFLICT
,
1402 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
1403 final_content
= super().check_conflict_on_edit(
1404 session
, final_content
, edit_content
, _id
1407 self
._check
_descriptor
_dependencies
(session
, final_content
)
1408 return final_content
1410 def check_conflict_on_del(self
, session
, _id
, db_content
):
1412 Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note
1413 that NST can be public and be used by other projects.
1414 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1415 :param _id: nst internal id
1416 :param db_content: The database content of the _id.
1417 :return: None or raises EngineException with the conflict
1419 # TODO: Check this method
1420 if session
["force"]:
1422 # Get Network Slice Template from Database
1423 _filter
= self
._get
_project
_filter
(session
)
1424 _filter
["_admin.nst-id"] = _id
1425 if self
.db
.get_list("nsis", _filter
):
1426 raise EngineException(
1427 "there is at least one Netslice Instance using this descriptor",
1428 http_code
=HTTPStatus
.CONFLICT
,
1431 def sol005_projection(self
, data
):
1432 data
["onboardingState"] = data
["_admin"]["onboardingState"]
1433 data
["operationalState"] = data
["_admin"]["operationalState"]
1434 data
["usageState"] = data
["_admin"]["usageState"]
1437 links
["self"] = {"href": "/nst/v1/netslice_templates/{}".format(data
["_id"])}
1438 links
["nst"] = {"href": "/nst/v1/netslice_templates/{}/nst".format(data
["_id"])}
1439 data
["_links"] = links
1441 return super().sol005_projection(data
)
1444 class PduTopic(BaseTopic
):
1447 quota_name
= "pduds"
1448 schema_new
= pdu_new_schema
1449 schema_edit
= pdu_edit_schema
1451 def __init__(self
, db
, fs
, msg
, auth
):
1452 BaseTopic
.__init
__(self
, db
, fs
, msg
, auth
)
1455 def format_on_new(content
, project_id
=None, make_public
=False):
1456 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
1457 content
["_admin"]["onboardingState"] = "CREATED"
1458 content
["_admin"]["operationalState"] = "ENABLED"
1459 content
["_admin"]["usageState"] = "NOT_IN_USE"
1461 def check_conflict_on_del(self
, session
, _id
, db_content
):
1463 Check that there is not any vnfr that uses this PDU
1464 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1465 :param _id: pdu internal id
1466 :param db_content: The database content of the _id.
1467 :return: None or raises EngineException with the conflict
1469 if session
["force"]:
1472 _filter
= self
._get
_project
_filter
(session
)
1473 _filter
["vdur.pdu-id"] = _id
1474 if self
.db
.get_list("vnfrs", _filter
):
1475 raise EngineException(
1476 "There is at least one VNF instance using this PDU",
1477 http_code
=HTTPStatus
.CONFLICT
,
1481 class VnfPkgOpTopic(BaseTopic
):
1484 schema_new
= vnfpkgop_new_schema
1487 def __init__(self
, db
, fs
, msg
, auth
):
1488 BaseTopic
.__init
__(self
, db
, fs
, msg
, auth
)
1490 def edit(self
, session
, _id
, indata
=None, kwargs
=None, content
=None):
1491 raise EngineException(
1492 "Method 'edit' not allowed for topic '{}'".format(self
.topic
),
1493 HTTPStatus
.METHOD_NOT_ALLOWED
,
1496 def delete(self
, session
, _id
, dry_run
=False):
1497 raise EngineException(
1498 "Method 'delete' not allowed for topic '{}'".format(self
.topic
),
1499 HTTPStatus
.METHOD_NOT_ALLOWED
,
1502 def delete_list(self
, session
, filter_q
=None):
1503 raise EngineException(
1504 "Method 'delete_list' not allowed for topic '{}'".format(self
.topic
),
1505 HTTPStatus
.METHOD_NOT_ALLOWED
,
1508 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
1510 Creates a new entry into database.
1511 :param rollback: list to append created items at database in case a rollback may to be done
1512 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
1513 :param indata: data to be inserted
1514 :param kwargs: used to override the indata descriptor
1515 :param headers: http request headers
1516 :return: _id, op_id:
1517 _id: identity of the inserted data.
1520 self
._update
_input
_with
_kwargs
(indata
, kwargs
)
1521 validate_input(indata
, self
.schema_new
)
1522 vnfpkg_id
= indata
["vnfPkgId"]
1523 filter_q
= BaseTopic
._get
_project
_filter
(session
)
1524 filter_q
["_id"] = vnfpkg_id
1525 vnfd
= self
.db
.get_one("vnfds", filter_q
)
1526 operation
= indata
["lcmOperationType"]
1527 kdu_name
= indata
["kdu_name"]
1528 for kdu
in vnfd
.get("kdu", []):
1529 if kdu
["name"] == kdu_name
:
1530 helm_chart
= kdu
.get("helm-chart")
1531 juju_bundle
= kdu
.get("juju-bundle")
1534 raise EngineException(
1535 "Not found vnfd[id='{}']:kdu[name='{}']".format(vnfpkg_id
, kdu_name
)
1538 indata
["helm-chart"] = helm_chart
1539 match
= fullmatch(r
"([^/]*)/([^/]*)", helm_chart
)
1540 repo_name
= match
.group(1) if match
else None
1542 indata
["juju-bundle"] = juju_bundle
1543 match
= fullmatch(r
"([^/]*)/([^/]*)", juju_bundle
)
1544 repo_name
= match
.group(1) if match
else None
1546 raise EngineException(
1547 "Found neither 'helm-chart' nor 'juju-bundle' in vnfd[id='{}']:kdu[name='{}']".format(
1553 filter_q
["name"] = repo_name
1554 repo
= self
.db
.get_one("k8srepos", filter_q
)
1555 k8srepo_id
= repo
.get("_id")
1556 k8srepo_url
= repo
.get("url")
1560 indata
["k8srepoId"] = k8srepo_id
1561 indata
["k8srepo_url"] = k8srepo_url
1562 vnfpkgop_id
= str(uuid4())
1565 "operationState": "PROCESSING",
1566 "vnfPkgId": vnfpkg_id
,
1567 "lcmOperationType": operation
,
1568 "isAutomaticInvocation": False,
1569 "isCancelPending": False,
1570 "operationParams": indata
,
1572 "self": "/osm/vnfpkgm/v1/vnfpkg_op_occs/" + vnfpkgop_id
,
1573 "vnfpkg": "/osm/vnfpkgm/v1/vnf_packages/" + vnfpkg_id
,
1577 vnfpkgop_desc
, session
["project_id"], make_public
=session
["public"]
1579 ctime
= vnfpkgop_desc
["_admin"]["created"]
1580 vnfpkgop_desc
["statusEnteredTime"] = ctime
1581 vnfpkgop_desc
["startTime"] = ctime
1582 self
.db
.create(self
.topic
, vnfpkgop_desc
)
1583 rollback
.append({"topic": self
.topic
, "_id": vnfpkgop_id
})
1584 self
.msg
.write(self
.topic_msg
, operation
, vnfpkgop_desc
)
1585 return vnfpkgop_id
, None