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
, not_send_msg
=None):
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 :param not_send_msg: To not send message (False) or store content (list) instead
104 :return: None if ok or raises EngineException with the problem
106 self
.fs
.file_delete(_id
, ignore_non_exist
=True)
107 self
.fs
.file_delete(_id
+ "_", ignore_non_exist
=True) # remove temp folder
110 def get_one_by_id(db
, session
, topic
, id):
111 # find owned by this project
112 _filter
= BaseTopic
._get
_project
_filter
(session
)
114 desc_list
= db
.get_list(topic
, _filter
)
115 if len(desc_list
) == 1:
117 elif len(desc_list
) > 1:
118 raise DbException("Found more than one {} with id='{}' belonging to this project".format(topic
[:-1], id),
121 # not found any: try to find public
122 _filter
= BaseTopic
._get
_project
_filter
(session
)
124 desc_list
= db
.get_list(topic
, _filter
)
126 raise DbException("Not found any {} with id='{}'".format(topic
[:-1], id), HTTPStatus
.NOT_FOUND
)
127 elif len(desc_list
) == 1:
130 raise DbException("Found more than one public {} with id='{}'; and no one belonging to this project".format(
131 topic
[:-1], id), HTTPStatus
.CONFLICT
)
133 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
135 Creates a new almost empty DISABLED entry into database. Due to SOL005, it does not follow normal procedure.
136 Creating a VNFD or NSD is done in two steps: 1. Creates an empty descriptor (this step) and 2) upload content
137 (self.upload_content)
138 :param rollback: list to append created items at database in case a rollback may to be done
139 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
140 :param indata: data to be inserted
141 :param kwargs: used to override the indata descriptor
142 :param headers: http request headers
143 :return: _id, None: identity of the inserted data; and None as there is not any operation
148 self
.check_quota(session
)
152 if "userDefinedData" in indata
:
153 indata
= indata
['userDefinedData']
155 # Override descriptor with query string kwargs
156 self
._update
_input
_with
_kwargs
(indata
, kwargs
)
157 # uncomment when this method is implemented.
158 # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors
159 # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"])
161 content
= {"_admin": {"userDefinedData": indata
}}
162 self
.format_on_new(content
, session
["project_id"], make_public
=session
["public"])
163 _id
= self
.db
.create(self
.topic
, content
)
164 rollback
.append({"topic": self
.topic
, "_id": _id
})
165 self
._send
_msg
("created", {"_id": _id
})
167 except ValidationError
as e
:
168 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
170 def upload_content(self
, session
, _id
, indata
, kwargs
, headers
):
172 Used for receiving content by chunks (with a transaction_id header and/or gzip file. It will store and extract)
173 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
174 :param _id : the nsd,vnfd is already created, this is the id
175 :param indata: http body request
176 :param kwargs: user query string to override parameters. NOT USED
177 :param headers: http request headers
178 :return: True if package is completely uploaded or False if partial content has been uploded
179 Raise exception on error
181 # Check that _id exists and it is valid
182 current_desc
= self
.show(session
, _id
)
184 content_range_text
= headers
.get("Content-Range")
185 expected_md5
= headers
.get("Content-File-MD5")
187 content_type
= headers
.get("Content-Type")
188 if content_type
and "application/gzip" in content_type
or "application/x-gzip" in content_type
or \
189 "application/zip" in content_type
:
191 filename
= headers
.get("Content-Filename")
193 filename
= "package.tar.gz" if compressed
else "package"
194 # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266
198 if content_range_text
:
199 content_range
= content_range_text
.replace("-", " ").replace("/", " ").split()
200 if content_range
[0] != "bytes": # TODO check x<y not negative < total....
202 start
= int(content_range
[1])
203 end
= int(content_range
[2]) + 1
204 total
= int(content_range
[3])
207 temp_folder
= _id
+ "_" # all the content is upload here and if ok, it is rename from id_ to is folder
210 if not self
.fs
.file_exists(temp_folder
, 'dir'):
211 raise EngineException("invalid Transaction-Id header", HTTPStatus
.NOT_FOUND
)
213 self
.fs
.file_delete(temp_folder
, ignore_non_exist
=True)
214 self
.fs
.mkdir(temp_folder
)
216 storage
= self
.fs
.get_params()
217 storage
["folder"] = _id
219 file_path
= (temp_folder
, filename
)
220 if self
.fs
.file_exists(file_path
, 'file'):
221 file_size
= self
.fs
.file_size(file_path
)
224 if file_size
!= start
:
225 raise EngineException("invalid Content-Range start sequence, expected '{}' but received '{}'".format(
226 file_size
, start
), HTTPStatus
.REQUESTED_RANGE_NOT_SATISFIABLE
)
227 file_pkg
= self
.fs
.file_open(file_path
, 'a+b')
228 if isinstance(indata
, dict):
229 indata_text
= yaml
.safe_dump(indata
, indent
=4, default_flow_style
=False)
230 file_pkg
.write(indata_text
.encode(encoding
="utf-8"))
234 indata_text
= indata
.read(4096)
235 indata_len
+= len(indata_text
)
238 file_pkg
.write(indata_text
)
239 if content_range_text
:
240 if indata_len
!= end
-start
:
241 raise EngineException("Mismatch between Content-Range header {}-{} and body length of {}".format(
242 start
, end
-1, indata_len
), HTTPStatus
.REQUESTED_RANGE_NOT_SATISFIABLE
)
244 # TODO update to UPLOADING
251 chunk_data
= file_pkg
.read(1024)
253 file_md5
.update(chunk_data
)
254 chunk_data
= file_pkg
.read(1024)
255 if expected_md5
!= file_md5
.hexdigest():
256 raise EngineException("Error, MD5 mismatch", HTTPStatus
.CONFLICT
)
258 if compressed
== "gzip":
259 tar
= tarfile
.open(mode
='r', fileobj
=file_pkg
)
260 descriptor_file_name
= None
262 tarname
= tarinfo
.name
263 tarname_path
= tarname
.split("/")
264 if not tarname_path
[0] or ".." in tarname_path
: # if start with "/" means absolute path
265 raise EngineException("Absolute path or '..' are not allowed for package descriptor tar.gz")
266 if len(tarname_path
) == 1 and not tarinfo
.isdir():
267 raise EngineException("All files must be inside a dir for package descriptor tar.gz")
268 if tarname
.endswith(".yaml") or tarname
.endswith(".json") or tarname
.endswith(".yml"):
269 storage
["pkg-dir"] = tarname_path
[0]
270 if len(tarname_path
) == 2:
271 if descriptor_file_name
:
272 raise EngineException(
273 "Found more than one descriptor file at package descriptor tar.gz")
274 descriptor_file_name
= tarname
275 if not descriptor_file_name
:
276 raise EngineException("Not found any descriptor file at package descriptor tar.gz")
277 storage
["descriptor"] = descriptor_file_name
278 storage
["zipfile"] = filename
279 self
.fs
.file_extract(tar
, temp_folder
)
280 with self
.fs
.file_open((temp_folder
, descriptor_file_name
), "r") as descriptor_file
:
281 content
= descriptor_file
.read()
283 content
= file_pkg
.read()
284 storage
["descriptor"] = descriptor_file_name
= filename
286 if descriptor_file_name
.endswith(".json"):
287 error_text
= "Invalid json format "
288 indata
= json
.load(content
)
290 error_text
= "Invalid yaml format "
291 indata
= yaml
.load(content
, Loader
=yaml
.SafeLoader
)
293 current_desc
["_admin"]["storage"] = storage
294 current_desc
["_admin"]["onboardingState"] = "ONBOARDED"
295 current_desc
["_admin"]["operationalState"] = "ENABLED"
297 indata
= self
._remove
_envelop
(indata
)
299 # Override descriptor with query string kwargs
301 self
._update
_input
_with
_kwargs
(indata
, kwargs
)
302 # it will call overrides method at VnfdTopic or NsdTopic
303 # indata = self._validate_input_edit(indata, force=session["force"])
305 deep_update_rfc7396(current_desc
, indata
)
306 self
.check_conflict_on_edit(session
, current_desc
, indata
, _id
=_id
)
307 current_desc
["_admin"]["modified"] = time()
308 self
.db
.replace(self
.topic
, _id
, current_desc
)
309 self
.fs
.dir_rename(temp_folder
, _id
)
312 self
._send
_msg
("edited", indata
)
314 # TODO if descriptor has changed because kwargs update content and remove cached zip
315 # TODO if zip is not present creates one
318 except EngineException
:
321 raise EngineException("invalid Content-Range header format. Expected 'bytes start-end/total'",
322 HTTPStatus
.REQUESTED_RANGE_NOT_SATISFIABLE
)
324 raise EngineException("invalid upload transaction sequence: '{}'".format(e
), HTTPStatus
.BAD_REQUEST
)
325 except tarfile
.ReadError
as e
:
326 raise EngineException("invalid file content {}".format(e
), HTTPStatus
.BAD_REQUEST
)
327 except (ValueError, yaml
.YAMLError
) as e
:
328 raise EngineException(error_text
+ str(e
))
329 except ValidationError
as e
:
330 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
335 def get_file(self
, session
, _id
, path
=None, accept_header
=None):
337 Return the file content of a vnfd or nsd
338 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
339 :param _id: Identity of the vnfd, nsd
340 :param path: artifact path or "$DESCRIPTOR" or None
341 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
342 :return: opened file plus Accept format or raises an exception
344 accept_text
= accept_zip
= False
346 if 'text/plain' in accept_header
or '*/*' in accept_header
:
348 if 'application/zip' in accept_header
or '*/*' in accept_header
:
349 accept_zip
= 'application/zip'
350 elif 'application/gzip' in accept_header
:
351 accept_zip
= 'application/gzip'
353 if not accept_text
and not accept_zip
:
354 raise EngineException("provide request header 'Accept' with 'application/zip' or 'text/plain'",
355 http_code
=HTTPStatus
.NOT_ACCEPTABLE
)
357 content
= self
.show(session
, _id
)
358 if content
["_admin"]["onboardingState"] != "ONBOARDED":
359 raise EngineException("Cannot get content because this resource is not at 'ONBOARDED' state. "
360 "onboardingState is {}".format(content
["_admin"]["onboardingState"]),
361 http_code
=HTTPStatus
.CONFLICT
)
362 storage
= content
["_admin"]["storage"]
363 if path
is not None and path
!= "$DESCRIPTOR": # artifacts
364 if not storage
.get('pkg-dir'):
365 raise EngineException("Packages does not contains artifacts", http_code
=HTTPStatus
.BAD_REQUEST
)
366 if self
.fs
.file_exists((storage
['folder'], storage
['pkg-dir'], *path
), 'dir'):
367 folder_content
= self
.fs
.dir_ls((storage
['folder'], storage
['pkg-dir'], *path
))
368 return folder_content
, "text/plain"
369 # TODO manage folders in http
371 return self
.fs
.file_open((storage
['folder'], storage
['pkg-dir'], *path
), "rb"),\
372 "application/octet-stream"
374 # pkgtype accept ZIP TEXT -> result
375 # manyfiles yes X -> zip
377 # onefile yes no -> zip
380 if accept_text
and (not storage
.get('pkg-dir') or path
== "$DESCRIPTOR"):
381 return self
.fs
.file_open((storage
['folder'], storage
['descriptor']), "r"), "text/plain"
382 elif storage
.get('pkg-dir') and not accept_zip
:
383 raise EngineException("Packages that contains several files need to be retrieved with 'application/zip'"
384 "Accept header", http_code
=HTTPStatus
.NOT_ACCEPTABLE
)
386 if not storage
.get('zipfile'):
387 # TODO generate zipfile if not present
388 raise EngineException("Only allowed 'text/plain' Accept header for this descriptor. To be solved in "
389 "future versions", http_code
=HTTPStatus
.NOT_ACCEPTABLE
)
390 return self
.fs
.file_open((storage
['folder'], storage
['zipfile']), "rb"), accept_zip
392 def pyangbind_validation(self
, item
, data
, force
=False):
396 pybindJSONDecoder
.load_ietf_json({'vnfd:vnfd-catalog': {'vnfd': [data
]}}, None, None, obj
=myvnfd
,
397 path_helper
=True, skip_unknown
=force
)
398 out
= pybindJSON
.dumps(myvnfd
, mode
="ietf")
401 pybindJSONDecoder
.load_ietf_json({'nsd:nsd-catalog': {'nsd': [data
]}}, None, None, obj
=mynsd
,
402 path_helper
=True, skip_unknown
=force
)
403 out
= pybindJSON
.dumps(mynsd
, mode
="ietf")
406 pybindJSONDecoder
.load_ietf_json({'nst': [data
]}, None, None, obj
=mynst
,
407 path_helper
=True, skip_unknown
=force
)
408 out
= pybindJSON
.dumps(mynst
, mode
="ietf")
410 raise EngineException("Not possible to validate '{}' item".format(item
),
411 http_code
=HTTPStatus
.INTERNAL_SERVER_ERROR
)
413 desc_out
= self
._remove
_envelop
(yaml
.safe_load(out
))
416 except Exception as e
:
417 raise EngineException("Error in pyangbind validation: {}".format(str(e
)),
418 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
421 class VnfdTopic(DescriptorTopic
):
425 def __init__(self
, db
, fs
, msg
, auth
):
426 DescriptorTopic
.__init
__(self
, db
, fs
, msg
, auth
)
429 def _remove_envelop(indata
=None):
432 clean_indata
= indata
433 if clean_indata
.get('vnfd:vnfd-catalog'):
434 clean_indata
= clean_indata
['vnfd:vnfd-catalog']
435 elif clean_indata
.get('vnfd-catalog'):
436 clean_indata
= clean_indata
['vnfd-catalog']
437 if clean_indata
.get('vnfd'):
438 if not isinstance(clean_indata
['vnfd'], list) or len(clean_indata
['vnfd']) != 1:
439 raise EngineException("'vnfd' must be a list of only one element")
440 clean_indata
= clean_indata
['vnfd'][0]
441 elif clean_indata
.get('vnfd:vnfd'):
442 if not isinstance(clean_indata
['vnfd:vnfd'], list) or len(clean_indata
['vnfd:vnfd']) != 1:
443 raise EngineException("'vnfd:vnfd' must be a list of only one element")
444 clean_indata
= clean_indata
['vnfd:vnfd'][0]
447 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
448 super().check_conflict_on_edit(session
, final_content
, edit_content
, _id
)
453 for vdu
in get_iterable(final_content
.get("vdu")):
454 if vdu
.get("pdu-type"):
459 final_content
["_admin"]["type"] = "hnfd" if contains_vdu
else "pnfd"
461 final_content
["_admin"]["type"] = "vnfd"
462 # if neither vud nor pdu do not fill type
464 def check_conflict_on_del(self
, session
, _id
, db_content
):
466 Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note
467 that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr
469 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
470 :param _id: vnfd internal id
471 :param db_content: The database content of the _id.
472 :return: None or raises EngineException with the conflict
476 descriptor
= db_content
477 descriptor_id
= descriptor
.get("id")
478 if not descriptor_id
: # empty vnfd not uploaded
481 _filter
= self
._get
_project
_filter
(session
)
483 # check vnfrs using this vnfd
484 _filter
["vnfd-id"] = _id
485 if self
.db
.get_list("vnfrs", _filter
):
486 raise EngineException("There is at least one VNF using this descriptor", http_code
=HTTPStatus
.CONFLICT
)
488 # check NSD referencing this VNFD
489 del _filter
["vnfd-id"]
490 _filter
["constituent-vnfd.ANYINDEX.vnfd-id-ref"] = descriptor_id
491 if self
.db
.get_list("nsds", _filter
):
492 raise EngineException("There is at least one NSD referencing this descriptor",
493 http_code
=HTTPStatus
.CONFLICT
)
495 def _validate_input_new(self
, indata
, storage_params
, force
=False):
496 indata
= self
.pyangbind_validation("vnfds", indata
, force
)
497 # Cross references validation in the descriptor
498 if indata
.get("vdu"):
499 if not indata
.get("mgmt-interface"):
500 raise EngineException("'mgmt-interface' is a mandatory field and it is not defined",
501 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
502 if indata
["mgmt-interface"].get("cp"):
503 for cp
in get_iterable(indata
.get("connection-point")):
504 if cp
["name"] == indata
["mgmt-interface"]["cp"]:
507 raise EngineException("mgmt-interface:cp='{}' must match an existing connection-point"
508 .format(indata
["mgmt-interface"]["cp"]),
509 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
511 for vdu
in get_iterable(indata
.get("vdu")):
514 for interface
in get_iterable(vdu
.get("interface")):
515 if interface
.get("external-connection-point-ref"):
516 if interface
.get("external-connection-point-ref") in ecp_refs
:
517 raise EngineException("vdu[id='{}']:interface[name='{}']:external-connection-point-ref='{}' "
518 "is referenced by other interface"
519 .format(vdu
["id"], interface
["name"],
520 interface
["external-connection-point-ref"]),
521 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
522 ecp_refs
.append(interface
.get("external-connection-point-ref"))
523 for cp
in get_iterable(indata
.get("connection-point")):
524 if cp
["name"] == interface
["external-connection-point-ref"]:
527 raise EngineException("vdu[id='{}']:interface[name='{}']:external-connection-point-ref='{}' "
528 "must match an existing connection-point"
529 .format(vdu
["id"], interface
["name"],
530 interface
["external-connection-point-ref"]),
531 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
532 elif interface
.get("internal-connection-point-ref"):
533 if interface
.get("internal-connection-point-ref") in icp_refs
:
534 raise EngineException("vdu[id='{}']:interface[name='{}']:internal-connection-point-ref='{}' "
535 "is referenced by other interface"
536 .format(vdu
["id"], interface
["name"],
537 interface
["internal-connection-point-ref"]),
538 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
539 icp_refs
.append(interface
.get("internal-connection-point-ref"))
540 for internal_cp
in get_iterable(vdu
.get("internal-connection-point")):
541 if interface
["internal-connection-point-ref"] == internal_cp
.get("id"):
544 raise EngineException("vdu[id='{}']:interface[name='{}']:internal-connection-point-ref='{}' "
545 "must match an existing vdu:internal-connection-point"
546 .format(vdu
["id"], interface
["name"],
547 interface
["internal-connection-point-ref"]),
548 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
549 # Validate that if descriptor contains charms, artifacts _admin.storage."pkg-dir" is not none
550 if vdu
.get("vdu-configuration"):
551 if vdu
["vdu-configuration"].get("juju"):
552 if not self
._validate
_package
_folders
(storage_params
, 'charms'):
553 raise EngineException("Charm defined in vnf[id={}]:vdu[id={}] but not present in "
554 "package".format(indata
["id"], vdu
["id"]))
555 # Validate that if descriptor contains cloud-init, artifacts _admin.storage."pkg-dir" is not none
556 if vdu
.get("cloud-init-file"):
557 if not self
._validate
_package
_folders
(storage_params
, 'cloud_init', vdu
["cloud-init-file"]):
558 raise EngineException("Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
559 "package".format(indata
["id"], vdu
["id"]))
560 # Validate that if descriptor contains charms, artifacts _admin.storage."pkg-dir" is not none
561 if indata
.get("vnf-configuration"):
562 if indata
["vnf-configuration"].get("juju"):
563 if not self
._validate
_package
_folders
(storage_params
, 'charms'):
564 raise EngineException("Charm defined in vnf[id={}] but not present in "
565 "package".format(indata
["id"]))
566 vld_names
= [] # For detection of duplicated VLD names
567 for ivld
in get_iterable(indata
.get("internal-vld")):
568 # BEGIN Detection of duplicated VLD names
569 ivld_name
= ivld
["name"]
570 if ivld_name
in vld_names
:
571 raise EngineException("Duplicated VLD name '{}' in vnfd[id={}]:internal-vld[id={}]"
572 .format(ivld
["name"], indata
["id"], ivld
["id"]),
573 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
575 vld_names
.append(ivld_name
)
576 # END Detection of duplicated VLD names
577 for icp
in get_iterable(ivld
.get("internal-connection-point")):
579 for vdu
in get_iterable(indata
.get("vdu")):
580 for internal_cp
in get_iterable(vdu
.get("internal-connection-point")):
581 if icp
["id-ref"] == internal_cp
["id"]:
587 raise EngineException("internal-vld[id='{}']:internal-connection-point='{}' must match an existing "
588 "vdu:internal-connection-point".format(ivld
["id"], icp
["id-ref"]),
589 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
590 if ivld
.get("ip-profile-ref"):
591 for ip_prof
in get_iterable(indata
.get("ip-profiles")):
592 if ip_prof
["name"] == get_iterable(ivld
.get("ip-profile-ref")):
595 raise EngineException("internal-vld[id='{}']:ip-profile-ref='{}' does not exist".format(
596 ivld
["id"], ivld
["ip-profile-ref"]),
597 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
598 for mp
in get_iterable(indata
.get("monitoring-param")):
599 if mp
.get("vdu-monitoring-param"):
601 for vdu
in get_iterable(indata
.get("vdu")):
602 for vmp
in get_iterable(vdu
.get("monitoring-param")):
603 if vmp
["id"] == mp
["vdu-monitoring-param"].get("vdu-monitoring-param-ref") and vdu
["id"] ==\
604 mp
["vdu-monitoring-param"]["vdu-ref"]:
610 raise EngineException("monitoring-param:vdu-monitoring-param:vdu-monitoring-param-ref='{}' not "
611 "defined at vdu[id='{}'] or vdu does not exist"
612 .format(mp
["vdu-monitoring-param"]["vdu-monitoring-param-ref"],
613 mp
["vdu-monitoring-param"]["vdu-ref"]),
614 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
615 elif mp
.get("vdu-metric"):
617 for vdu
in get_iterable(indata
.get("vdu")):
618 if vdu
.get("vdu-configuration"):
619 for metric
in get_iterable(vdu
["vdu-configuration"].get("metrics")):
620 if metric
["name"] == mp
["vdu-metric"]["vdu-metric-name-ref"] and vdu
["id"] == \
621 mp
["vdu-metric"]["vdu-ref"]:
627 raise EngineException("monitoring-param:vdu-metric:vdu-metric-name-ref='{}' not defined at "
628 "vdu[id='{}'] or vdu does not exist"
629 .format(mp
["vdu-metric"]["vdu-metric-name-ref"],
630 mp
["vdu-metric"]["vdu-ref"]),
631 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
633 for sgd
in get_iterable(indata
.get("scaling-group-descriptor")):
634 for sp
in get_iterable(sgd
.get("scaling-policy")):
635 for sc
in get_iterable(sp
.get("scaling-criteria")):
636 for mp
in get_iterable(indata
.get("monitoring-param")):
637 if mp
["id"] == get_iterable(sc
.get("vnf-monitoring-param-ref")):
640 raise EngineException("scaling-group-descriptor[name='{}']:scaling-criteria[name='{}']:"
641 "vnf-monitoring-param-ref='{}' not defined in any monitoring-param"
642 .format(sgd
["name"], sc
["name"], sc
["vnf-monitoring-param-ref"]),
643 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
644 for sgd_vdu
in get_iterable(sgd
.get("vdu")):
646 for vdu
in get_iterable(indata
.get("vdu")):
647 if vdu
["id"] == sgd_vdu
["vdu-id-ref"]:
653 raise EngineException("scaling-group-descriptor[name='{}']:vdu-id-ref={} does not match any vdu"
654 .format(sgd
["name"], sgd_vdu
["vdu-id-ref"]),
655 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
656 for sca
in get_iterable(sgd
.get("scaling-config-action")):
657 if not indata
.get("vnf-configuration"):
658 raise EngineException("'vnf-configuration' not defined in the descriptor but it is referenced by "
659 "scaling-group-descriptor[name='{}']:scaling-config-action"
660 .format(sgd
["name"]),
661 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
662 for primitive
in get_iterable(indata
["vnf-configuration"].get("config-primitive")):
663 if primitive
["name"] == sca
["vnf-config-primitive-name-ref"]:
666 raise EngineException("scaling-group-descriptor[name='{}']:scaling-config-action:vnf-config-"
667 "primitive-name-ref='{}' does not match any "
668 "vnf-configuration:config-primitive:name"
669 .format(sgd
["name"], sca
["vnf-config-primitive-name-ref"]),
670 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
673 def _validate_input_edit(self
, indata
, force
=False):
674 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
677 def _validate_package_folders(self
, storage_params
, folder
, file=None):
678 if not storage_params
or not storage_params
.get("pkg-dir"):
681 if self
.fs
.file_exists("{}_".format(storage_params
["folder"]), 'dir'):
682 f
= "{}_/{}/{}".format(storage_params
["folder"], storage_params
["pkg-dir"], folder
)
684 f
= "{}/{}/{}".format(storage_params
["folder"], storage_params
["pkg-dir"], folder
)
686 return self
.fs
.file_exists("{}/{}".format(f
, file), 'file')
688 if self
.fs
.file_exists(f
, 'dir'):
689 if self
.fs
.dir_ls(f
):
694 class NsdTopic(DescriptorTopic
):
698 def __init__(self
, db
, fs
, msg
, auth
):
699 DescriptorTopic
.__init
__(self
, db
, fs
, msg
, auth
)
702 def _remove_envelop(indata
=None):
705 clean_indata
= indata
707 if clean_indata
.get('nsd:nsd-catalog'):
708 clean_indata
= clean_indata
['nsd:nsd-catalog']
709 elif clean_indata
.get('nsd-catalog'):
710 clean_indata
= clean_indata
['nsd-catalog']
711 if clean_indata
.get('nsd'):
712 if not isinstance(clean_indata
['nsd'], list) or len(clean_indata
['nsd']) != 1:
713 raise EngineException("'nsd' must be a list of only one element")
714 clean_indata
= clean_indata
['nsd'][0]
715 elif clean_indata
.get('nsd:nsd'):
716 if not isinstance(clean_indata
['nsd:nsd'], list) or len(clean_indata
['nsd:nsd']) != 1:
717 raise EngineException("'nsd:nsd' must be a list of only one element")
718 clean_indata
= clean_indata
['nsd:nsd'][0]
721 def _validate_input_new(self
, indata
, storage_params
, force
=False):
722 indata
= self
.pyangbind_validation("nsds", indata
, force
)
723 # Cross references validation in the descriptor
724 # TODO validata that if contains cloud-init-file or charms, have artifacts _admin.storage."pkg-dir" is not none
725 for vld
in get_iterable(indata
.get("vld")):
726 if vld
.get("mgmt-network") and vld
.get("ip-profile-ref"):
727 raise EngineException("Error at vld[id='{}']:ip-profile-ref"
728 " You cannot set an ip-profile when mgmt-network is True"
729 .format(vld
["id"]), http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
730 for vnfd_cp
in get_iterable(vld
.get("vnfd-connection-point-ref")):
731 for constituent_vnfd
in get_iterable(indata
.get("constituent-vnfd")):
732 if vnfd_cp
["member-vnf-index-ref"] == constituent_vnfd
["member-vnf-index"]:
733 if vnfd_cp
.get("vnfd-id-ref") and vnfd_cp
["vnfd-id-ref"] != constituent_vnfd
["vnfd-id-ref"]:
734 raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[vnfd-id-ref='{}'] "
735 "does not match constituent-vnfd[member-vnf-index='{}']:vnfd-id-ref"
736 " '{}'".format(vld
["id"], vnfd_cp
["vnfd-id-ref"],
737 constituent_vnfd
["member-vnf-index"],
738 constituent_vnfd
["vnfd-id-ref"]),
739 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
742 raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}'] "
743 "does not match any constituent-vnfd:member-vnf-index"
744 .format(vld
["id"], vnfd_cp
["member-vnf-index-ref"]),
745 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
747 for fgd
in get_iterable(indata
.get("vnffgd")):
748 for cls
in get_iterable(fgd
.get("classifier")):
749 rspref
= cls
.get("rsp-id-ref")
750 for rsp
in get_iterable(fgd
.get("rsp")):
751 rspid
= rsp
.get("id")
752 if rspid
and rspref
and rspid
== rspref
:
755 raise EngineException(
756 "Error at vnffgd[id='{}']:classifier[id='{}']:rsp-id-ref '{}' does not match any rsp:id"
757 .format(fgd
["id"], cls
["id"], rspref
),
758 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
761 def _validate_input_edit(self
, indata
, force
=False):
762 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
765 def _check_descriptor_dependencies(self
, session
, descriptor
):
767 Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd
768 connection points are ok
769 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
770 :param descriptor: descriptor to be inserted or edit
771 :return: None or raises exception
775 member_vnfd_index
= {}
776 if descriptor
.get("constituent-vnfd") and not session
["force"]:
777 for vnf
in descriptor
["constituent-vnfd"]:
778 vnfd_id
= vnf
["vnfd-id-ref"]
779 filter_q
= self
._get
_project
_filter
(session
)
780 filter_q
["id"] = vnfd_id
781 vnf_list
= self
.db
.get_list("vnfds", filter_q
)
783 raise EngineException("Descriptor error at 'constituent-vnfd':'vnfd-id-ref'='{}' references a non "
784 "existing vnfd".format(vnfd_id
), http_code
=HTTPStatus
.CONFLICT
)
785 # elif len(vnf_list) > 1:
786 # raise EngineException("More than one vnfd found for id='{}'".format(vnfd_id),
787 # http_code=HTTPStatus.CONFLICT)
788 member_vnfd_index
[vnf
["member-vnf-index"]] = vnf_list
[0]
790 # Cross references validation in the descriptor and vnfd connection point validation
791 for vld
in get_iterable(descriptor
.get("vld")):
792 for referenced_vnfd_cp
in get_iterable(vld
.get("vnfd-connection-point-ref")):
793 # look if this vnfd contains this connection point
794 vnfd
= member_vnfd_index
.get(referenced_vnfd_cp
["member-vnf-index-ref"])
795 for vnfd_cp
in get_iterable(vnfd
.get("connection-point")):
796 if referenced_vnfd_cp
.get("vnfd-connection-point-ref") == vnfd_cp
["name"]:
799 raise EngineException(
800 "Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}']:vnfd-"
801 "connection-point-ref='{}' references a non existing conection-point:name inside vnfd '{}'"
802 .format(vld
["id"], referenced_vnfd_cp
["member-vnf-index-ref"],
803 referenced_vnfd_cp
["vnfd-connection-point-ref"], vnfd
["id"]),
804 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
806 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
807 super().check_conflict_on_edit(session
, final_content
, edit_content
, _id
)
809 self
._check
_descriptor
_dependencies
(session
, final_content
)
811 def check_conflict_on_del(self
, session
, _id
, db_content
):
813 Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note
814 that NSD can be public and be used by other projects.
815 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
816 :param _id: nsd internal id
817 :param db_content: The database content of the _id
818 :return: None or raises EngineException with the conflict
822 descriptor
= db_content
823 descriptor_id
= descriptor
.get("id")
824 if not descriptor_id
: # empty nsd not uploaded
827 # check NSD used by NS
828 _filter
= self
._get
_project
_filter
(session
)
829 _filter
["nsd-id"] = _id
830 if self
.db
.get_list("nsrs", _filter
):
831 raise EngineException("There is at least one NS using this descriptor", http_code
=HTTPStatus
.CONFLICT
)
833 # check NSD referenced by NST
834 del _filter
["nsd-id"]
835 _filter
["netslice-subnet.ANYINDEX.nsd-ref"] = descriptor_id
836 if self
.db
.get_list("nsts", _filter
):
837 raise EngineException("There is at least one NetSlice Template referencing this descriptor",
838 http_code
=HTTPStatus
.CONFLICT
)
841 class NstTopic(DescriptorTopic
):
845 def __init__(self
, db
, fs
, msg
, auth
):
846 DescriptorTopic
.__init
__(self
, db
, fs
, msg
, auth
)
849 def _remove_envelop(indata
=None):
852 clean_indata
= indata
854 if clean_indata
.get('nst'):
855 if not isinstance(clean_indata
['nst'], list) or len(clean_indata
['nst']) != 1:
856 raise EngineException("'nst' must be a list only one element")
857 clean_indata
= clean_indata
['nst'][0]
858 elif clean_indata
.get('nst:nst'):
859 if not isinstance(clean_indata
['nst:nst'], list) or len(clean_indata
['nst:nst']) != 1:
860 raise EngineException("'nst:nst' must be a list only one element")
861 clean_indata
= clean_indata
['nst:nst'][0]
864 def _validate_input_edit(self
, indata
, force
=False):
865 # TODO validate with pyangbind, serialize
868 def _validate_input_new(self
, indata
, storage_params
, force
=False):
869 indata
= self
.pyangbind_validation("nsts", indata
, force
)
872 def _check_descriptor_dependencies(self
, session
, descriptor
):
874 Check that the dependent descriptors exist on a new descriptor or edition
875 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
876 :param descriptor: descriptor to be inserted or edit
877 :return: None or raises exception
879 if not descriptor
.get("netslice-subnet"):
881 for nsd
in descriptor
["netslice-subnet"]:
882 nsd_id
= nsd
["nsd-ref"]
883 filter_q
= self
._get
_project
_filter
(session
)
884 filter_q
["id"] = nsd_id
885 if not self
.db
.get_list("nsds", filter_q
):
886 raise EngineException("Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non "
887 "existing nsd".format(nsd_id
), http_code
=HTTPStatus
.CONFLICT
)
889 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
890 super().check_conflict_on_edit(session
, final_content
, edit_content
, _id
)
892 self
._check
_descriptor
_dependencies
(session
, final_content
)
894 def check_conflict_on_del(self
, session
, _id
, db_content
):
896 Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note
897 that NST can be public and be used by other projects.
898 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
899 :param _id: nst internal id
900 :param db_content: The database content of the _id.
901 :return: None or raises EngineException with the conflict
903 # TODO: Check this method
906 # Get Network Slice Template from Database
907 _filter
= self
._get
_project
_filter
(session
)
908 _filter
["_admin.nst-id"] = _id
909 if self
.db
.get_list("nsis", _filter
):
910 raise EngineException("there is at least one Netslice Instance using this descriptor",
911 http_code
=HTTPStatus
.CONFLICT
)
914 class PduTopic(BaseTopic
):
917 schema_new
= pdu_new_schema
918 schema_edit
= pdu_edit_schema
920 def __init__(self
, db
, fs
, msg
, auth
):
921 BaseTopic
.__init
__(self
, db
, fs
, msg
, auth
)
924 def format_on_new(content
, project_id
=None, make_public
=False):
925 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
926 content
["_admin"]["onboardingState"] = "CREATED"
927 content
["_admin"]["operationalState"] = "ENABLED"
928 content
["_admin"]["usageState"] = "NOT_IN_USE"
930 def check_conflict_on_del(self
, session
, _id
, db_content
):
932 Check that there is not any vnfr that uses this PDU
933 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
934 :param _id: pdu internal id
935 :param db_content: The database content of the _id.
936 :return: None or raises EngineException with the conflict
941 _filter
= self
._get
_project
_filter
(session
)
942 _filter
["vdur.pdu-id"] = _id
943 if self
.db
.get_list("vnfrs", _filter
):
944 raise EngineException("There is at least one VNF using this PDU", http_code
=HTTPStatus
.CONFLICT
)