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.
20 from hashlib
import md5
21 from osm_common
.dbbase
import DbException
, deep_update_rfc7396
22 from http
import HTTPStatus
24 from osm_nbi
.validation
import ValidationError
, pdu_new_schema
, pdu_edit_schema
25 from osm_nbi
.base_topic
import BaseTopic
, EngineException
, get_iterable
26 from osm_im
.vnfd
import vnfd
as vnfd_im
27 from osm_im
.nsd
import nsd
as nsd_im
28 from osm_im
.nst
import nst
as nst_im
29 from pyangbind
.lib
.serialise
import pybindJSONDecoder
30 import pyangbind
.lib
.pybindJSON
as pybindJSON
32 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
35 class DescriptorTopic(BaseTopic
):
37 def __init__(self
, db
, fs
, msg
, auth
):
38 BaseTopic
.__init
__(self
, db
, fs
, msg
, auth
)
40 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
41 super().check_conflict_on_edit(session
, final_content
, edit_content
, _id
)
43 def _check_unique_id_name(descriptor
, position
=""):
44 for desc_key
, desc_item
in descriptor
.items():
45 if isinstance(desc_item
, list) and desc_item
:
48 for index
, list_item
in enumerate(desc_item
):
49 if isinstance(list_item
, dict):
50 _check_unique_id_name(list_item
, "{}.{}[{}]"
51 .format(position
, desc_key
, index
))
53 if index
== 0 and (list_item
.get("id") or list_item
.get("name")):
54 desc_item_id
= "id" if list_item
.get("id") else "name"
55 if desc_item_id
and list_item
.get(desc_item_id
):
56 if list_item
[desc_item_id
] in used_ids
:
57 position
= "{}.{}[{}]".format(position
, desc_key
, index
)
58 raise EngineException("Error: identifier {} '{}' is not unique and repeats at '{}'"
59 .format(desc_item_id
, list_item
[desc_item_id
],
60 position
), HTTPStatus
.UNPROCESSABLE_ENTITY
)
61 used_ids
.append(list_item
[desc_item_id
])
62 _check_unique_id_name(final_content
)
63 # 1. validate again with pyangbind
64 # 1.1. remove internal keys
66 for k
in ("_id", "_admin"):
67 if k
in final_content
:
68 internal_keys
[k
] = final_content
.pop(k
)
69 storage_params
= internal_keys
["_admin"].get("storage")
70 serialized
= self
._validate
_input
_new
(final_content
, storage_params
, session
["force"])
71 # 1.2. modify final_content with a serialized version
73 final_content
.update(serialized
)
74 # 1.3. restore internal keys
75 for k
, v
in internal_keys
.items():
80 # 2. check that this id is not present
81 if "id" in edit_content
:
82 _filter
= self
._get
_project
_filter
(session
)
83 _filter
["id"] = final_content
["id"]
84 _filter
["_id.neq"] = _id
85 if self
.db
.get_one(self
.topic
, _filter
, fail_on_empty
=False):
86 raise EngineException("{} with id '{}' already exists for this project".format(self
.topic
[:-1],
91 def format_on_new(content
, project_id
=None, make_public
=False):
92 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
93 content
["_admin"]["onboardingState"] = "CREATED"
94 content
["_admin"]["operationalState"] = "DISABLED"
95 content
["_admin"]["usageState"] = "NOT_IN_USE"
97 def delete_extra(self
, session
, _id
, db_content
):
99 Deletes file system storage associated with the descriptor
100 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
101 :param _id: server internal id
102 :param db_content: The database content of the descriptor
103 :return: None if ok or raises EngineException with the problem
105 self
.fs
.file_delete(_id
, ignore_non_exist
=True)
106 self
.fs
.file_delete(_id
+ "_", ignore_non_exist
=True) # remove temp folder
109 def get_one_by_id(db
, session
, topic
, id):
110 # find owned by this project
111 _filter
= BaseTopic
._get
_project
_filter
(session
)
113 desc_list
= db
.get_list(topic
, _filter
)
114 if len(desc_list
) == 1:
116 elif len(desc_list
) > 1:
117 raise DbException("Found more than one {} with id='{}' belonging to this project".format(topic
[:-1], id),
120 # not found any: try to find public
121 _filter
= BaseTopic
._get
_project
_filter
(session
)
123 desc_list
= db
.get_list(topic
, _filter
)
125 raise DbException("Not found any {} with id='{}'".format(topic
[:-1], id), HTTPStatus
.NOT_FOUND
)
126 elif len(desc_list
) == 1:
129 raise DbException("Found more than one public {} with id='{}'; and no one belonging to this project".format(
130 topic
[:-1], id), HTTPStatus
.CONFLICT
)
132 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
134 Creates a new almost empty DISABLED entry into database. Due to SOL005, it does not follow normal procedure.
135 Creating a VNFD or NSD is done in two steps: 1. Creates an empty descriptor (this step) and 2) upload content
136 (self.upload_content)
137 :param rollback: list to append created items at database in case a rollback may to be done
138 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
139 :param indata: data to be inserted
140 :param kwargs: used to override the indata descriptor
141 :param headers: http request headers
142 :return: _id, None: identity of the inserted data; and None as there is not any operation
147 self
.check_quota(session
)
151 if "userDefinedData" in indata
:
152 indata
= indata
['userDefinedData']
154 # Override descriptor with query string kwargs
155 self
._update
_input
_with
_kwargs
(indata
, kwargs
)
156 # uncomment when this method is implemented.
157 # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors
158 # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"])
160 content
= {"_admin": {"userDefinedData": indata
}}
161 self
.format_on_new(content
, session
["project_id"], make_public
=session
["public"])
162 _id
= self
.db
.create(self
.topic
, content
)
163 rollback
.append({"topic": self
.topic
, "_id": _id
})
164 self
._send
_msg
("created", {"_id": _id
})
166 except ValidationError
as e
:
167 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
169 def upload_content(self
, session
, _id
, indata
, kwargs
, headers
):
171 Used for receiving content by chunks (with a transaction_id header and/or gzip file. It will store and extract)
172 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
173 :param _id : the nsd,vnfd is already created, this is the id
174 :param indata: http body request
175 :param kwargs: user query string to override parameters. NOT USED
176 :param headers: http request headers
177 :return: True if package is completely uploaded or False if partial content has been uploded
178 Raise exception on error
180 # Check that _id exists and it is valid
181 current_desc
= self
.show(session
, _id
)
183 content_range_text
= headers
.get("Content-Range")
184 expected_md5
= headers
.get("Content-File-MD5")
186 content_type
= headers
.get("Content-Type")
187 if content_type
and "application/gzip" in content_type
or "application/x-gzip" in content_type
or \
188 "application/zip" in content_type
:
190 filename
= headers
.get("Content-Filename")
192 filename
= "package.tar.gz" if compressed
else "package"
193 # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266
197 if content_range_text
:
198 content_range
= content_range_text
.replace("-", " ").replace("/", " ").split()
199 if content_range
[0] != "bytes": # TODO check x<y not negative < total....
201 start
= int(content_range
[1])
202 end
= int(content_range
[2]) + 1
203 total
= int(content_range
[3])
206 temp_folder
= _id
+ "_" # all the content is upload here and if ok, it is rename from id_ to is folder
209 if not self
.fs
.file_exists(temp_folder
, 'dir'):
210 raise EngineException("invalid Transaction-Id header", HTTPStatus
.NOT_FOUND
)
212 self
.fs
.file_delete(temp_folder
, ignore_non_exist
=True)
213 self
.fs
.mkdir(temp_folder
)
215 storage
= self
.fs
.get_params()
216 storage
["folder"] = _id
218 file_path
= (temp_folder
, filename
)
219 if self
.fs
.file_exists(file_path
, 'file'):
220 file_size
= self
.fs
.file_size(file_path
)
223 if file_size
!= start
:
224 raise EngineException("invalid Content-Range start sequence, expected '{}' but received '{}'".format(
225 file_size
, start
), HTTPStatus
.REQUESTED_RANGE_NOT_SATISFIABLE
)
226 file_pkg
= self
.fs
.file_open(file_path
, 'a+b')
227 if isinstance(indata
, dict):
228 indata_text
= yaml
.safe_dump(indata
, indent
=4, default_flow_style
=False)
229 file_pkg
.write(indata_text
.encode(encoding
="utf-8"))
233 indata_text
= indata
.read(4096)
234 indata_len
+= len(indata_text
)
237 file_pkg
.write(indata_text
)
238 if content_range_text
:
239 if indata_len
!= end
-start
:
240 raise EngineException("Mismatch between Content-Range header {}-{} and body length of {}".format(
241 start
, end
-1, indata_len
), HTTPStatus
.REQUESTED_RANGE_NOT_SATISFIABLE
)
243 # TODO update to UPLOADING
250 chunk_data
= file_pkg
.read(1024)
252 file_md5
.update(chunk_data
)
253 chunk_data
= file_pkg
.read(1024)
254 if expected_md5
!= file_md5
.hexdigest():
255 raise EngineException("Error, MD5 mismatch", HTTPStatus
.CONFLICT
)
257 if compressed
== "gzip":
258 tar
= tarfile
.open(mode
='r', fileobj
=file_pkg
)
259 descriptor_file_name
= None
261 tarname
= tarinfo
.name
262 tarname_path
= tarname
.split("/")
263 if not tarname_path
[0] or ".." in tarname_path
: # if start with "/" means absolute path
264 raise EngineException("Absolute path or '..' are not allowed for package descriptor tar.gz")
265 if len(tarname_path
) == 1 and not tarinfo
.isdir():
266 raise EngineException("All files must be inside a dir for package descriptor tar.gz")
267 if tarname
.endswith(".yaml") or tarname
.endswith(".json") or tarname
.endswith(".yml"):
268 storage
["pkg-dir"] = tarname_path
[0]
269 if len(tarname_path
) == 2:
270 if descriptor_file_name
:
271 raise EngineException(
272 "Found more than one descriptor file at package descriptor tar.gz")
273 descriptor_file_name
= tarname
274 if not descriptor_file_name
:
275 raise EngineException("Not found any descriptor file at package descriptor tar.gz")
276 storage
["descriptor"] = descriptor_file_name
277 storage
["zipfile"] = filename
278 self
.fs
.file_extract(tar
, temp_folder
)
279 with self
.fs
.file_open((temp_folder
, descriptor_file_name
), "r") as descriptor_file
:
280 content
= descriptor_file
.read()
282 content
= file_pkg
.read()
283 storage
["descriptor"] = descriptor_file_name
= filename
285 if descriptor_file_name
.endswith(".json"):
286 error_text
= "Invalid json format "
287 indata
= json
.load(content
)
289 error_text
= "Invalid yaml format "
290 indata
= yaml
.load(content
, Loader
=yaml
.SafeLoader
)
292 current_desc
["_admin"]["storage"] = storage
293 current_desc
["_admin"]["onboardingState"] = "ONBOARDED"
294 current_desc
["_admin"]["operationalState"] = "ENABLED"
296 indata
= self
._remove
_envelop
(indata
)
298 # Override descriptor with query string kwargs
300 self
._update
_input
_with
_kwargs
(indata
, kwargs
)
301 # it will call overrides method at VnfdTopic or NsdTopic
302 # indata = self._validate_input_edit(indata, force=session["force"])
304 deep_update_rfc7396(current_desc
, indata
)
305 self
.check_conflict_on_edit(session
, current_desc
, indata
, _id
=_id
)
306 current_desc
["_admin"]["modified"] = time()
307 self
.db
.replace(self
.topic
, _id
, current_desc
)
308 self
.fs
.dir_rename(temp_folder
, _id
)
311 self
._send
_msg
("edited", indata
)
313 # TODO if descriptor has changed because kwargs update content and remove cached zip
314 # TODO if zip is not present creates one
317 except EngineException
:
320 raise EngineException("invalid Content-Range header format. Expected 'bytes start-end/total'",
321 HTTPStatus
.REQUESTED_RANGE_NOT_SATISFIABLE
)
323 raise EngineException("invalid upload transaction sequence: '{}'".format(e
), HTTPStatus
.BAD_REQUEST
)
324 except tarfile
.ReadError
as e
:
325 raise EngineException("invalid file content {}".format(e
), HTTPStatus
.BAD_REQUEST
)
326 except (ValueError, yaml
.YAMLError
) as e
:
327 raise EngineException(error_text
+ str(e
))
328 except ValidationError
as e
:
329 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
334 def get_file(self
, session
, _id
, path
=None, accept_header
=None):
336 Return the file content of a vnfd or nsd
337 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
338 :param _id: Identity of the vnfd, nsd
339 :param path: artifact path or "$DESCRIPTOR" or None
340 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
341 :return: opened file plus Accept format or raises an exception
343 accept_text
= accept_zip
= False
345 if 'text/plain' in accept_header
or '*/*' in accept_header
:
347 if 'application/zip' in accept_header
or '*/*' in accept_header
:
348 accept_zip
= 'application/zip'
349 elif 'application/gzip' in accept_header
:
350 accept_zip
= 'application/gzip'
352 if not accept_text
and not accept_zip
:
353 raise EngineException("provide request header 'Accept' with 'application/zip' or 'text/plain'",
354 http_code
=HTTPStatus
.NOT_ACCEPTABLE
)
356 content
= self
.show(session
, _id
)
357 if content
["_admin"]["onboardingState"] != "ONBOARDED":
358 raise EngineException("Cannot get content because this resource is not at 'ONBOARDED' state. "
359 "onboardingState is {}".format(content
["_admin"]["onboardingState"]),
360 http_code
=HTTPStatus
.CONFLICT
)
361 storage
= content
["_admin"]["storage"]
362 if path
is not None and path
!= "$DESCRIPTOR": # artifacts
363 if not storage
.get('pkg-dir'):
364 raise EngineException("Packages does not contains artifacts", http_code
=HTTPStatus
.BAD_REQUEST
)
365 if self
.fs
.file_exists((storage
['folder'], storage
['pkg-dir'], *path
), 'dir'):
366 folder_content
= self
.fs
.dir_ls((storage
['folder'], storage
['pkg-dir'], *path
))
367 return folder_content
, "text/plain"
368 # TODO manage folders in http
370 return self
.fs
.file_open((storage
['folder'], storage
['pkg-dir'], *path
), "rb"),\
371 "application/octet-stream"
373 # pkgtype accept ZIP TEXT -> result
374 # manyfiles yes X -> zip
376 # onefile yes no -> zip
379 if accept_text
and (not storage
.get('pkg-dir') or path
== "$DESCRIPTOR"):
380 return self
.fs
.file_open((storage
['folder'], storage
['descriptor']), "r"), "text/plain"
381 elif storage
.get('pkg-dir') and not accept_zip
:
382 raise EngineException("Packages that contains several files need to be retrieved with 'application/zip'"
383 "Accept header", http_code
=HTTPStatus
.NOT_ACCEPTABLE
)
385 if not storage
.get('zipfile'):
386 # TODO generate zipfile if not present
387 raise EngineException("Only allowed 'text/plain' Accept header for this descriptor. To be solved in "
388 "future versions", http_code
=HTTPStatus
.NOT_ACCEPTABLE
)
389 return self
.fs
.file_open((storage
['folder'], storage
['zipfile']), "rb"), accept_zip
391 def pyangbind_validation(self
, item
, data
, force
=False):
395 pybindJSONDecoder
.load_ietf_json({'vnfd:vnfd-catalog': {'vnfd': [data
]}}, None, None, obj
=myvnfd
,
396 path_helper
=True, skip_unknown
=force
)
397 out
= pybindJSON
.dumps(myvnfd
, mode
="ietf")
400 pybindJSONDecoder
.load_ietf_json({'nsd:nsd-catalog': {'nsd': [data
]}}, None, None, obj
=mynsd
,
401 path_helper
=True, skip_unknown
=force
)
402 out
= pybindJSON
.dumps(mynsd
, mode
="ietf")
405 pybindJSONDecoder
.load_ietf_json({'nst': [data
]}, None, None, obj
=mynst
,
406 path_helper
=True, skip_unknown
=force
)
407 out
= pybindJSON
.dumps(mynst
, mode
="ietf")
409 raise EngineException("Not possible to validate '{}' item".format(item
),
410 http_code
=HTTPStatus
.INTERNAL_SERVER_ERROR
)
412 desc_out
= self
._remove
_envelop
(yaml
.safe_load(out
))
415 except Exception as e
:
416 raise EngineException("Error in pyangbind validation: {}".format(str(e
)),
417 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
420 class VnfdTopic(DescriptorTopic
):
424 def __init__(self
, db
, fs
, msg
, auth
):
425 DescriptorTopic
.__init
__(self
, db
, fs
, msg
, auth
)
428 def _remove_envelop(indata
=None):
431 clean_indata
= indata
432 if clean_indata
.get('vnfd:vnfd-catalog'):
433 clean_indata
= clean_indata
['vnfd:vnfd-catalog']
434 elif clean_indata
.get('vnfd-catalog'):
435 clean_indata
= clean_indata
['vnfd-catalog']
436 if clean_indata
.get('vnfd'):
437 if not isinstance(clean_indata
['vnfd'], list) or len(clean_indata
['vnfd']) != 1:
438 raise EngineException("'vnfd' must be a list of only one element")
439 clean_indata
= clean_indata
['vnfd'][0]
440 elif clean_indata
.get('vnfd:vnfd'):
441 if not isinstance(clean_indata
['vnfd:vnfd'], list) or len(clean_indata
['vnfd:vnfd']) != 1:
442 raise EngineException("'vnfd:vnfd' must be a list of only one element")
443 clean_indata
= clean_indata
['vnfd:vnfd'][0]
446 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
447 super().check_conflict_on_edit(session
, final_content
, edit_content
, _id
)
452 for vdu
in get_iterable(final_content
.get("vdu")):
453 if vdu
.get("pdu-type"):
458 final_content
["_admin"]["type"] = "hnfd" if contains_vdu
else "pnfd"
460 final_content
["_admin"]["type"] = "vnfd"
461 # if neither vud nor pdu do not fill type
463 def check_conflict_on_del(self
, session
, _id
, db_content
):
465 Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note
466 that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr
468 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
469 :param _id: vnfd internal id
470 :param db_content: The database content of the _id.
471 :return: None or raises EngineException with the conflict
475 descriptor
= db_content
476 descriptor_id
= descriptor
.get("id")
477 if not descriptor_id
: # empty vnfd not uploaded
480 _filter
= self
._get
_project
_filter
(session
)
482 # check vnfrs using this vnfd
483 _filter
["vnfd-id"] = _id
484 if self
.db
.get_list("vnfrs", _filter
):
485 raise EngineException("There is at least one VNF using this descriptor", http_code
=HTTPStatus
.CONFLICT
)
487 # check NSD referencing this VNFD
488 del _filter
["vnfd-id"]
489 _filter
["constituent-vnfd.ANYINDEX.vnfd-id-ref"] = descriptor_id
490 if self
.db
.get_list("nsds", _filter
):
491 raise EngineException("There is at least one NSD referencing this descriptor",
492 http_code
=HTTPStatus
.CONFLICT
)
494 def _validate_input_new(self
, indata
, storage_params
, force
=False):
495 indata
= self
.pyangbind_validation("vnfds", indata
, force
)
496 # Cross references validation in the descriptor
497 if indata
.get("vdu"):
498 if not indata
.get("mgmt-interface"):
499 raise EngineException("'mgmt-interface' is a mandatory field and it is not defined",
500 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
501 if indata
["mgmt-interface"].get("cp"):
502 for cp
in get_iterable(indata
.get("connection-point")):
503 if cp
["name"] == indata
["mgmt-interface"]["cp"]:
506 raise EngineException("mgmt-interface:cp='{}' must match an existing connection-point"
507 .format(indata
["mgmt-interface"]["cp"]),
508 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
510 for vdu
in get_iterable(indata
.get("vdu")):
511 for interface
in get_iterable(vdu
.get("interface")):
512 if interface
.get("external-connection-point-ref"):
513 for cp
in get_iterable(indata
.get("connection-point")):
514 if cp
["name"] == interface
["external-connection-point-ref"]:
517 raise EngineException("vdu[id='{}']:interface[name='{}']:external-connection-point-ref='{}' "
518 "must match an existing connection-point"
519 .format(vdu
["id"], interface
["name"],
520 interface
["external-connection-point-ref"]),
521 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
523 elif interface
.get("internal-connection-point-ref"):
524 for internal_cp
in get_iterable(vdu
.get("internal-connection-point")):
525 if interface
["internal-connection-point-ref"] == internal_cp
.get("id"):
528 raise EngineException("vdu[id='{}']:interface[name='{}']:internal-connection-point-ref='{}' "
529 "must match an existing vdu:internal-connection-point"
530 .format(vdu
["id"], interface
["name"],
531 interface
["internal-connection-point-ref"]),
532 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
533 # Validate that if descriptor contains charms, artifacts _admin.storage."pkg-dir" is not none
534 if vdu
.get("vdu-configuration"):
535 if vdu
["vdu-configuration"].get("juju"):
536 if not self
._validate
_package
_folders
(storage_params
, 'charms'):
537 raise EngineException("Charm defined in vnf[id={}]:vdu[id={}] but not present in "
538 "package".format(indata
["id"], vdu
["id"]))
539 # Validate that if descriptor contains cloud-init, artifacts _admin.storage."pkg-dir" is not none
540 if vdu
.get("cloud-init-file"):
541 if not self
._validate
_package
_folders
(storage_params
, 'cloud_init', vdu
["cloud-init-file"]):
542 raise EngineException("Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
543 "package".format(indata
["id"], vdu
["id"]))
544 # Validate that if descriptor contains charms, artifacts _admin.storage."pkg-dir" is not none
545 if indata
.get("vnf-configuration"):
546 if indata
["vnf-configuration"].get("juju"):
547 if not self
._validate
_package
_folders
(storage_params
, 'charms'):
548 raise EngineException("Charm defined in vnf[id={}] but not present in "
549 "package".format(indata
["id"]))
550 vld_names
= [] # For detection of duplicated VLD names
551 for ivld
in get_iterable(indata
.get("internal-vld")):
552 # BEGIN Detection of duplicated VLD names
553 ivld_name
= ivld
["name"]
554 if ivld_name
in vld_names
:
555 raise EngineException("Duplicated VLD name '{}' in vnfd[id={}]:internal-vld[id={}]"
556 .format(ivld
["name"], indata
["id"], ivld
["id"]),
557 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
559 vld_names
.append(ivld_name
)
560 # END Detection of duplicated VLD names
561 for icp
in get_iterable(ivld
.get("internal-connection-point")):
563 for vdu
in get_iterable(indata
.get("vdu")):
564 for internal_cp
in get_iterable(vdu
.get("internal-connection-point")):
565 if icp
["id-ref"] == internal_cp
["id"]:
571 raise EngineException("internal-vld[id='{}']:internal-connection-point='{}' must match an existing "
572 "vdu:internal-connection-point".format(ivld
["id"], icp
["id-ref"]),
573 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
574 if ivld
.get("ip-profile-ref"):
575 for ip_prof
in get_iterable(indata
.get("ip-profiles")):
576 if ip_prof
["name"] == get_iterable(ivld
.get("ip-profile-ref")):
579 raise EngineException("internal-vld[id='{}']:ip-profile-ref='{}' does not exist".format(
580 ivld
["id"], ivld
["ip-profile-ref"]),
581 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
582 for mp
in get_iterable(indata
.get("monitoring-param")):
583 if mp
.get("vdu-monitoring-param"):
585 for vdu
in get_iterable(indata
.get("vdu")):
586 for vmp
in get_iterable(vdu
.get("monitoring-param")):
587 if vmp
["id"] == mp
["vdu-monitoring-param"].get("vdu-monitoring-param-ref") and vdu
["id"] ==\
588 mp
["vdu-monitoring-param"]["vdu-ref"]:
594 raise EngineException("monitoring-param:vdu-monitoring-param:vdu-monitoring-param-ref='{}' not "
595 "defined at vdu[id='{}'] or vdu does not exist"
596 .format(mp
["vdu-monitoring-param"]["vdu-monitoring-param-ref"],
597 mp
["vdu-monitoring-param"]["vdu-ref"]),
598 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
599 elif mp
.get("vdu-metric"):
601 for vdu
in get_iterable(indata
.get("vdu")):
602 if vdu
.get("vdu-configuration"):
603 for metric
in get_iterable(vdu
["vdu-configuration"].get("metrics")):
604 if metric
["name"] == mp
["vdu-metric"]["vdu-metric-name-ref"] and vdu
["id"] == \
605 mp
["vdu-metric"]["vdu-ref"]:
611 raise EngineException("monitoring-param:vdu-metric:vdu-metric-name-ref='{}' not defined at "
612 "vdu[id='{}'] or vdu does not exist"
613 .format(mp
["vdu-metric"]["vdu-metric-name-ref"],
614 mp
["vdu-metric"]["vdu-ref"]),
615 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
617 for sgd
in get_iterable(indata
.get("scaling-group-descriptor")):
618 for sp
in get_iterable(sgd
.get("scaling-policy")):
619 for sc
in get_iterable(sp
.get("scaling-criteria")):
620 for mp
in get_iterable(indata
.get("monitoring-param")):
621 if mp
["id"] == get_iterable(sc
.get("vnf-monitoring-param-ref")):
624 raise EngineException("scaling-group-descriptor[name='{}']:scaling-criteria[name='{}']:"
625 "vnf-monitoring-param-ref='{}' not defined in any monitoring-param"
626 .format(sgd
["name"], sc
["name"], sc
["vnf-monitoring-param-ref"]),
627 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
628 for sgd_vdu
in get_iterable(sgd
.get("vdu")):
630 for vdu
in get_iterable(indata
.get("vdu")):
631 if vdu
["id"] == sgd_vdu
["vdu-id-ref"]:
637 raise EngineException("scaling-group-descriptor[name='{}']:vdu-id-ref={} does not match any vdu"
638 .format(sgd
["name"], sgd_vdu
["vdu-id-ref"]),
639 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
640 for sca
in get_iterable(sgd
.get("scaling-config-action")):
641 if not indata
.get("vnf-configuration"):
642 raise EngineException("'vnf-configuration' not defined in the descriptor but it is referenced by "
643 "scaling-group-descriptor[name='{}']:scaling-config-action"
644 .format(sgd
["name"]),
645 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
646 for primitive
in get_iterable(indata
["vnf-configuration"].get("config-primitive")):
647 if primitive
["name"] == sca
["vnf-config-primitive-name-ref"]:
650 raise EngineException("scaling-group-descriptor[name='{}']:scaling-config-action:vnf-config-"
651 "primitive-name-ref='{}' does not match any "
652 "vnf-configuration:config-primitive:name"
653 .format(sgd
["name"], sca
["vnf-config-primitive-name-ref"]),
654 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
657 def _validate_input_edit(self
, indata
, force
=False):
658 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
661 def _validate_package_folders(self
, storage_params
, folder
, file=None):
662 if not storage_params
or not storage_params
.get("pkg-dir"):
665 if self
.fs
.file_exists("{}_".format(storage_params
["folder"]), 'dir'):
666 f
= "{}_/{}/{}".format(storage_params
["folder"], storage_params
["pkg-dir"], folder
)
668 f
= "{}/{}/{}".format(storage_params
["folder"], storage_params
["pkg-dir"], folder
)
670 return self
.fs
.file_exists("{}/{}".format(f
, file), 'file')
672 if self
.fs
.file_exists(f
, 'dir'):
673 if self
.fs
.dir_ls(f
):
678 class NsdTopic(DescriptorTopic
):
682 def __init__(self
, db
, fs
, msg
, auth
):
683 DescriptorTopic
.__init
__(self
, db
, fs
, msg
, auth
)
686 def _remove_envelop(indata
=None):
689 clean_indata
= indata
691 if clean_indata
.get('nsd:nsd-catalog'):
692 clean_indata
= clean_indata
['nsd:nsd-catalog']
693 elif clean_indata
.get('nsd-catalog'):
694 clean_indata
= clean_indata
['nsd-catalog']
695 if clean_indata
.get('nsd'):
696 if not isinstance(clean_indata
['nsd'], list) or len(clean_indata
['nsd']) != 1:
697 raise EngineException("'nsd' must be a list of only one element")
698 clean_indata
= clean_indata
['nsd'][0]
699 elif clean_indata
.get('nsd:nsd'):
700 if not isinstance(clean_indata
['nsd:nsd'], list) or len(clean_indata
['nsd:nsd']) != 1:
701 raise EngineException("'nsd:nsd' must be a list of only one element")
702 clean_indata
= clean_indata
['nsd:nsd'][0]
705 def _validate_input_new(self
, indata
, storage_params
, force
=False):
706 indata
= self
.pyangbind_validation("nsds", indata
, force
)
707 # Cross references validation in the descriptor
708 # TODO validata that if contains cloud-init-file or charms, have artifacts _admin.storage."pkg-dir" is not none
709 for vld
in get_iterable(indata
.get("vld")):
710 if vld
.get("mgmt-network") and vld
.get("ip-profile-ref"):
711 raise EngineException("Error at vld[id='{}']:ip-profile-ref"
712 " You cannot set an ip-profile when mgmt-network is True"
713 .format(vld
["id"]), http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
714 for vnfd_cp
in get_iterable(vld
.get("vnfd-connection-point-ref")):
715 for constituent_vnfd
in get_iterable(indata
.get("constituent-vnfd")):
716 if vnfd_cp
["member-vnf-index-ref"] == constituent_vnfd
["member-vnf-index"]:
717 if vnfd_cp
.get("vnfd-id-ref") and vnfd_cp
["vnfd-id-ref"] != constituent_vnfd
["vnfd-id-ref"]:
718 raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[vnfd-id-ref='{}'] "
719 "does not match constituent-vnfd[member-vnf-index='{}']:vnfd-id-ref"
720 " '{}'".format(vld
["id"], vnfd_cp
["vnfd-id-ref"],
721 constituent_vnfd
["member-vnf-index"],
722 constituent_vnfd
["vnfd-id-ref"]),
723 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
726 raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}'] "
727 "does not match any constituent-vnfd:member-vnf-index"
728 .format(vld
["id"], vnfd_cp
["member-vnf-index-ref"]),
729 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
731 for fgd
in get_iterable(indata
.get("vnffgd")):
732 for cls
in get_iterable(fgd
.get("classifier")):
733 rspref
= cls
.get("rsp-id-ref")
734 for rsp
in get_iterable(fgd
.get("rsp")):
735 rspid
= rsp
.get("id")
736 if rspid
and rspref
and rspid
== rspref
:
739 raise EngineException(
740 "Error at vnffgd[id='{}']:classifier[id='{}']:rsp-id-ref '{}' does not match any rsp:id"
741 .format(fgd
["id"], cls
["id"], rspref
),
742 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
745 def _validate_input_edit(self
, indata
, force
=False):
746 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
749 def _check_descriptor_dependencies(self
, session
, descriptor
):
751 Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd
752 connection points are ok
753 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
754 :param descriptor: descriptor to be inserted or edit
755 :return: None or raises exception
759 member_vnfd_index
= {}
760 if descriptor
.get("constituent-vnfd") and not session
["force"]:
761 for vnf
in descriptor
["constituent-vnfd"]:
762 vnfd_id
= vnf
["vnfd-id-ref"]
763 filter_q
= self
._get
_project
_filter
(session
)
764 filter_q
["id"] = vnfd_id
765 vnf_list
= self
.db
.get_list("vnfds", filter_q
)
767 raise EngineException("Descriptor error at 'constituent-vnfd':'vnfd-id-ref'='{}' references a non "
768 "existing vnfd".format(vnfd_id
), http_code
=HTTPStatus
.CONFLICT
)
769 # elif len(vnf_list) > 1:
770 # raise EngineException("More than one vnfd found for id='{}'".format(vnfd_id),
771 # http_code=HTTPStatus.CONFLICT)
772 member_vnfd_index
[vnf
["member-vnf-index"]] = vnf_list
[0]
774 # Cross references validation in the descriptor and vnfd connection point validation
775 for vld
in get_iterable(descriptor
.get("vld")):
776 for referenced_vnfd_cp
in get_iterable(vld
.get("vnfd-connection-point-ref")):
777 # look if this vnfd contains this connection point
778 vnfd
= member_vnfd_index
.get(referenced_vnfd_cp
["member-vnf-index-ref"])
779 for vnfd_cp
in get_iterable(vnfd
.get("connection-point")):
780 if referenced_vnfd_cp
.get("vnfd-connection-point-ref") == vnfd_cp
["name"]:
783 raise EngineException(
784 "Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}']:vnfd-"
785 "connection-point-ref='{}' references a non existing conection-point:name inside vnfd '{}'"
786 .format(vld
["id"], referenced_vnfd_cp
["member-vnf-index-ref"],
787 referenced_vnfd_cp
["vnfd-connection-point-ref"], vnfd
["id"]),
788 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
790 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
791 super().check_conflict_on_edit(session
, final_content
, edit_content
, _id
)
793 self
._check
_descriptor
_dependencies
(session
, final_content
)
795 def check_conflict_on_del(self
, session
, _id
, db_content
):
797 Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note
798 that NSD can be public and be used by other projects.
799 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
800 :param _id: nsd internal id
801 :param db_content: The database content of the _id
802 :return: None or raises EngineException with the conflict
806 descriptor
= db_content
807 descriptor_id
= descriptor
.get("id")
808 if not descriptor_id
: # empty nsd not uploaded
811 # check NSD used by NS
812 _filter
= self
._get
_project
_filter
(session
)
813 _filter
["nsd-id"] = _id
814 if self
.db
.get_list("nsrs", _filter
):
815 raise EngineException("There is at least one NS using this descriptor", http_code
=HTTPStatus
.CONFLICT
)
817 # check NSD referenced by NST
818 del _filter
["nsd-id"]
819 _filter
["netslice-subnet.ANYINDEX.nsd-ref"] = descriptor_id
820 if self
.db
.get_list("nsts", _filter
):
821 raise EngineException("There is at least one NetSlice Template referencing this descriptor",
822 http_code
=HTTPStatus
.CONFLICT
)
825 class NstTopic(DescriptorTopic
):
829 def __init__(self
, db
, fs
, msg
, auth
):
830 DescriptorTopic
.__init
__(self
, db
, fs
, msg
, auth
)
833 def _remove_envelop(indata
=None):
836 clean_indata
= indata
838 if clean_indata
.get('nst'):
839 if not isinstance(clean_indata
['nst'], list) or len(clean_indata
['nst']) != 1:
840 raise EngineException("'nst' must be a list only one element")
841 clean_indata
= clean_indata
['nst'][0]
842 elif clean_indata
.get('nst:nst'):
843 if not isinstance(clean_indata
['nst:nst'], list) or len(clean_indata
['nst:nst']) != 1:
844 raise EngineException("'nst:nst' must be a list only one element")
845 clean_indata
= clean_indata
['nst:nst'][0]
848 def _validate_input_edit(self
, indata
, force
=False):
849 # TODO validate with pyangbind, serialize
852 def _validate_input_new(self
, indata
, storage_params
, force
=False):
853 indata
= self
.pyangbind_validation("nsts", indata
, force
)
856 def _check_descriptor_dependencies(self
, session
, descriptor
):
858 Check that the dependent descriptors exist on a new descriptor or edition
859 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
860 :param descriptor: descriptor to be inserted or edit
861 :return: None or raises exception
863 if not descriptor
.get("netslice-subnet"):
865 for nsd
in descriptor
["netslice-subnet"]:
866 nsd_id
= nsd
["nsd-ref"]
867 filter_q
= self
._get
_project
_filter
(session
)
868 filter_q
["id"] = nsd_id
869 if not self
.db
.get_list("nsds", filter_q
):
870 raise EngineException("Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non "
871 "existing nsd".format(nsd_id
), http_code
=HTTPStatus
.CONFLICT
)
873 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
874 super().check_conflict_on_edit(session
, final_content
, edit_content
, _id
)
876 self
._check
_descriptor
_dependencies
(session
, final_content
)
878 def check_conflict_on_del(self
, session
, _id
, db_content
):
880 Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note
881 that NST can be public and be used by other projects.
882 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
883 :param _id: nst internal id
884 :param db_content: The database content of the _id.
885 :return: None or raises EngineException with the conflict
887 # TODO: Check this method
890 # Get Network Slice Template from Database
891 _filter
= self
._get
_project
_filter
(session
)
892 _filter
["_admin.nst-id"] = _id
893 if self
.db
.get_list("nsis", _filter
):
894 raise EngineException("there is at least one Netslice Instance using this descriptor",
895 http_code
=HTTPStatus
.CONFLICT
)
898 class PduTopic(BaseTopic
):
901 schema_new
= pdu_new_schema
902 schema_edit
= pdu_edit_schema
904 def __init__(self
, db
, fs
, msg
, auth
):
905 BaseTopic
.__init
__(self
, db
, fs
, msg
, auth
)
908 def format_on_new(content
, project_id
=None, make_public
=False):
909 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
910 content
["_admin"]["onboardingState"] = "CREATED"
911 content
["_admin"]["operationalState"] = "ENABLED"
912 content
["_admin"]["usageState"] = "NOT_IN_USE"
914 def check_conflict_on_del(self
, session
, _id
, db_content
):
916 Check that there is not any vnfr that uses this PDU
917 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
918 :param _id: pdu internal id
919 :param db_content: The database content of the _id.
920 :return: None or raises EngineException with the conflict
925 _filter
= self
._get
_project
_filter
(session
)
926 _filter
["vdur.pdu-id"] = _id
927 if self
.db
.get_list("vnfrs", _filter
):
928 raise EngineException("There is at least one VNF using this PDU", http_code
=HTTPStatus
.CONFLICT
)