1 # Copyright 2018 Telefonica
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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
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
31 def __init__(self
, http
=None, client
=None):
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
43 def _wait(self
, op_id
, wait_time
, delete_flag
=False):
44 self
._logger
.debug("")
45 # Endpoint to get operation status
46 apiUrlStatus
= "{}{}{}".format(
47 self
._apiName
, self
._apiVersion
, "/ns_lcm_op_occs"
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(
58 deleteFlag
=delete_flag
,
61 def list(self
, filter=None):
62 """Returns a list of NS"""
63 self
._logger
.debug("")
64 self
._client
.get_token()
67 filter_string
= "?{}".format(filter)
68 _
, resp
= self
._http
.get2_cmd("{}{}".format(self
._apiBase
, filter_string
))
70 return json
.loads(resp
)
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():
82 for ns
in self
.list():
83 if name
== ns
["name"]:
85 raise NotFound("ns '{}' not found".format(name
))
87 def get_individual(self
, name
):
88 self
._logger
.debug("")
89 self
._client
.get_token()
91 if not utils
.validate_uuid4(name
):
92 for ns
in self
.list():
93 if name
== ns
["name"]:
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))
101 return json
.loads(resp
)
103 raise NotFound("ns '{}' not found".format(name
))
104 raise NotFound("ns '{}' not found".format(name
))
106 def delete(self
, name
, force
=False, config
=None, wait
=False):
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 termination 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
119 self
._logger
.debug("")
121 querystring_list
= []
124 ns_config
= yaml
.safe_load(config
)
125 querystring_list
+= ["{}={}".format(k
, v
) for k
, v
in ns_config
.items()]
127 querystring_list
.append("FORCE=True")
129 querystring
= "?" + "&".join(querystring_list
)
130 http_code
, resp
= self
._http
.delete_cmd(
131 "{}/{}{}".format(self
._apiBase
, ns
["_id"], querystring
)
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))
140 resp
= json
.loads(resp
)
141 # For the 'delete' operation, '_id' is used
142 self
._wait
(resp
.get("_id"), wait
, delete_flag
=True)
144 print("Deletion in progress")
145 elif http_code
== 204:
149 raise ClientException("failed to delete ns {} - {}".format(name
, msg
))
151 def _get_vim_account_id(self
, vim_account
: str, vim_account_dict
: dict) -> str:
152 """Get VIM account ID.
154 vim_account (str): VIM account id as string
155 vim_account_dict (dict): A dictionary which includes vim account id
158 vim_id (str): VIM account id as string
163 self
._logger
.debug("")
164 if vim_account_dict
.get(vim_account
):
165 return vim_account_dict
[vim_account
]
166 vim
= self
._client
.vim
.get(vim_account
)
168 raise NotFound("cannot find vim account '{}'".format(vim_account
))
169 vim_account_dict
[vim_account
] = vim
["_id"]
172 def _get_wim_account_id(self
, wim_account
: str, wim_account_dict
: dict) -> str:
173 """Get WIM account ID.
175 wim_account (str): WIM account id as string
176 wim_account_dict (dict): A dictionary which includes wim account id
179 wim_id (str): WIM account id as string
184 self
._logger
.debug("")
185 # wim_account can be False (boolean) to indicate not use wim account
186 if not isinstance(wim_account
, str):
188 if wim_account_dict
.get(wim_account
):
189 return wim_account_dict
[wim_account
]
190 wim
= self
._client
.wim
.get(wim_account
)
192 raise NotFound("cannot find wim account '{}'".format(wim_account
))
193 wim_account_dict
[wim_account
] = wim
["_id"]
196 def _get_paas_account_id(self
, paas_account
: str) -> str:
197 """Get PaaS account ID.
199 paas_account (str): PaaS account id as string
202 paas_id (str): PaaS account id as string
207 self
._logger
.debug("")
208 paas
= self
._client
.paas
.get(paas_account
)
210 raise NotFound("cannot find PaaS account '{}'".format(paas_account
))
213 def _update_vnf_in_ns_config(self
, ns_config
: dict, vim_account_dict
: dict) -> dict:
214 """Update vnf field in ns_config.
216 ns_config (dict): NS config dictionary which includes additional params
217 vim_account_dict (dict): A dictionary which includes vim account id
220 ns (dict): NS dictionary
222 if "vnf" in ns_config
:
223 for vnf
in ns_config
["vnf"]:
224 if vnf
.get("vim_account"):
225 vnf
["vimAccountId"] = self
._get
_vim
_account
_id
(
226 vnf
.pop("vim_account"), vim_account_dict
230 def _update_wim_account_in_ns(
231 self
, ns_config
: dict, wim_account_dict
: dict, ns
: dict
233 """Update WIM_account in NS dictionary.
235 ns_config (dict): NS config dictionary which includes additional params
236 wim_account_dict (dict): A dictionary which includes wim account id
237 ns (dict): NS dictionary which includes ns_id, ns_name, description etc.
240 ns (dict): NS dictionary
242 if "wim_account" in ns_config
:
243 wim_account
= ns_config
.pop("wim_account")
244 if wim_account
is not None:
245 ns
["wimAccountId"] = self
._get
_wim
_account
_id
(
246 wim_account
, wim_account_dict
250 def _update_vld_in_ns_config(
251 self
, ns_config
: dict, vim_account_dict
: dict, wim_account_dict
: dict
253 """Validating the additionalParamsForNs and additionalParamsForVnf in ns_config.
256 ns_config (dict): NS config dictionary which includes additional params
257 vim_account_dict (dict): A dictionary which includes vim account id
258 wim_account_dict (dict): A dictionary which includes wim account id
261 ns_config (dict): NS config dictionary which includes additional params
266 if "vld" in ns_config
:
267 if not isinstance(ns_config
["vld"], list):
268 raise ClientException(
269 "Error at --config 'vld' must be a list of dictionaries"
271 for vld
in ns_config
["vld"]:
272 if not isinstance(vld
, dict):
273 raise ClientException(
274 "Error at --config 'vld' must be a list of dictionaries"
276 if vld
.get("vim-network-name"):
277 if isinstance(vld
["vim-network-name"], dict):
278 vim_network_name_dict
= {}
279 for vim_account
, vim_net
in vld
["vim-network-name"].items():
280 vim_network_name_dict
[
281 self
._get
_vim
_account
_id
(vim_account
, vim_account_dict
)
283 vld
["vim-network-name"] = vim_network_name_dict
284 if "wim_account" in vld
and vld
["wim_account"] is not None:
285 vld
["wimAccountId"] = self
._get
_wim
_account
_id
(
286 vld
.pop("wim_account"), wim_account_dict
290 def _validate_additional_params_in_ns_config(self
, ns_config
: dict) -> None:
291 """Validating the additionalParamsForNs and additionalParamsForVnf in ns_config.
293 ns_config (dict): NS config dictionary which includes additional params
298 if "additionalParamsForNs" in ns_config
:
299 if not isinstance(ns_config
["additionalParamsForNs"], dict):
300 raise ClientException(
301 "Error at --config 'additionalParamsForNs' must be a dictionary"
303 if "additionalParamsForVnf" in ns_config
:
304 if not isinstance(ns_config
["additionalParamsForVnf"], list):
305 raise ClientException(
306 "Error at --config 'additionalParamsForVnf' must be a list"
308 for additional_param_vnf
in ns_config
["additionalParamsForVnf"]:
309 if not isinstance(additional_param_vnf
, dict):
310 raise ClientException(
311 "Error at --config 'additionalParamsForVnf' items must be dictionaries"
313 if not additional_param_vnf
.get("member-vnf-index"):
314 raise ClientException(
315 "Error at --config 'additionalParamsForVnf' items must contain "
319 def process_ns_create_with_vim_account(
326 ssh_keys
: str = None,
329 """Process NS create request which includes VIM Account.
331 vim_account (str): VIM Account id as string
332 nsd (dict): A dictionary which includes network service description
333 nsr_name (str): Network service record name
334 description (str): Service description
335 config (dict): Placeholder for additional configuration
336 ssh_keys (str): ssh-key file
337 timeout (int): Max time to wait (seconds)
340 ns (dict): Payload for ns create request
345 vim_account_dict
= {}
346 wim_account_dict
= {}
347 vim_id
= self
._get
_vim
_account
_id
(vim_account
, vim_account_dict
)
351 "nsDescription": description
,
352 "vimAccountId": vim_id
,
355 if ssh_keys
is not None:
357 for pubkeyfile
in ssh_keys
.split(","):
358 with
open(pubkeyfile
, "r") as f
:
359 ns
["ssh_keys"].append(f
.read())
362 ns
["timeout_ns_deploy"] = timeout
365 ns_config
= yaml
.safe_load(config
)
366 if "vim-network-name" in ns_config
:
367 ns_config
["vld"] = ns_config
.pop("vim-network-name")
369 ns_config
= self
._update
_vld
_in
_ns
_config
(
370 ns_config
, vim_account_dict
, wim_account_dict
372 ns_config
= self
._update
_vnf
_in
_ns
_config
(ns_config
, vim_account_dict
)
373 self
._validate
_additional
_params
_in
_ns
_config
(ns_config
)
374 ns
= self
._update
_wim
_account
_in
_ns
(ns_config
, vim_account_dict
, ns
)
379 def process_ns_create_with_paas_account(
388 """Process NS create request which includes PaaS Account.
390 paas_account (str): PaaS Account id as string
391 nsd (dict): A dictionary which includes network service description
392 nsr_name (str): Network service record name
393 description (str): Service description
394 config (dict): Placeholder for additional configuration
395 timeout (int): Max time to wait (seconds)
398 ns (dict): Payload for ns create request
403 paas_id
= self
._get
_paas
_account
_id
(paas_account
)
407 "nsDescription": description
,
408 "paasAccountId": paas_id
,
412 ns
["timeout_ns_deploy"] = timeout
415 ns_config
= yaml
.safe_load(config
)
416 self
._validate
_additional
_params
_in
_ns
_config
(ns_config
)
425 vim_account
: str = None,
426 paas_account
: str = None,
428 ssh_keys
: str = None,
429 description
: str = "default description",
430 admin_status
: str = "ENABLED",
434 """NS create request which includes PaaS Account or VIM account.
436 nsd_name (dict): A dictionary which includes network service description
437 nsr_name (str): Network service record name
438 vim_account (str): VIM account ID as string
439 paas_account (str): PaaS Account id as string
440 config (dict): Placeholder for additional configuration
441 ssh_keys (str): ssh-key file
442 description (str): Service description
443 admin_status (str): Administration Status
444 wait (Boolean): True or False
445 timeout (int): Max time to wait (seconds)
448 response id (str): Response ID
453 self
._logger
.debug("")
455 if not (vim_account
or paas_account
):
456 raise ClientException(
457 "Both of vim_account and paas_account options are empty."
460 if vim_account
and paas_account
:
461 raise ClientException(
462 "Both of vim_account and paas_account options are set."
465 self
._client
.get_token()
466 nsd
= self
._client
.nsd
.get(nsd_name
)
469 # VIM account is provided as input parameter.
470 ns
= self
.process_ns_create_with_vim_account(
481 # PaaS account is provided as input parameter.
482 ns
= self
.process_ns_create_with_paas_account(
483 paas_account
, nsd
, nsr_name
, description
, config
=config
, timeout
=timeout
487 self
._apiResource
= "/ns_instances_content"
488 self
._apiBase
= "{}{}{}".format(
489 self
._apiName
, self
._apiVersion
, self
._apiResource
491 headers
= self
._client
._headers
492 headers
["Content-Type"] = "application/yaml"
494 "{}: {}".format(key
, val
) for (key
, val
) in list(headers
.items())
496 self
._http
.set_http_header(http_header
)
497 http_code
, resp
= self
._http
.post_cmd(
498 endpoint
=self
._apiBase
, postfields_dict
=ns
501 if not resp
or "id" not in resp
:
502 raise ClientException(
503 "unexpected response from server - {} ".format(resp
)
506 resp
= json
.loads(resp
)
507 print(str(resp
["id"]))
509 # Wait for status for NS instance creation
510 self
._wait
(resp
.get("nslcmop_id"), wait
)
514 except ClientException
as exc
:
515 message
= "failed to create ns: {} nsd: {}\nerror:\n{}".format(
516 nsr_name
, nsd_name
, str(exc
)
518 raise ClientException(message
)
520 def list_op(self
, name
, filter=None):
521 """Returns the list of operations of a NS"""
522 self
._logger
.debug("")
525 self
._apiResource
= "/ns_lcm_op_occs"
526 self
._apiBase
= "{}{}{}".format(
527 self
._apiName
, self
._apiVersion
, self
._apiResource
531 filter_string
= "&{}".format(filter)
532 http_code
, resp
= self
._http
.get2_cmd(
533 "{}?nsInstanceId={}{}".format(self
._apiBase
, ns
["_id"], filter_string
)
535 # print('HTTP CODE: {}'.format(http_code))
536 # print('RESP: {}'.format(resp))
539 resp
= json
.loads(resp
)
542 raise ClientException("unexpected response from server")
547 # resp = json.loads(resp)
548 # msg = resp['detail']
551 raise ClientException(msg
)
552 except ClientException
as exc
:
553 message
= "failed to get operation list of NS {}:\nerror:\n{}".format(
556 raise ClientException(message
)
558 def get_op(self
, operationId
):
559 """Returns the status of an operation"""
560 self
._logger
.debug("")
561 self
._client
.get_token()
563 self
._apiResource
= "/ns_lcm_op_occs"
564 self
._apiBase
= "{}{}{}".format(
565 self
._apiName
, self
._apiVersion
, self
._apiResource
567 http_code
, resp
= self
._http
.get2_cmd(
568 "{}/{}".format(self
._apiBase
, operationId
)
570 # print('HTTP CODE: {}'.format(http_code))
571 # print('RESP: {}'.format(resp))
574 resp
= json
.loads(resp
)
577 raise ClientException("unexpected response from server")
582 # resp = json.loads(resp)
583 # msg = resp['detail']
586 raise ClientException(msg
)
587 except ClientException
as exc
:
588 message
= "failed to get status of operation {}:\nerror:\n{}".format(
589 operationId
, str(exc
)
591 raise ClientException(message
)
600 """Executes an operation on a NS"""
601 self
._logger
.debug("")
605 self
._apiResource
= "/ns_instances"
606 self
._apiBase
= "{}{}{}".format(
607 self
._apiName
, self
._apiVersion
, self
._apiResource
609 endpoint
= "{}/{}/{}".format(self
._apiBase
, ns
["_id"], op_name
)
610 # print('OP_NAME: {}'.format(op_name))
611 # print('OP_DATA: {}'.format(json.dumps(op_data)))
612 http_code
, resp
= self
._http
.post_cmd(
613 endpoint
=endpoint
, postfields_dict
=op_data
615 # print('HTTP CODE: {}'.format(http_code))
616 # print('RESP: {}'.format(resp))
617 # if http_code in (200, 201, 202, 204):
619 resp
= json
.loads(resp
)
620 if not resp
or "id" not in resp
:
621 raise ClientException(
622 "unexpected response from server - {}".format(resp
)
625 # Wait for status for NS instance action
626 # For the 'action' operation, 'id' is used
627 self
._wait
(resp
.get("id"), wait
)
633 # msg = json.loads(resp)
636 # raise ClientException(msg)
637 except ClientException
as exc
:
638 message
= "failed to exec operation {}:\nerror:\n{}".format(name
, str(exc
))
639 raise ClientException(message
)
651 """Scales a VNF by adding/removing VDUs"""
652 self
._logger
.debug("")
653 self
._client
.get_token()
656 op_data
["scaleType"] = "SCALE_VNF"
657 op_data
["scaleVnfData"] = {}
658 if scale_in
and not scale_out
:
659 op_data
["scaleVnfData"]["scaleVnfType"] = "SCALE_IN"
660 elif not scale_in
and scale_out
:
661 op_data
["scaleVnfData"]["scaleVnfType"] = "SCALE_OUT"
663 raise ClientException("you must set either 'scale_in' or 'scale_out'")
664 op_data
["scaleVnfData"]["scaleByStepData"] = {
665 "member-vnf-index": vnf_name
,
666 "scaling-group-descriptor": scaling_group
,
669 op_data
["timeout_ns_scale"] = timeout
670 op_id
= self
.exec_op(ns_name
, op_name
="scale", op_data
=op_data
, wait
=wait
)
672 except ClientException
as exc
:
673 message
= "failed to scale vnf {} of ns {}:\nerror:\n{}".format(
674 vnf_name
, ns_name
, str(exc
)
676 raise ClientException(message
)
678 def update(self
, ns_name
, data
, wait
=False):
679 """Update NS instance.
681 This function calls the NBI in order to perform an update operation
682 on a Network Service instance.
693 self
._logger
.debug("")
694 self
._client
.get_token()
696 op_data
= {"updateType": data
.pop("updateType")}
698 # Check update parameters availability according to update type
699 if op_data
["updateType"] == "CHANGE_VNFPKG":
701 data
["config"]["changeVnfPackageData"][0].get("vnfInstanceId")
702 and data
["config"]["changeVnfPackageData"][0].get("vnfdId")
704 raise ClientException("you must set both vnfInstanceId and vnfdId")
707 op_data
["changeVnfPackageData"] = {}
708 op_data
["changeVnfPackageData"]["vnfInstanceId"] = data
["config"][
709 "changeVnfPackageData"
710 ][0].get("vnfInstanceId")
712 op_data
["changeVnfPackageData"]["vnfdId"] = data
["config"][
713 "changeVnfPackageData"
716 if data
.get("timeout"):
717 op_data
["timeout_ns_update"] = data
["timeout"]
719 op_id
= self
.exec_op(ns_name
, op_name
="update", op_data
=op_data
, wait
=wait
)
722 except ClientException
as exc
:
723 message
= "failed to update ns {}:\nerror:\n{}".format(ns_name
, str(exc
))
724 raise ClientException(message
)
726 def create_alarm(self
, alarm
):
727 self
._logger
.debug("")
728 self
._client
.get_token()
730 data
["create_alarm_request"] = {}
731 data
["create_alarm_request"]["alarm_create_request"] = alarm
733 http_code
, resp
= self
._http
.post_cmd(
734 endpoint
="/test/message/alarm_request", postfields_dict
=data
736 # print('HTTP CODE: {}'.format(http_code))
737 # print('RESP: {}'.format(resp))
738 # if http_code in (200, 201, 202, 204):
739 # resp = json.loads(resp)
740 print("Alarm created")
745 # msg = json.loads(resp)
748 # raise ClientException('error: code: {}, resp: {}'.format(
750 except ClientException
as exc
:
751 message
= "failed to create alarm: alarm {}\n{}".format(alarm
, str(exc
))
752 raise ClientException(message
)
754 def delete_alarm(self
, name
):
755 self
._logger
.debug("")
756 self
._client
.get_token()
758 data
["delete_alarm_request"] = {}
759 data
["delete_alarm_request"]["alarm_delete_request"] = {}
760 data
["delete_alarm_request"]["alarm_delete_request"]["alarm_uuid"] = name
762 http_code
, resp
= self
._http
.post_cmd(
763 endpoint
="/test/message/alarm_request", postfields_dict
=data
765 # print('HTTP CODE: {}'.format(http_code))
766 # print('RESP: {}'.format(resp))
767 # if http_code in (200, 201, 202, 204):
768 # resp = json.loads(resp)
769 print("Alarm deleted")
774 # msg = json.loads(resp)
777 # raise ClientException('error: code: {}, resp: {}'.format(
779 except ClientException
as exc
:
780 message
= "failed to delete alarm: alarm {}\n{}".format(name
, str(exc
))
781 raise ClientException(message
)
783 def get_alarm(self
, project_name
=None, ns_id
=None, uuid
=None):
784 self
._client
.get_token()
786 self
._apiName
= "/nsfm"
787 self
._apiResource
= "/alarms"
788 self
._apiBase
= "{}{}{}".format(
789 self
._apiName
, self
._apiVersion
, self
._apiResource
792 # if request is for any uuid
793 http_code
, resp
= self
._http
.get2_cmd(
794 "{}/{}".format(self
._apiBase
, uuid
)
797 http_code
, resp
= self
._http
.get2_cmd(
798 "{}/{}/{}/{}".format(self
._apiBase
, uuid
, project_name
, ns_id
)
802 resp
= json
.loads(resp
)
805 raise ClientException("unexpected response from server")
808 raise ClientException(msg
)
809 except ClientException
as exc
:
810 message
= "failed to get alarm :\nerror:\n{}".format(str(exc
))
811 raise ClientException(message
)
813 def update_alarm(self
, uuid
, threshold
=None, is_enable
=None, wait
=None):
814 self
._client
.get_token()
817 op_data
["uuid"] = uuid
818 op_data
["threshold"] = threshold
819 op_data
["is_enable"] = is_enable
820 self
._apiName
= "/nsfm"
821 self
._apiResource
= "/alarms"
822 self
._apiBase
= "{}{}{}".format(
823 self
._apiName
, self
._apiVersion
, self
._apiResource
825 http_code
, resp
= self
._http
.patch_cmd(
826 endpoint
="{}".format(self
._apiBase
), postfields_dict
=op_data
829 resp
= json
.loads(resp
)
832 except ClientException
as exc
:
833 message
= "failed to update alarm :\nerror:\n{}".format(str(exc
))
834 raise ClientException(message
)
836 def export_metric(self
, metric
):
837 self
._logger
.debug("")
838 self
._client
.get_token()
840 data
["read_metric_data_request"] = metric
842 http_code
, resp
= self
._http
.post_cmd(
843 endpoint
="/test/message/metric_request", postfields_dict
=data
845 # print('HTTP CODE: {}'.format(http_code))
846 # print('RESP: {}'.format(resp))
847 # if http_code in (200, 201, 202, 204):
848 # resp = json.loads(resp)
849 return "Metric exported"
854 # msg = json.loads(resp)
857 # raise ClientException('error: code: {}, resp: {}'.format(
859 except ClientException
as exc
:
860 message
= "failed to export metric: metric {}\n{}".format(metric
, str(exc
))
861 raise ClientException(message
)
863 def get_field(self
, ns_name
, field
):
864 self
._logger
.debug("")
865 nsr
= self
.get(ns_name
)
866 print(yaml
.safe_dump(nsr
))
868 raise NotFound("failed to retrieve ns {}".format(ns_name
))
873 raise NotFound("failed to find {} in ns {}".format(field
, ns_name
))
883 self
._logger
.debug("")
884 self
._client
.get_token()
888 op_data
["timeout_ns_heal"] = timeout
889 op_id
= self
.exec_op(ns_name
, op_name
="heal", op_data
=op_data
, wait
=wait
)
891 except ClientException
as exc
:
892 message
= "failed to heal ns {}:\nerror:\n{}".format(ns_name
, str(exc
))
893 raise ClientException(message
)