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
23 from osm_nbi
.validation
import ValidationError
, pdu_new_schema
, pdu_edit_schema
24 from osm_nbi
.base_topic
import BaseTopic
, EngineException
, get_iterable
25 from osm_im
.vnfd
import vnfd
as vnfd_im
26 from osm_im
.nsd
import nsd
as nsd_im
27 from osm_im
.nst
import nst
as nst_im
28 from pyangbind
.lib
.serialise
import pybindJSONDecoder
29 import pyangbind
.lib
.pybindJSON
as pybindJSON
31 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
34 class DescriptorTopic(BaseTopic
):
36 def __init__(self
, db
, fs
, msg
, auth
):
37 BaseTopic
.__init
__(self
, db
, fs
, msg
, auth
)
39 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
40 super().check_conflict_on_edit(session
, final_content
, edit_content
, _id
)
41 # 1. validate again with pyangbind
42 # 1.1. remove internal keys
44 for k
in ("_id", "_admin"):
45 if k
in final_content
:
46 internal_keys
[k
] = final_content
.pop(k
)
47 storage_params
= internal_keys
["_admin"].get("storage")
48 serialized
= self
._validate
_input
_new
(final_content
, storage_params
, session
["force"])
49 # 1.2. modify final_content with a serialized version
51 final_content
.update(serialized
)
52 # 1.3. restore internal keys
53 for k
, v
in internal_keys
.items():
58 # 2. check that this id is not present
59 if "id" in edit_content
:
60 _filter
= self
._get
_project
_filter
(session
)
61 _filter
["id"] = final_content
["id"]
62 _filter
["_id.neq"] = _id
63 if self
.db
.get_one(self
.topic
, _filter
, fail_on_empty
=False):
64 raise EngineException("{} with id '{}' already exists for this project".format(self
.topic
[:-1],
69 def format_on_new(content
, project_id
=None, make_public
=False):
70 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
71 content
["_admin"]["onboardingState"] = "CREATED"
72 content
["_admin"]["operationalState"] = "DISABLED"
73 content
["_admin"]["usageState"] = "NOT_IN_USE"
75 def delete_extra(self
, session
, _id
, db_content
):
77 Deletes file system storage associated with the descriptor
78 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
79 :param _id: server internal id
80 :param db_content: The database content of the descriptor
81 :return: None if ok or raises EngineException with the problem
83 self
.fs
.file_delete(_id
, ignore_non_exist
=True)
84 self
.fs
.file_delete(_id
+ "_", ignore_non_exist
=True) # remove temp folder
87 def get_one_by_id(db
, session
, topic
, id):
88 # find owned by this project
89 _filter
= BaseTopic
._get
_project
_filter
(session
)
91 desc_list
= db
.get_list(topic
, _filter
)
92 if len(desc_list
) == 1:
94 elif len(desc_list
) > 1:
95 raise DbException("Found more than one {} with id='{}' belonging to this project".format(topic
[:-1], id),
98 # not found any: try to find public
99 _filter
= BaseTopic
._get
_project
_filter
(session
)
101 desc_list
= db
.get_list(topic
, _filter
)
103 raise DbException("Not found any {} with id='{}'".format(topic
[:-1], id), HTTPStatus
.NOT_FOUND
)
104 elif len(desc_list
) == 1:
107 raise DbException("Found more than one public {} with id='{}'; and no one belonging to this project".format(
108 topic
[:-1], id), HTTPStatus
.CONFLICT
)
110 def new(self
, rollback
, session
, indata
=None, kwargs
=None, headers
=None):
112 Creates a new almost empty DISABLED entry into database. Due to SOL005, it does not follow normal procedure.
113 Creating a VNFD or NSD is done in two steps: 1. Creates an empty descriptor (this step) and 2) upload content
114 (self.upload_content)
115 :param rollback: list to append created items at database in case a rollback may to be done
116 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
117 :param indata: data to be inserted
118 :param kwargs: used to override the indata descriptor
119 :param headers: http request headers
120 :return: _id, None: identity of the inserted data; and None as there is not any operation
125 self
.check_quota(session
)
129 if "userDefinedData" in indata
:
130 indata
= indata
['userDefinedData']
132 # Override descriptor with query string kwargs
133 self
._update
_input
_with
_kwargs
(indata
, kwargs
)
134 # uncomment when this method is implemented.
135 # Avoid override in this case as the target is userDefinedData, but not vnfd,nsd descriptors
136 # indata = DescriptorTopic._validate_input_new(self, indata, project_id=session["force"])
138 content
= {"_admin": {"userDefinedData": indata
}}
139 self
.format_on_new(content
, session
["project_id"], make_public
=session
["public"])
140 _id
= self
.db
.create(self
.topic
, content
)
141 rollback
.append({"topic": self
.topic
, "_id": _id
})
143 except ValidationError
as e
:
144 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
146 def upload_content(self
, session
, _id
, indata
, kwargs
, headers
):
148 Used for receiving content by chunks (with a transaction_id header and/or gzip file. It will store and extract)
149 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
150 :param _id : the nsd,vnfd is already created, this is the id
151 :param indata: http body request
152 :param kwargs: user query string to override parameters. NOT USED
153 :param headers: http request headers
154 :return: True if package is completely uploaded or False if partial content has been uploded
155 Raise exception on error
157 # Check that _id exists and it is valid
158 current_desc
= self
.show(session
, _id
)
160 content_range_text
= headers
.get("Content-Range")
161 expected_md5
= headers
.get("Content-File-MD5")
163 content_type
= headers
.get("Content-Type")
164 if content_type
and "application/gzip" in content_type
or "application/x-gzip" in content_type
or \
165 "application/zip" in content_type
:
167 filename
= headers
.get("Content-Filename")
169 filename
= "package.tar.gz" if compressed
else "package"
170 # TODO change to Content-Disposition filename https://tools.ietf.org/html/rfc6266
174 if content_range_text
:
175 content_range
= content_range_text
.replace("-", " ").replace("/", " ").split()
176 if content_range
[0] != "bytes": # TODO check x<y not negative < total....
178 start
= int(content_range
[1])
179 end
= int(content_range
[2]) + 1
180 total
= int(content_range
[3])
183 temp_folder
= _id
+ "_" # all the content is upload here and if ok, it is rename from id_ to is folder
186 if not self
.fs
.file_exists(temp_folder
, 'dir'):
187 raise EngineException("invalid Transaction-Id header", HTTPStatus
.NOT_FOUND
)
189 self
.fs
.file_delete(temp_folder
, ignore_non_exist
=True)
190 self
.fs
.mkdir(temp_folder
)
192 storage
= self
.fs
.get_params()
193 storage
["folder"] = _id
195 file_path
= (temp_folder
, filename
)
196 if self
.fs
.file_exists(file_path
, 'file'):
197 file_size
= self
.fs
.file_size(file_path
)
200 if file_size
!= start
:
201 raise EngineException("invalid Content-Range start sequence, expected '{}' but received '{}'".format(
202 file_size
, start
), HTTPStatus
.REQUESTED_RANGE_NOT_SATISFIABLE
)
203 file_pkg
= self
.fs
.file_open(file_path
, 'a+b')
204 if isinstance(indata
, dict):
205 indata_text
= yaml
.safe_dump(indata
, indent
=4, default_flow_style
=False)
206 file_pkg
.write(indata_text
.encode(encoding
="utf-8"))
210 indata_text
= indata
.read(4096)
211 indata_len
+= len(indata_text
)
214 file_pkg
.write(indata_text
)
215 if content_range_text
:
216 if indata_len
!= end
-start
:
217 raise EngineException("Mismatch between Content-Range header {}-{} and body length of {}".format(
218 start
, end
-1, indata_len
), HTTPStatus
.REQUESTED_RANGE_NOT_SATISFIABLE
)
220 # TODO update to UPLOADING
227 chunk_data
= file_pkg
.read(1024)
229 file_md5
.update(chunk_data
)
230 chunk_data
= file_pkg
.read(1024)
231 if expected_md5
!= file_md5
.hexdigest():
232 raise EngineException("Error, MD5 mismatch", HTTPStatus
.CONFLICT
)
234 if compressed
== "gzip":
235 tar
= tarfile
.open(mode
='r', fileobj
=file_pkg
)
236 descriptor_file_name
= None
238 tarname
= tarinfo
.name
239 tarname_path
= tarname
.split("/")
240 if not tarname_path
[0] or ".." in tarname_path
: # if start with "/" means absolute path
241 raise EngineException("Absolute path or '..' are not allowed for package descriptor tar.gz")
242 if len(tarname_path
) == 1 and not tarinfo
.isdir():
243 raise EngineException("All files must be inside a dir for package descriptor tar.gz")
244 if tarname
.endswith(".yaml") or tarname
.endswith(".json") or tarname
.endswith(".yml"):
245 storage
["pkg-dir"] = tarname_path
[0]
246 if len(tarname_path
) == 2:
247 if descriptor_file_name
:
248 raise EngineException(
249 "Found more than one descriptor file at package descriptor tar.gz")
250 descriptor_file_name
= tarname
251 if not descriptor_file_name
:
252 raise EngineException("Not found any descriptor file at package descriptor tar.gz")
253 storage
["descriptor"] = descriptor_file_name
254 storage
["zipfile"] = filename
255 self
.fs
.file_extract(tar
, temp_folder
)
256 with self
.fs
.file_open((temp_folder
, descriptor_file_name
), "r") as descriptor_file
:
257 content
= descriptor_file
.read()
259 content
= file_pkg
.read()
260 storage
["descriptor"] = descriptor_file_name
= filename
262 if descriptor_file_name
.endswith(".json"):
263 error_text
= "Invalid json format "
264 indata
= json
.load(content
)
266 error_text
= "Invalid yaml format "
267 indata
= yaml
.load(content
)
269 current_desc
["_admin"]["storage"] = storage
270 current_desc
["_admin"]["onboardingState"] = "ONBOARDED"
271 current_desc
["_admin"]["operationalState"] = "ENABLED"
273 indata
= self
._remove
_envelop
(indata
)
275 # Override descriptor with query string kwargs
277 self
._update
_input
_with
_kwargs
(indata
, kwargs
)
278 # it will call overrides method at VnfdTopic or NsdTopic
279 # indata = self._validate_input_edit(indata, force=session["force"])
281 deep_update_rfc7396(current_desc
, indata
)
282 self
.check_conflict_on_edit(session
, current_desc
, indata
, _id
=_id
)
283 self
.db
.replace(self
.topic
, _id
, current_desc
)
284 self
.fs
.dir_rename(temp_folder
, _id
)
287 self
._send
_msg
("created", indata
)
289 # TODO if descriptor has changed because kwargs update content and remove cached zip
290 # TODO if zip is not present creates one
293 except EngineException
:
296 raise EngineException("invalid Content-Range header format. Expected 'bytes start-end/total'",
297 HTTPStatus
.REQUESTED_RANGE_NOT_SATISFIABLE
)
299 raise EngineException("invalid upload transaction sequence: '{}'".format(e
), HTTPStatus
.BAD_REQUEST
)
300 except tarfile
.ReadError
as e
:
301 raise EngineException("invalid file content {}".format(e
), HTTPStatus
.BAD_REQUEST
)
302 except (ValueError, yaml
.YAMLError
) as e
:
303 raise EngineException(error_text
+ str(e
))
304 except ValidationError
as e
:
305 raise EngineException(e
, HTTPStatus
.UNPROCESSABLE_ENTITY
)
310 def get_file(self
, session
, _id
, path
=None, accept_header
=None):
312 Return the file content of a vnfd or nsd
313 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
314 :param _id: Identity of the vnfd, nsd
315 :param path: artifact path or "$DESCRIPTOR" or None
316 :param accept_header: Content of Accept header. Must contain applition/zip or/and text/plain
317 :return: opened file plus Accept format or raises an exception
319 accept_text
= accept_zip
= False
321 if 'text/plain' in accept_header
or '*/*' in accept_header
:
323 if 'application/zip' in accept_header
or '*/*' in accept_header
:
324 accept_zip
= 'application/zip'
325 elif 'application/gzip' in accept_header
:
326 accept_zip
= 'application/gzip'
328 if not accept_text
and not accept_zip
:
329 raise EngineException("provide request header 'Accept' with 'application/zip' or 'text/plain'",
330 http_code
=HTTPStatus
.NOT_ACCEPTABLE
)
332 content
= self
.show(session
, _id
)
333 if content
["_admin"]["onboardingState"] != "ONBOARDED":
334 raise EngineException("Cannot get content because this resource is not at 'ONBOARDED' state. "
335 "onboardingState is {}".format(content
["_admin"]["onboardingState"]),
336 http_code
=HTTPStatus
.CONFLICT
)
337 storage
= content
["_admin"]["storage"]
338 if path
is not None and path
!= "$DESCRIPTOR": # artifacts
339 if not storage
.get('pkg-dir'):
340 raise EngineException("Packages does not contains artifacts", http_code
=HTTPStatus
.BAD_REQUEST
)
341 if self
.fs
.file_exists((storage
['folder'], storage
['pkg-dir'], *path
), 'dir'):
342 folder_content
= self
.fs
.dir_ls((storage
['folder'], storage
['pkg-dir'], *path
))
343 return folder_content
, "text/plain"
344 # TODO manage folders in http
346 return self
.fs
.file_open((storage
['folder'], storage
['pkg-dir'], *path
), "rb"),\
347 "application/octet-stream"
349 # pkgtype accept ZIP TEXT -> result
350 # manyfiles yes X -> zip
352 # onefile yes no -> zip
355 if accept_text
and (not storage
.get('pkg-dir') or path
== "$DESCRIPTOR"):
356 return self
.fs
.file_open((storage
['folder'], storage
['descriptor']), "r"), "text/plain"
357 elif storage
.get('pkg-dir') and not accept_zip
:
358 raise EngineException("Packages that contains several files need to be retrieved with 'application/zip'"
359 "Accept header", http_code
=HTTPStatus
.NOT_ACCEPTABLE
)
361 if not storage
.get('zipfile'):
362 # TODO generate zipfile if not present
363 raise EngineException("Only allowed 'text/plain' Accept header for this descriptor. To be solved in "
364 "future versions", http_code
=HTTPStatus
.NOT_ACCEPTABLE
)
365 return self
.fs
.file_open((storage
['folder'], storage
['zipfile']), "rb"), accept_zip
367 def pyangbind_validation(self
, item
, data
, force
=False):
371 pybindJSONDecoder
.load_ietf_json({'vnfd:vnfd-catalog': {'vnfd': [data
]}}, None, None, obj
=myvnfd
,
372 path_helper
=True, skip_unknown
=force
)
373 out
= pybindJSON
.dumps(myvnfd
, mode
="ietf")
376 pybindJSONDecoder
.load_ietf_json({'nsd:nsd-catalog': {'nsd': [data
]}}, None, None, obj
=mynsd
,
377 path_helper
=True, skip_unknown
=force
)
378 out
= pybindJSON
.dumps(mynsd
, mode
="ietf")
381 pybindJSONDecoder
.load_ietf_json({'nst': [data
]}, None, None, obj
=mynst
,
382 path_helper
=True, skip_unknown
=force
)
383 out
= pybindJSON
.dumps(mynst
, mode
="ietf")
385 raise EngineException("Not possible to validate '{}' item".format(item
),
386 http_code
=HTTPStatus
.INTERNAL_SERVER_ERROR
)
388 desc_out
= self
._remove
_envelop
(yaml
.safe_load(out
))
391 except Exception as e
:
392 raise EngineException("Error in pyangbind validation: {}".format(str(e
)),
393 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
396 class VnfdTopic(DescriptorTopic
):
400 def __init__(self
, db
, fs
, msg
, auth
):
401 DescriptorTopic
.__init
__(self
, db
, fs
, msg
, auth
)
404 def _remove_envelop(indata
=None):
407 clean_indata
= indata
408 if clean_indata
.get('vnfd:vnfd-catalog'):
409 clean_indata
= clean_indata
['vnfd:vnfd-catalog']
410 elif clean_indata
.get('vnfd-catalog'):
411 clean_indata
= clean_indata
['vnfd-catalog']
412 if clean_indata
.get('vnfd'):
413 if not isinstance(clean_indata
['vnfd'], list) or len(clean_indata
['vnfd']) != 1:
414 raise EngineException("'vnfd' must be a list of only one element")
415 clean_indata
= clean_indata
['vnfd'][0]
416 elif clean_indata
.get('vnfd:vnfd'):
417 if not isinstance(clean_indata
['vnfd:vnfd'], list) or len(clean_indata
['vnfd:vnfd']) != 1:
418 raise EngineException("'vnfd:vnfd' must be a list of only one element")
419 clean_indata
= clean_indata
['vnfd:vnfd'][0]
422 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
423 super().check_conflict_on_edit(session
, final_content
, edit_content
, _id
)
428 for vdu
in get_iterable(final_content
.get("vdu")):
429 if vdu
.get("pdu-type"):
434 final_content
["_admin"]["type"] = "hnfd" if contains_vdu
else "pnfd"
436 final_content
["_admin"]["type"] = "vnfd"
437 # if neither vud nor pdu do not fill type
439 def check_conflict_on_del(self
, session
, _id
, db_content
):
441 Check that there is not any NSD that uses this VNFD. Only NSDs belonging to this project are considered. Note
442 that VNFD can be public and be used by NSD of other projects. Also check there are not deployments, or vnfr
444 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
445 :param _id: vnfd internal id
446 :param db_content: The database content of the _id.
447 :return: None or raises EngineException with the conflict
451 descriptor
= db_content
452 descriptor_id
= descriptor
.get("id")
453 if not descriptor_id
: # empty vnfd not uploaded
456 _filter
= self
._get
_project
_filter
(session
)
458 # check vnfrs using this vnfd
459 _filter
["vnfd-id"] = _id
460 if self
.db
.get_list("vnfrs", _filter
):
461 raise EngineException("There is at least one VNF using this descriptor", http_code
=HTTPStatus
.CONFLICT
)
463 # check NSD referencing this VNFD
464 del _filter
["vnfd-id"]
465 _filter
["constituent-vnfd.ANYINDEX.vnfd-id-ref"] = descriptor_id
466 if self
.db
.get_list("nsds", _filter
):
467 raise EngineException("There is at least one NSD referencing this descriptor",
468 http_code
=HTTPStatus
.CONFLICT
)
470 def _validate_input_new(self
, indata
, storage_params
, force
=False):
471 indata
= self
.pyangbind_validation("vnfds", indata
, force
)
472 # Cross references validation in the descriptor
473 if indata
.get("vdu"):
474 if not indata
.get("mgmt-interface"):
475 raise EngineException("'mgmt-interface' is a mandatory field and it is not defined",
476 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
477 if indata
["mgmt-interface"].get("cp"):
478 for cp
in get_iterable(indata
.get("connection-point")):
479 if cp
["name"] == indata
["mgmt-interface"]["cp"]:
482 raise EngineException("mgmt-interface:cp='{}' must match an existing connection-point"
483 .format(indata
["mgmt-interface"]["cp"]),
484 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
486 for vdu
in get_iterable(indata
.get("vdu")):
487 for interface
in get_iterable(vdu
.get("interface")):
488 if interface
.get("external-connection-point-ref"):
489 for cp
in get_iterable(indata
.get("connection-point")):
490 if cp
["name"] == interface
["external-connection-point-ref"]:
493 raise EngineException("vdu[id='{}']:interface[name='{}']:external-connection-point-ref='{}' "
494 "must match an existing connection-point"
495 .format(vdu
["id"], interface
["name"],
496 interface
["external-connection-point-ref"]),
497 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
499 elif interface
.get("internal-connection-point-ref"):
500 for internal_cp
in get_iterable(vdu
.get("internal-connection-point")):
501 if interface
["internal-connection-point-ref"] == internal_cp
.get("id"):
504 raise EngineException("vdu[id='{}']:interface[name='{}']:internal-connection-point-ref='{}' "
505 "must match an existing vdu:internal-connection-point"
506 .format(vdu
["id"], interface
["name"],
507 interface
["internal-connection-point-ref"]),
508 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
509 # Validate that if descriptor contains charms, artifacts _admin.storage."pkg-dir" is not none
510 if vdu
.get("vdu-configuration"):
511 if vdu
["vdu-configuration"].get("juju"):
512 if not self
._validate
_package
_folders
(storage_params
, 'charms'):
513 raise EngineException("Charm defined in vnf[id={}]:vdu[id={}] but not present in "
514 "package".format(indata
["id"], vdu
["id"]))
515 # Validate that if descriptor contains cloud-init, artifacts _admin.storage."pkg-dir" is not none
516 if vdu
.get("cloud-init-file"):
517 if not self
._validate
_package
_folders
(storage_params
, 'cloud_init', vdu
["cloud-init-file"]):
518 raise EngineException("Cloud-init defined in vnf[id={}]:vdu[id={}] but not present in "
519 "package".format(indata
["id"], vdu
["id"]))
520 # Validate that if descriptor contains charms, artifacts _admin.storage."pkg-dir" is not none
521 if indata
.get("vnf-configuration"):
522 if indata
["vnf-configuration"].get("juju"):
523 if not self
._validate
_package
_folders
(storage_params
, 'charms'):
524 raise EngineException("Charm defined in vnf[id={}] but not present in "
525 "package".format(indata
["id"]))
526 vld_names
= [] # For detection of duplicated VLD names
527 for ivld
in get_iterable(indata
.get("internal-vld")):
528 # BEGIN Detection of duplicated VLD names
529 ivld_name
= ivld
["name"]
530 if ivld_name
in vld_names
:
531 raise EngineException("Duplicated VLD name '{}' in vnfd[id={}]:internal-vld[id={}]"
532 .format(ivld
["name"], indata
["id"], ivld
["id"]),
533 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
535 vld_names
.append(ivld_name
)
536 # END Detection of duplicated VLD names
537 for icp
in get_iterable(ivld
.get("internal-connection-point")):
539 for vdu
in get_iterable(indata
.get("vdu")):
540 for internal_cp
in get_iterable(vdu
.get("internal-connection-point")):
541 if icp
["id-ref"] == internal_cp
["id"]:
547 raise EngineException("internal-vld[id='{}']:internal-connection-point='{}' must match an existing "
548 "vdu:internal-connection-point".format(ivld
["id"], icp
["id-ref"]),
549 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
550 if ivld
.get("ip-profile-ref"):
551 for ip_prof
in get_iterable(indata
.get("ip-profiles")):
552 if ip_prof
["name"] == get_iterable(ivld
.get("ip-profile-ref")):
555 raise EngineException("internal-vld[id='{}']:ip-profile-ref='{}' does not exist".format(
556 ivld
["id"], ivld
["ip-profile-ref"]),
557 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
558 for mp
in get_iterable(indata
.get("monitoring-param")):
559 if mp
.get("vdu-monitoring-param"):
561 for vdu
in get_iterable(indata
.get("vdu")):
562 for vmp
in get_iterable(vdu
.get("monitoring-param")):
563 if vmp
["id"] == mp
["vdu-monitoring-param"].get("vdu-monitoring-param-ref") and vdu
["id"] ==\
564 mp
["vdu-monitoring-param"]["vdu-ref"]:
570 raise EngineException("monitoring-param:vdu-monitoring-param:vdu-monitoring-param-ref='{}' not "
571 "defined at vdu[id='{}'] or vdu does not exist"
572 .format(mp
["vdu-monitoring-param"]["vdu-monitoring-param-ref"],
573 mp
["vdu-monitoring-param"]["vdu-ref"]),
574 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
575 elif mp
.get("vdu-metric"):
577 for vdu
in get_iterable(indata
.get("vdu")):
578 if vdu
.get("vdu-configuration"):
579 for metric
in get_iterable(vdu
["vdu-configuration"].get("metrics")):
580 if metric
["name"] == mp
["vdu-metric"]["vdu-metric-name-ref"] and vdu
["id"] == \
581 mp
["vdu-metric"]["vdu-ref"]:
587 raise EngineException("monitoring-param:vdu-metric:vdu-metric-name-ref='{}' not defined at "
588 "vdu[id='{}'] or vdu does not exist"
589 .format(mp
["vdu-metric"]["vdu-metric-name-ref"],
590 mp
["vdu-metric"]["vdu-ref"]),
591 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
593 for sgd
in get_iterable(indata
.get("scaling-group-descriptor")):
594 for sp
in get_iterable(sgd
.get("scaling-policy")):
595 for sc
in get_iterable(sp
.get("scaling-criteria")):
596 for mp
in get_iterable(indata
.get("monitoring-param")):
597 if mp
["id"] == get_iterable(sc
.get("vnf-monitoring-param-ref")):
600 raise EngineException("scaling-group-descriptor[name='{}']:scaling-criteria[name='{}']:"
601 "vnf-monitoring-param-ref='{}' not defined in any monitoring-param"
602 .format(sgd
["name"], sc
["name"], sc
["vnf-monitoring-param-ref"]),
603 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
604 for sgd_vdu
in get_iterable(sgd
.get("vdu")):
606 for vdu
in get_iterable(indata
.get("vdu")):
607 if vdu
["id"] == sgd_vdu
["vdu-id-ref"]:
613 raise EngineException("scaling-group-descriptor[name='{}']:vdu-id-ref={} does not match any vdu"
614 .format(sgd
["name"], sgd_vdu
["vdu-id-ref"]),
615 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
616 for sca
in get_iterable(sgd
.get("scaling-config-action")):
617 if not indata
.get("vnf-configuration"):
618 raise EngineException("'vnf-configuration' not defined in the descriptor but it is referenced by "
619 "scaling-group-descriptor[name='{}']:scaling-config-action"
620 .format(sgd
["name"]),
621 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
622 for primitive
in get_iterable(indata
["vnf-configuration"].get("config-primitive")):
623 if primitive
["name"] == sca
["vnf-config-primitive-name-ref"]:
626 raise EngineException("scaling-group-descriptor[name='{}']:scaling-config-action:vnf-config-"
627 "primitive-name-ref='{}' does not match any "
628 "vnf-configuration:config-primitive:name"
629 .format(sgd
["name"], sca
["vnf-config-primitive-name-ref"]),
630 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
633 def _validate_input_edit(self
, indata
, force
=False):
634 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
637 def _validate_package_folders(self
, storage_params
, folder
, file=None):
638 if not storage_params
or not storage_params
.get("pkg-dir"):
641 if self
.fs
.file_exists("{}_".format(storage_params
["folder"]), 'dir'):
642 f
= "{}_/{}/{}".format(storage_params
["folder"], storage_params
["pkg-dir"], folder
)
644 f
= "{}/{}/{}".format(storage_params
["folder"], storage_params
["pkg-dir"], folder
)
646 return self
.fs
.file_exists("{}/{}".format(f
, file), 'file')
648 if self
.fs
.file_exists(f
, 'dir'):
649 if self
.fs
.dir_ls(f
):
654 class NsdTopic(DescriptorTopic
):
658 def __init__(self
, db
, fs
, msg
, auth
):
659 DescriptorTopic
.__init
__(self
, db
, fs
, msg
, auth
)
662 def _remove_envelop(indata
=None):
665 clean_indata
= indata
667 if clean_indata
.get('nsd:nsd-catalog'):
668 clean_indata
= clean_indata
['nsd:nsd-catalog']
669 elif clean_indata
.get('nsd-catalog'):
670 clean_indata
= clean_indata
['nsd-catalog']
671 if clean_indata
.get('nsd'):
672 if not isinstance(clean_indata
['nsd'], list) or len(clean_indata
['nsd']) != 1:
673 raise EngineException("'nsd' must be a list of only one element")
674 clean_indata
= clean_indata
['nsd'][0]
675 elif clean_indata
.get('nsd:nsd'):
676 if not isinstance(clean_indata
['nsd:nsd'], list) or len(clean_indata
['nsd:nsd']) != 1:
677 raise EngineException("'nsd:nsd' must be a list of only one element")
678 clean_indata
= clean_indata
['nsd:nsd'][0]
681 def _validate_input_new(self
, indata
, storage_params
, force
=False):
682 indata
= self
.pyangbind_validation("nsds", indata
, force
)
683 # Cross references validation in the descriptor
684 # TODO validata that if contains cloud-init-file or charms, have artifacts _admin.storage."pkg-dir" is not none
685 for vld
in get_iterable(indata
.get("vld")):
686 if vld
.get("mgmt-network") and vld
.get("ip-profile-ref"):
687 raise EngineException("Error at vld[id='{}']:ip-profile-ref"
688 " You cannot set an ip-profile when mgmt-network is True"
689 .format(vld
["id"]), http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
690 for vnfd_cp
in get_iterable(vld
.get("vnfd-connection-point-ref")):
691 for constituent_vnfd
in get_iterable(indata
.get("constituent-vnfd")):
692 if vnfd_cp
["member-vnf-index-ref"] == constituent_vnfd
["member-vnf-index"]:
693 if vnfd_cp
.get("vnfd-id-ref") and vnfd_cp
["vnfd-id-ref"] != constituent_vnfd
["vnfd-id-ref"]:
694 raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[vnfd-id-ref='{}'] "
695 "does not match constituent-vnfd[member-vnf-index='{}']:vnfd-id-ref"
696 " '{}'".format(vld
["id"], vnfd_cp
["vnfd-id-ref"],
697 constituent_vnfd
["member-vnf-index"],
698 constituent_vnfd
["vnfd-id-ref"]),
699 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
702 raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}'] "
703 "does not match any constituent-vnfd:member-vnf-index"
704 .format(vld
["id"], vnfd_cp
["member-vnf-index-ref"]),
705 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
708 def _validate_input_edit(self
, indata
, force
=False):
709 # not needed to validate with pyangbind becuase it will be validated at check_conflict_on_edit
712 def _check_descriptor_dependencies(self
, session
, descriptor
):
714 Check that the dependent descriptors exist on a new descriptor or edition. Also checks references to vnfd
715 connection points are ok
716 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
717 :param descriptor: descriptor to be inserted or edit
718 :return: None or raises exception
722 member_vnfd_index
= {}
723 if descriptor
.get("constituent-vnfd") and not session
["force"]:
724 for vnf
in descriptor
["constituent-vnfd"]:
725 vnfd_id
= vnf
["vnfd-id-ref"]
726 filter_q
= self
._get
_project
_filter
(session
)
727 filter_q
["id"] = vnfd_id
728 vnf_list
= self
.db
.get_list("vnfds", filter_q
)
730 raise EngineException("Descriptor error at 'constituent-vnfd':'vnfd-id-ref'='{}' references a non "
731 "existing vnfd".format(vnfd_id
), http_code
=HTTPStatus
.CONFLICT
)
732 # elif len(vnf_list) > 1:
733 # raise EngineException("More than one vnfd found for id='{}'".format(vnfd_id),
734 # http_code=HTTPStatus.CONFLICT)
735 member_vnfd_index
[vnf
["member-vnf-index"]] = vnf_list
[0]
737 # Cross references validation in the descriptor and vnfd connection point validation
738 for vld
in get_iterable(descriptor
.get("vld")):
739 for referenced_vnfd_cp
in get_iterable(vld
.get("vnfd-connection-point-ref")):
740 # look if this vnfd contains this connection point
741 vnfd
= member_vnfd_index
.get(referenced_vnfd_cp
["member-vnf-index-ref"])
743 raise EngineException("Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}'] "
744 "does not match any constituent-vnfd:member-vnf-index"
745 .format(vld
["id"], referenced_vnfd_cp
["member-vnf-index-ref"]),
746 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
747 for vnfd_cp
in get_iterable(vnfd
.get("connection-point")):
748 if referenced_vnfd_cp
.get("vnfd-connection-point-ref") == vnfd_cp
["name"]:
751 raise EngineException(
752 "Error at vld[id='{}']:vnfd-connection-point-ref[member-vnf-index-ref='{}']:vnfd-"
753 "connection-point-ref='{}' references a non existing conection-point:name inside vnfd '{}'"
754 .format(vld
["id"], referenced_vnfd_cp
["member-vnf-index-ref"],
755 referenced_vnfd_cp
["vnfd-connection-point-ref"], vnfd
["id"]),
756 http_code
=HTTPStatus
.UNPROCESSABLE_ENTITY
)
758 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
759 super().check_conflict_on_edit(session
, final_content
, edit_content
, _id
)
761 self
._check
_descriptor
_dependencies
(session
, final_content
)
763 def check_conflict_on_del(self
, session
, _id
, db_content
):
765 Check that there is not any NSR that uses this NSD. Only NSRs belonging to this project are considered. Note
766 that NSD can be public and be used by other projects.
767 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
768 :param _id: nsd internal id
769 :param db_content: The database content of the _id
770 :return: None or raises EngineException with the conflict
774 descriptor
= db_content
775 descriptor_id
= descriptor
.get("id")
776 if not descriptor_id
: # empty nsd not uploaded
779 # check NSD used by NS
780 _filter
= self
._get
_project
_filter
(session
)
781 _filter
["nsd-id"] = _id
782 if self
.db
.get_list("nsrs", _filter
):
783 raise EngineException("There is at least one NS using this descriptor", http_code
=HTTPStatus
.CONFLICT
)
785 # check NSD referenced by NST
786 del _filter
["nsd-id"]
787 _filter
["netslice-subnet.ANYINDEX.nsd-ref"] = descriptor_id
788 if self
.db
.get_list("nsts", _filter
):
789 raise EngineException("There is at least one NetSlice Template referencing this descriptor",
790 http_code
=HTTPStatus
.CONFLICT
)
793 class NstTopic(DescriptorTopic
):
797 def __init__(self
, db
, fs
, msg
, auth
):
798 DescriptorTopic
.__init
__(self
, db
, fs
, msg
, auth
)
801 def _remove_envelop(indata
=None):
804 clean_indata
= indata
806 if clean_indata
.get('nst'):
807 if not isinstance(clean_indata
['nst'], list) or len(clean_indata
['nst']) != 1:
808 raise EngineException("'nst' must be a list only one element")
809 clean_indata
= clean_indata
['nst'][0]
810 elif clean_indata
.get('nst:nst'):
811 if not isinstance(clean_indata
['nst:nst'], list) or len(clean_indata
['nst:nst']) != 1:
812 raise EngineException("'nst:nst' must be a list only one element")
813 clean_indata
= clean_indata
['nst:nst'][0]
816 def _validate_input_edit(self
, indata
, force
=False):
817 # TODO validate with pyangbind, serialize
820 def _validate_input_new(self
, indata
, storage_params
, force
=False):
821 indata
= self
.pyangbind_validation("nsts", indata
, force
)
824 def _check_descriptor_dependencies(self
, session
, descriptor
):
826 Check that the dependent descriptors exist on a new descriptor or edition
827 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
828 :param descriptor: descriptor to be inserted or edit
829 :return: None or raises exception
831 if not descriptor
.get("netslice-subnet"):
833 for nsd
in descriptor
["netslice-subnet"]:
834 nsd_id
= nsd
["nsd-ref"]
835 filter_q
= self
._get
_project
_filter
(session
)
836 filter_q
["id"] = nsd_id
837 if not self
.db
.get_list("nsds", filter_q
):
838 raise EngineException("Descriptor error at 'netslice-subnet':'nsd-ref'='{}' references a non "
839 "existing nsd".format(nsd_id
), http_code
=HTTPStatus
.CONFLICT
)
841 def check_conflict_on_edit(self
, session
, final_content
, edit_content
, _id
):
842 super().check_conflict_on_edit(session
, final_content
, edit_content
, _id
)
844 self
._check
_descriptor
_dependencies
(session
, final_content
)
846 def check_conflict_on_del(self
, session
, _id
, db_content
):
848 Check that there is not any NSIR that uses this NST. Only NSIRs belonging to this project are considered. Note
849 that NST can be public and be used by other projects.
850 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
851 :param _id: nst internal id
852 :param db_content: The database content of the _id.
853 :return: None or raises EngineException with the conflict
855 # TODO: Check this method
858 # Get Network Slice Template from Database
859 _filter
= self
._get
_project
_filter
(session
)
860 _filter
["_admin.nst-id"] = _id
861 if self
.db
.get_list("nsis", _filter
):
862 raise EngineException("there is at least one Netslice Instance using this descriptor",
863 http_code
=HTTPStatus
.CONFLICT
)
866 class PduTopic(BaseTopic
):
869 schema_new
= pdu_new_schema
870 schema_edit
= pdu_edit_schema
872 def __init__(self
, db
, fs
, msg
, auth
):
873 BaseTopic
.__init
__(self
, db
, fs
, msg
, auth
)
876 def format_on_new(content
, project_id
=None, make_public
=False):
877 BaseTopic
.format_on_new(content
, project_id
=project_id
, make_public
=make_public
)
878 content
["_admin"]["onboardingState"] = "CREATED"
879 content
["_admin"]["operationalState"] = "ENABLED"
880 content
["_admin"]["usageState"] = "NOT_IN_USE"
882 def check_conflict_on_del(self
, session
, _id
, db_content
):
884 Check that there is not any vnfr that uses this PDU
885 :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
886 :param _id: pdu internal id
887 :param db_content: The database content of the _id.
888 :return: None or raises EngineException with the conflict
893 _filter
= self
._get
_project
_filter
(session
)
894 _filter
["vdur.pdu-id"] = _id
895 if self
.db
.get_list("vnfrs", _filter
):
896 raise EngineException("There is at least one VNF using this PDU", http_code
=HTTPStatus
.CONFLICT
)