Feature 10909: Heal operation for VDU
[osm/osmclient.git] / osmclient / sol005 / ns.py
1 # Copyright 2018 Telefonica
2 #
3 # All Rights Reserved.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License"); you may
6 # not use this file except in compliance with the License. You may obtain
7 # a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 # License for the specific language governing permissions and limitations
15 # under the License.
16
17 """
18 OSM ns API handling
19 """
20
21 from osmclient.common import utils
22 from osmclient.common import wait as WaitForStatus
23 from osmclient.common.exceptions import ClientException
24 from osmclient.common.exceptions import NotFound
25 import yaml
26 import json
27 import logging
28
29
30 class Ns(object):
31 def __init__(self, http=None, client=None):
32 self._http = http
33 self._client = client
34 self._logger = logging.getLogger("osmclient")
35 self._apiName = "/nslcm"
36 self._apiVersion = "/v1"
37 self._apiResource = "/ns_instances_content"
38 self._apiBase = "{}{}{}".format(
39 self._apiName, self._apiVersion, self._apiResource
40 )
41
42 # NS '--wait' option
43 def _wait(self, id, wait_time, deleteFlag=False):
44 self._logger.debug("")
45 # Endpoint to get operation status
46 apiUrlStatus = "{}{}{}".format(
47 self._apiName, self._apiVersion, "/ns_lcm_op_occs"
48 )
49 # Wait for status for NS instance creation/update/deletion
50 if isinstance(wait_time, bool):
51 wait_time = WaitForStatus.TIMEOUT_NS_OPERATION
52 WaitForStatus.wait_for_status(
53 "NS",
54 str(id),
55 wait_time,
56 apiUrlStatus,
57 self._http.get2_cmd,
58 deleteFlag=deleteFlag,
59 )
60
61 def list(self, filter=None):
62 """Returns a list of NS"""
63 self._logger.debug("")
64 self._client.get_token()
65 filter_string = ""
66 if filter:
67 filter_string = "?{}".format(filter)
68 _, resp = self._http.get2_cmd("{}{}".format(self._apiBase, filter_string))
69 if resp:
70 return json.loads(resp)
71 return list()
72
73 def get(self, name):
74 """Returns an NS based on name or id"""
75 self._logger.debug("")
76 self._client.get_token()
77 if utils.validate_uuid4(name):
78 for ns in self.list():
79 if name == ns["_id"]:
80 return ns
81 else:
82 for ns in self.list():
83 if name == ns["name"]:
84 return ns
85 raise NotFound("ns '{}' not found".format(name))
86
87 def get_individual(self, name):
88 self._logger.debug("")
89 self._client.get_token()
90 ns_id = name
91 if not utils.validate_uuid4(name):
92 for ns in self.list():
93 if name == ns["name"]:
94 ns_id = ns["_id"]
95 break
96 try:
97 _, resp = self._http.get2_cmd("{}/{}".format(self._apiBase, ns_id))
98 # resp = self._http.get_cmd('{}/{}/nsd_content'.format(self._apiBase, ns_id))
99 # print(yaml.safe_dump(resp))
100 if resp:
101 return json.loads(resp)
102 except NotFound:
103 raise NotFound("ns '{}' not found".format(name))
104 raise NotFound("ns '{}' not found".format(name))
105
106 def delete(self, name, force=False, config=None, wait=False):
107 """
108 Deletes a Network Service (NS)
109 :param name: name of network service
110 :param force: set force. Direct deletion without cleaning at VIM
111 :param config: parameters of deletion, as:
112 autoremove: Bool (default True)
113 timeout_ns_terminate: int
114 skip_terminate_primitives: Bool (default False) to not exec the terminate primitives
115 :param wait: Make synchronous. Wait until deletion is completed:
116 False to not wait (by default), True to wait a standard time, or int (time to wait)
117 :return: None. Exception if fail
118 """
119 self._logger.debug("")
120 ns = self.get(name)
121 querystring_list = []
122 querystring = ""
123 if config:
124 ns_config = yaml.safe_load(config)
125 querystring_list += ["{}={}".format(k, v) for k, v in ns_config.items()]
126 if force:
127 querystring_list.append("FORCE=True")
128 if querystring_list:
129 querystring = "?" + "&".join(querystring_list)
130 http_code, resp = self._http.delete_cmd(
131 "{}/{}{}".format(self._apiBase, ns["_id"], querystring)
132 )
133 # TODO change to use a POST self._http.post_cmd('{}/{}/terminate{}'.format(_apiBase, ns['_id'], querystring),
134 # postfields_dict=ns_config)
135 # seting autoremove as True by default
136 # print('HTTP CODE: {}'.format(http_code))
137 # print('RESP: {}'.format(resp))
138 if http_code == 202:
139 if wait and resp:
140 resp = json.loads(resp)
141 # For the 'delete' operation, '_id' is used
142 self._wait(resp.get("_id"), wait, deleteFlag=True)
143 else:
144 print("Deletion in progress")
145 elif http_code == 204:
146 print("Deleted")
147 else:
148 msg = resp or ""
149 # if resp:
150 # try:
151 # msg = json.loads(resp)
152 # except ValueError:
153 # msg = resp
154 raise ClientException("failed to delete ns {} - {}".format(name, msg))
155
156 def create(
157 self,
158 nsd_name,
159 nsr_name,
160 account,
161 config=None,
162 ssh_keys=None,
163 description="default description",
164 admin_status="ENABLED",
165 wait=False,
166 timeout=None,
167 ):
168 self._logger.debug("")
169 self._client.get_token()
170 nsd = self._client.nsd.get(nsd_name)
171
172 vim_account_id = {}
173 wim_account_id = {}
174
175 def get_vim_account_id(vim_account):
176 self._logger.debug("")
177 if vim_account_id.get(vim_account):
178 return vim_account_id[vim_account]
179 vim = self._client.vim.get(vim_account)
180 if vim is None:
181 raise NotFound("cannot find vim account '{}'".format(vim_account))
182 vim_account_id[vim_account] = vim["_id"]
183 return vim["_id"]
184
185 def get_wim_account_id(wim_account):
186 self._logger.debug("")
187 # wim_account can be False (boolean) to indicate not use wim account
188 if not isinstance(wim_account, str):
189 return wim_account
190 if wim_account_id.get(wim_account):
191 return wim_account_id[wim_account]
192 wim = self._client.wim.get(wim_account)
193 if wim is None:
194 raise NotFound("cannot find wim account '{}'".format(wim_account))
195 wim_account_id[wim_account] = wim["_id"]
196 return wim["_id"]
197
198 vim_id = get_vim_account_id(account)
199 ns = {}
200 ns["nsdId"] = nsd["_id"]
201 ns["nsName"] = nsr_name
202 ns["nsDescription"] = description
203 ns["vimAccountId"] = vim_id
204 # ns['userdata'] = {}
205 # ns['userdata']['key1']='value1'
206 # ns['userdata']['key2']='value2'
207
208 if ssh_keys is not None:
209 ns["ssh_keys"] = []
210 for pubkeyfile in ssh_keys.split(","):
211 with open(pubkeyfile, "r") as f:
212 ns["ssh_keys"].append(f.read())
213 if timeout:
214 ns["timeout_ns_deploy"] = timeout
215 if config:
216 ns_config = yaml.safe_load(config)
217 if "vim-network-name" in ns_config:
218 ns_config["vld"] = ns_config.pop("vim-network-name")
219 if "vld" in ns_config:
220 if not isinstance(ns_config["vld"], list):
221 raise ClientException(
222 "Error at --config 'vld' must be a list of dictionaries"
223 )
224 for vld in ns_config["vld"]:
225 if not isinstance(vld, dict):
226 raise ClientException(
227 "Error at --config 'vld' must be a list of dictionaries"
228 )
229 if vld.get("vim-network-name"):
230 if isinstance(vld["vim-network-name"], dict):
231 vim_network_name_dict = {}
232 for vim_account, vim_net in vld["vim-network-name"].items():
233 vim_network_name_dict[
234 get_vim_account_id(vim_account)
235 ] = vim_net
236 vld["vim-network-name"] = vim_network_name_dict
237 if "wim_account" in vld and vld["wim_account"] is not None:
238 vld["wimAccountId"] = get_wim_account_id(vld.pop("wim_account"))
239 if "vnf" in ns_config:
240 for vnf in ns_config["vnf"]:
241 if vnf.get("vim_account"):
242 vnf["vimAccountId"] = get_vim_account_id(vnf.pop("vim_account"))
243
244 if "additionalParamsForNs" in ns_config:
245 if not isinstance(ns_config["additionalParamsForNs"], dict):
246 raise ClientException(
247 "Error at --config 'additionalParamsForNs' must be a dictionary"
248 )
249 if "additionalParamsForVnf" in ns_config:
250 if not isinstance(ns_config["additionalParamsForVnf"], list):
251 raise ClientException(
252 "Error at --config 'additionalParamsForVnf' must be a list"
253 )
254 for additional_param_vnf in ns_config["additionalParamsForVnf"]:
255 if not isinstance(additional_param_vnf, dict):
256 raise ClientException(
257 "Error at --config 'additionalParamsForVnf' items must be dictionaries"
258 )
259 if not additional_param_vnf.get("member-vnf-index"):
260 raise ClientException(
261 "Error at --config 'additionalParamsForVnf' items must contain "
262 "'member-vnf-index'"
263 )
264 if "wim_account" in ns_config:
265 wim_account = ns_config.pop("wim_account")
266 if wim_account is not None:
267 ns["wimAccountId"] = get_wim_account_id(wim_account)
268 # rest of parameters without any transformation or checking
269 # "timeout_ns_deploy"
270 # "placement-engine"
271 ns.update(ns_config)
272
273 # print(yaml.safe_dump(ns))
274 try:
275 self._apiResource = "/ns_instances_content"
276 self._apiBase = "{}{}{}".format(
277 self._apiName, self._apiVersion, self._apiResource
278 )
279 headers = self._client._headers
280 headers["Content-Type"] = "application/yaml"
281 http_header = [
282 "{}: {}".format(key, val) for (key, val) in list(headers.items())
283 ]
284 self._http.set_http_header(http_header)
285 http_code, resp = self._http.post_cmd(
286 endpoint=self._apiBase, postfields_dict=ns
287 )
288 # print('HTTP CODE: {}'.format(http_code))
289 # print('RESP: {}'.format(resp))
290 # if http_code in (200, 201, 202, 204):
291 if resp:
292 resp = json.loads(resp)
293 if not resp or "id" not in resp:
294 raise ClientException(
295 "unexpected response from server - {} ".format(resp)
296 )
297 if wait:
298 # Wait for status for NS instance creation
299 self._wait(resp.get("nslcmop_id"), wait)
300 print(resp["id"])
301 return resp["id"]
302 # else:
303 # msg = ""
304 # if resp:
305 # try:
306 # msg = json.loads(resp)
307 # except ValueError:
308 # msg = resp
309 # raise ClientException(msg)
310 except ClientException as exc:
311 message = "failed to create ns: {} nsd: {}\nerror:\n{}".format(
312 nsr_name, nsd_name, str(exc)
313 )
314 raise ClientException(message)
315
316 def list_op(self, name, filter=None):
317 """Returns the list of operations of a NS"""
318 self._logger.debug("")
319 ns = self.get(name)
320 try:
321 self._apiResource = "/ns_lcm_op_occs"
322 self._apiBase = "{}{}{}".format(
323 self._apiName, self._apiVersion, self._apiResource
324 )
325 filter_string = ""
326 if filter:
327 filter_string = "&{}".format(filter)
328 http_code, resp = self._http.get2_cmd(
329 "{}?nsInstanceId={}{}".format(self._apiBase, ns["_id"], filter_string)
330 )
331 # print('HTTP CODE: {}'.format(http_code))
332 # print('RESP: {}'.format(resp))
333 if http_code == 200:
334 if resp:
335 resp = json.loads(resp)
336 return resp
337 else:
338 raise ClientException("unexpected response from server")
339 else:
340 msg = resp or ""
341 # if resp:
342 # try:
343 # resp = json.loads(resp)
344 # msg = resp['detail']
345 # except ValueError:
346 # msg = resp
347 raise ClientException(msg)
348 except ClientException as exc:
349 message = "failed to get operation list of NS {}:\nerror:\n{}".format(
350 name, str(exc)
351 )
352 raise ClientException(message)
353
354 def get_op(self, operationId):
355 """Returns the status of an operation"""
356 self._logger.debug("")
357 self._client.get_token()
358 try:
359 self._apiResource = "/ns_lcm_op_occs"
360 self._apiBase = "{}{}{}".format(
361 self._apiName, self._apiVersion, self._apiResource
362 )
363 http_code, resp = self._http.get2_cmd(
364 "{}/{}".format(self._apiBase, operationId)
365 )
366 # print('HTTP CODE: {}'.format(http_code))
367 # print('RESP: {}'.format(resp))
368 if http_code == 200:
369 if resp:
370 resp = json.loads(resp)
371 return resp
372 else:
373 raise ClientException("unexpected response from server")
374 else:
375 msg = resp or ""
376 # if resp:
377 # try:
378 # resp = json.loads(resp)
379 # msg = resp['detail']
380 # except ValueError:
381 # msg = resp
382 raise ClientException(msg)
383 except ClientException as exc:
384 message = "failed to get status of operation {}:\nerror:\n{}".format(
385 operationId, str(exc)
386 )
387 raise ClientException(message)
388
389 def exec_op(
390 self,
391 name,
392 op_name,
393 op_data=None,
394 wait=False,
395 ):
396 """Executes an operation on a NS"""
397 self._logger.debug("")
398 ns = self.get(name)
399 try:
400 ns = self.get(name)
401 self._apiResource = "/ns_instances"
402 self._apiBase = "{}{}{}".format(
403 self._apiName, self._apiVersion, self._apiResource
404 )
405 endpoint = "{}/{}/{}".format(self._apiBase, ns["_id"], op_name)
406 # print('OP_NAME: {}'.format(op_name))
407 # print('OP_DATA: {}'.format(json.dumps(op_data)))
408 http_code, resp = self._http.post_cmd(
409 endpoint=endpoint, postfields_dict=op_data
410 )
411 # print('HTTP CODE: {}'.format(http_code))
412 # print('RESP: {}'.format(resp))
413 # if http_code in (200, 201, 202, 204):
414 if resp:
415 resp = json.loads(resp)
416 if not resp or "id" not in resp:
417 raise ClientException(
418 "unexpected response from server - {}".format(resp)
419 )
420 if wait:
421 # Wait for status for NS instance action
422 # For the 'action' operation, 'id' is used
423 self._wait(resp.get("id"), wait)
424 return resp["id"]
425 # else:
426 # msg = ""
427 # if resp:
428 # try:
429 # msg = json.loads(resp)
430 # except ValueError:
431 # msg = resp
432 # raise ClientException(msg)
433 except ClientException as exc:
434 message = "failed to exec operation {}:\nerror:\n{}".format(name, str(exc))
435 raise ClientException(message)
436
437 def scale_vnf(
438 self,
439 ns_name,
440 vnf_name,
441 scaling_group,
442 scale_in,
443 scale_out,
444 wait=False,
445 timeout=None,
446 ):
447 """Scales a VNF by adding/removing VDUs"""
448 self._logger.debug("")
449 self._client.get_token()
450 try:
451 op_data = {}
452 op_data["scaleType"] = "SCALE_VNF"
453 op_data["scaleVnfData"] = {}
454 if scale_in and not scale_out:
455 op_data["scaleVnfData"]["scaleVnfType"] = "SCALE_IN"
456 elif not scale_in and scale_out:
457 op_data["scaleVnfData"]["scaleVnfType"] = "SCALE_OUT"
458 else:
459 raise ClientException("you must set either 'scale_in' or 'scale_out'")
460 op_data["scaleVnfData"]["scaleByStepData"] = {
461 "member-vnf-index": vnf_name,
462 "scaling-group-descriptor": scaling_group,
463 }
464 if timeout:
465 op_data["timeout_ns_scale"] = timeout
466 op_id = self.exec_op(ns_name, op_name="scale", op_data=op_data, wait=wait)
467 print(str(op_id))
468 except ClientException as exc:
469 message = "failed to scale vnf {} of ns {}:\nerror:\n{}".format(
470 vnf_name, ns_name, str(exc)
471 )
472 raise ClientException(message)
473
474 def update(self, ns_name, data, wait=False):
475 """Update NS instance.
476
477 This function calls the NBI in order to perform an update operation
478 on a Network Service instance.
479
480 Args:
481 ns_name: (str)
482 data: (dict)
483 wait: (boolean)
484
485 Returns:
486 None
487
488 """
489 self._logger.debug("")
490 self._client.get_token()
491 try:
492 op_data = {"updateType": data.pop("updateType")}
493
494 # Check update parameters availability according to update type
495 if op_data["updateType"] == "CHANGE_VNFPKG":
496 if not (
497 data["config"]["changeVnfPackageData"][0].get("vnfInstanceId")
498 and data["config"]["changeVnfPackageData"][0].get("vnfdId")
499 ):
500 raise ClientException("you must set both vnfInstanceId and vnfdId")
501
502 # Fill up op_data
503 op_data["changeVnfPackageData"] = {}
504 op_data["changeVnfPackageData"]["vnfInstanceId"] = data["config"][
505 "changeVnfPackageData"
506 ][0].get("vnfInstanceId")
507
508 op_data["changeVnfPackageData"]["vnfdId"] = data["config"][
509 "changeVnfPackageData"
510 ][0].get("vnfdId")
511
512 if data.get("timeout"):
513 op_data["timeout_ns_update"] = data["timeout"]
514
515 op_id = self.exec_op(ns_name, op_name="update", op_data=op_data, wait=wait)
516 print(str(op_id))
517
518 except ClientException as exc:
519 message = "failed to update ns {}:\nerror:\n{}".format(ns_name, str(exc))
520 raise ClientException(message)
521
522 def create_alarm(self, alarm):
523 self._logger.debug("")
524 self._client.get_token()
525 data = {}
526 data["create_alarm_request"] = {}
527 data["create_alarm_request"]["alarm_create_request"] = alarm
528 try:
529 http_code, resp = self._http.post_cmd(
530 endpoint="/test/message/alarm_request", postfields_dict=data
531 )
532 # print('HTTP CODE: {}'.format(http_code))
533 # print('RESP: {}'.format(resp))
534 # if http_code in (200, 201, 202, 204):
535 # resp = json.loads(resp)
536 print("Alarm created")
537 # else:
538 # msg = ""
539 # if resp:
540 # try:
541 # msg = json.loads(resp)
542 # except ValueError:
543 # msg = resp
544 # raise ClientException('error: code: {}, resp: {}'.format(
545 # http_code, msg))
546 except ClientException as exc:
547 message = "failed to create alarm: alarm {}\n{}".format(alarm, str(exc))
548 raise ClientException(message)
549
550 def delete_alarm(self, name):
551 self._logger.debug("")
552 self._client.get_token()
553 data = {}
554 data["delete_alarm_request"] = {}
555 data["delete_alarm_request"]["alarm_delete_request"] = {}
556 data["delete_alarm_request"]["alarm_delete_request"]["alarm_uuid"] = name
557 try:
558 http_code, resp = self._http.post_cmd(
559 endpoint="/test/message/alarm_request", postfields_dict=data
560 )
561 # print('HTTP CODE: {}'.format(http_code))
562 # print('RESP: {}'.format(resp))
563 # if http_code in (200, 201, 202, 204):
564 # resp = json.loads(resp)
565 print("Alarm deleted")
566 # else:
567 # msg = ""
568 # if resp:
569 # try:
570 # msg = json.loads(resp)
571 # except ValueError:
572 # msg = resp
573 # raise ClientException('error: code: {}, resp: {}'.format(
574 # http_code, msg))
575 except ClientException as exc:
576 message = "failed to delete alarm: alarm {}\n{}".format(name, str(exc))
577 raise ClientException(message)
578
579 def get_alarm(self, project_name=None, ns_id=None, uuid=None):
580 self._client.get_token()
581 try:
582 self._apiName = "/nsfm"
583 self._apiResource = "/alarms"
584 self._apiBase = "{}{}{}".format(
585 self._apiName, self._apiVersion, self._apiResource
586 )
587 if uuid:
588 # if request is for any uuid
589 http_code, resp = self._http.get2_cmd(
590 "{}/{}".format(self._apiBase, uuid)
591 )
592 if not uuid:
593 http_code, resp = self._http.get2_cmd(
594 "{}/{}/{}/{}".format(self._apiBase, uuid, project_name, ns_id)
595 )
596 if http_code == 200:
597 if resp:
598 resp = json.loads(resp)
599 return resp
600 else:
601 raise ClientException("unexpected response from server")
602 else:
603 msg = resp or ""
604 raise ClientException(msg)
605 except ClientException as exc:
606 message = "failed to get alarm :\nerror:\n{}".format(str(exc))
607 raise ClientException(message)
608
609 def update_alarm(self, uuid, threshold=None, is_enable=None, wait=None):
610 self._client.get_token()
611 try:
612 op_data = {}
613 op_data["uuid"] = uuid
614 op_data["threshold"] = threshold
615 op_data["is_enable"] = is_enable
616 self._apiName = "/nsfm"
617 self._apiResource = "/alarms"
618 self._apiBase = "{}{}{}".format(
619 self._apiName, self._apiVersion, self._apiResource
620 )
621 http_code, resp = self._http.patch_cmd(
622 endpoint="{}".format(self._apiBase), postfields_dict=op_data
623 )
624 if resp:
625 resp = json.loads(resp)
626 print(resp)
627 return resp
628 except ClientException as exc:
629 message = "failed to update alarm :\nerror:\n{}".format(str(exc))
630 raise ClientException(message)
631
632 def export_metric(self, metric):
633 self._logger.debug("")
634 self._client.get_token()
635 data = {}
636 data["read_metric_data_request"] = metric
637 try:
638 http_code, resp = self._http.post_cmd(
639 endpoint="/test/message/metric_request", postfields_dict=data
640 )
641 # print('HTTP CODE: {}'.format(http_code))
642 # print('RESP: {}'.format(resp))
643 # if http_code in (200, 201, 202, 204):
644 # resp = json.loads(resp)
645 return "Metric exported"
646 # else:
647 # msg = ""
648 # if resp:
649 # try:
650 # msg = json.loads(resp)
651 # except ValueError:
652 # msg = resp
653 # raise ClientException('error: code: {}, resp: {}'.format(
654 # http_code, msg))
655 except ClientException as exc:
656 message = "failed to export metric: metric {}\n{}".format(metric, str(exc))
657 raise ClientException(message)
658
659 def get_field(self, ns_name, field):
660 self._logger.debug("")
661 nsr = self.get(ns_name)
662 print(yaml.safe_dump(nsr))
663 if nsr is None:
664 raise NotFound("failed to retrieve ns {}".format(ns_name))
665
666 if field in nsr:
667 return nsr[field]
668
669 raise NotFound("failed to find {} in ns {}".format(field, ns_name))
670
671 def heal(
672 self,
673 ns_name,
674 heal_dict,
675 wait=False,
676 timeout=None,
677 ):
678 """Heals a NS"""
679 self._logger.debug("")
680 self._client.get_token()
681 try:
682 op_data = heal_dict
683 if timeout:
684 op_data["timeout_ns_heal"] = timeout
685 op_id = self.exec_op(ns_name, op_name="heal", op_data=op_data, wait=wait)
686 print(str(op_id))
687 except ClientException as exc:
688 message = "failed to heal ns {}:\nerror:\n{}".format(ns_name, str(exc))
689 raise ClientException(message)