def test_edi_undeploy_model_by_id(self): with EDITestServer() as edi: with m_func('kubernetes.client.CoreV1Api.list_namespaced_service', 'demo_abc_model_1_0'), \ m_func('kubernetes.client.CoreV1Api.read_namespaced_service', ApiException(404)), \ m_func('kubernetes.client.ExtensionsV1beta1Api.read_namespaced_deployment', ['demo_abc_model_1_0', ApiException(404)]), \ m_func('kubernetes.client.apis.apps_v1beta1_api.AppsV1beta1Api.delete_namespaced_deployment', 'model_deleted'), \ m_func('kubernetes.client.CoreV1Api.delete_namespaced_service', 'undeploy_done'), \ mock.patch('legion.k8s.utils.build_client', return_value=None), \ mock.patch('legion.k8s.enclave.Enclave.graphite_service', return_value=None): deployments = edi.edi_client.undeploy('demo-abc-model') self.assertIsInstance(deployments, list) self.assertEqual(len(deployments), 1) # Check undeployed model fields expected_result = { 'image': '127.0.0.1/legion/test-bare-model-api-model-1:0.9.0-20181106123540.560.3b9739a', 'model': 'demo-abc-model', 'version': '1.0', 'scale': 1, 'ready_replicas': 1, 'status': 'ok', 'namespace': 'debug-enclave' } actual_result = { 'image': deployments[0].image, 'model': deployments[0].model, 'version': deployments[0].version, 'scale': deployments[0].scale, 'ready_replicas': deployments[0].ready_replicas, 'status': deployments[0].status, 'namespace': deployments[0].namespace } self.assertDictEqual(expected_result, actual_result)
def read_storage_class(self, name): if self.name == 'fail': raise ApiException('Get storage class fail') if self.name == '404' or name == '404': raise ApiException(reason='Not Found') my_response = namedtuple('my_response', 'metadata status') my_status = namedtuple( 'my_status', 'replicas available_replicas ready_replicas updated_replicas unavailable_replicas' ) if self.name == 'test1': return my_response(metadata={}, status=my_status(replicas=3, available_replicas=2, ready_replicas=1, updated_replicas=None, unavailable_replicas=1)) if self.name == 'test2' or name == 'test2': return my_response(metadata={}, status=my_status(replicas=1, available_replicas=1, ready_replicas=1, updated_replicas=1, unavailable_replicas=None)) return my_response(metadata={'key1': 'value1'}, status={'key1': 'value1'})
def test_atomic_creation_of_interactive_session(sample_serial_workflow_in_db): """Test the correct creation of all objects related to an interactive sesison as well as writing the state to DB, either all should be done or nothing..""" mocked_k8s_client = Mock() mocked_k8s_client.create_namespaced_deployment =\ Mock(side_effect=ApiException( reason='Error while creating deployment')) # Raise 404 when deleting Deployment, because it doesn't exist mocked_k8s_client.delete_namespaced_deployment =\ Mock(side_effect=ApiException( reason='Not Found')) with patch.multiple('reana_workflow_controller.k8s', current_k8s_extensions_v1beta1=mocked_k8s_client, current_k8s_corev1_api_client=DEFAULT) as mocks: try: kwrm = KubernetesWorkflowRunManager(sample_serial_workflow_in_db) if len(INTERACTIVE_SESSION_TYPES): kwrm.start_interactive_session(INTERACTIVE_SESSION_TYPES[0]) except REANAInteractiveSessionError: mocks['current_k8s_corev1_api_client']\ .delete_namespaced_service.assert_called_once() mocked_k8s_client.delete_namespaced_ingress.assert_called_once() mocked_k8s_client.delete_namespaced_deployment.assert_called_once() assert sample_serial_workflow_in_db.interactive_session is None
def read_namespaced_job(self, name, namespace): if self.name == 'fail': raise ApiException('Get daemonset fail') if self.name == '404': raise ApiException(reason='Not Found') my_response = namedtuple('my_response', 'metadata status') my_status = namedtuple('my_status', 'failed conditions') if self.name == 'test1': return my_response(metadata={}, status=my_status(failed='Failed', conditions=[])) if self.name == 'test2': my_conditions = namedtuple('my_conditions', 'type') return my_response(metadata={}, status=my_status( failed=None, conditions=[my_conditions(type='Failed')])) if self.name == 'test3': my_conditions = namedtuple('my_conditions', 'type') return my_response( metadata={}, status=my_status(failed=None, conditions=[my_conditions(type='Complete')])) return my_response(metadata={'key1': 'value1'}, status={'key1': 'value1'})
def read_namespaced_service(self, name, namespace, body=None): if self.name == 'fail': raise ApiException('Get service fail') if self.name == '404': raise ApiException(reason='Not Found') my_response = namedtuple('my_response', 'metadata status spec') my_status = namedtuple( 'my_status', 'replicas available_replicas ready_replicas updated_replicas unavailable_replicas' ) my_spec = namedtuple('my_spec', 'ports') my_port = namedtuple('my_port', 'port name') if self.name == 'test1': return my_response( metadata={}, spec=my_spec(ports=[my_port(port=123, name='test1')]), status=my_status(replicas=3, available_replicas=2, ready_replicas=1, updated_replicas=None, unavailable_replicas=1)) if self.name == 'test2': return my_response(metadata={}, spec=my_spec(ports=[]), status=my_status(replicas=1, available_replicas=1, ready_replicas=1, updated_replicas=1, unavailable_replicas=None)) return my_response(metadata={'key1': 'value1'}, status={'key1': 'value1'}, spec={})
def read_namespaced_deployment(self, name, namespace): if self.name == 'fail': raise ApiException('Get deployment fail') if self.name == '404' or name == '404': raise ApiException(reason='Not Found') my_response = namedtuple('my_response', 'metadata status spec') my_status = namedtuple( 'my_status', 'replicas available_replicas ready_replicas updated_replicas unavailable_replicas' ) my_spec = namedtuple('my_spec', 'replicas') if self.name == 'test1': return my_response(metadata={}, spec=my_spec(replicas=3), status=my_status(replicas=3, available_replicas=2, ready_replicas=1, updated_replicas=None, unavailable_replicas=1)) if self.name == 'test2' or name == 'test2': return my_response(metadata={}, spec=my_spec(replicas=1), status=my_status(replicas=1, available_replicas=1, ready_replicas=1, updated_replicas=1, unavailable_replicas=None)) return my_response(metadata={'key1': 'value1'}, status={'key1': 'value1'}, spec={'key1': 'value1'})
def read_namespaced_daemon_set(self, name, namespace): if self.name == 'fail': raise ApiException('Get daemonset fail') if self.name == '404': raise ApiException(reason='Not Found') my_response = namedtuple('my_response', 'metadata status') my_status = namedtuple( 'my_status', 'desired_number_scheduled number_available ' 'number_ready updated_number_scheduled number_unavailable') if self.name == 'test1': return my_response(metadata={}, status=my_status(desired_number_scheduled=2, number_available=2, number_ready=1, updated_number_scheduled=1, number_unavailable=1)) if self.name == 'test2': return my_response(metadata={}, status=my_status(desired_number_scheduled=2, number_available=2, number_ready=2, updated_number_scheduled=2, number_unavailable=None)) return my_response(metadata={'key1': 'value1'}, status={'key1': 'value1'})
def test_atomic_creation_of_interactive_session(sample_serial_workflow_in_db): """Test atomic creation of interactive sessions. All interactive session should be created as well as writing the state to DB, either all should be done or nothing. """ mocked_k8s_client = Mock() mocked_k8s_client.create_namespaced_deployment = Mock( side_effect=ApiException(reason="Error while creating deployment") ) # Raise 404 when deleting Deployment, because it doesn't exist mocked_k8s_client.delete_namespaced_deployment = Mock( side_effect=ApiException(reason="Not Found") ) with patch.multiple( "reana_workflow_controller.k8s", current_k8s_appsv1_api_client=mocked_k8s_client, current_k8s_networking_api_client=DEFAULT, current_k8s_corev1_api_client=DEFAULT, ) as mocks: try: kwrm = KubernetesWorkflowRunManager(sample_serial_workflow_in_db) if len(InteractiveSessionType): kwrm.start_interactive_session(InteractiveSessionType(0).name) except REANAInteractiveSessionError: mocks[ "current_k8s_corev1_api_client" ].delete_namespaced_service.assert_called_once() mocks[ "current_k8s_networking_api_client" ].delete_namespaced_ingress.assert_called_once() mocked_k8s_client.delete_namespaced_deployment.assert_called_once() assert not sample_serial_workflow_in_db.sessions.all()
def test_logging_records_failed_deletion_of_ui_components(self): # Arrange body = {'job_name': 'test-spark-job'} kubernetes_response = {'status': 'Success'} self.mock_k8s_adapter.delete_namespaced_custom_object.return_value = kubernetes_response self.mock_k8s_adapter.delete_options.return_value = {"api_version": "version", "other_values": "values"} self.mock_k8s_adapter.delete_namespaced_deployment.side_effect = ApiException('Failed to delete proxy') self.mock_k8s_adapter.delete_namespaced_service.side_effect = ApiException('Failed to delete service') self.mock_k8s_adapter.delete_namespaced_ingress.side_effect = ApiException('Failed to delete ingress') # Act response_body, response_code = yield self.send_request(body) # Assert assert response_code == 200 self.assertDictEqual(response_body, { 'status': 'success', 'data': { 'message': '"test-spark-job" deleted\nError deleting spark ui for job "test-spark-job", please ' 'contact an administrator', }}) self.mock_logger.error.assert_any_call('Trying to delete spark ui proxy resulted in exception: ' '(Failed to delete proxy)\nReason: None\n') self.mock_logger.error.assert_any_call('Trying to delete spark ui service resulted in exception: ' '(Failed to delete service)\nReason: None\n') self.mock_logger.error.assert_any_call('Trying to delete spark ui ingress resulted in exception: ' '(Failed to delete ingress)\nReason: None\n')
def main(): SERVICE_TOKEN_FILENAME = "/var/run/secrets/kubernetes.io/serviceaccount/token" SERVICE_CERT_FILENAME = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" KUBERNETES_HOST = "https://%s:%s" % (os.getenv("KUBERNETES_SERVICE_HOST"), os.getenv("KUBERNETES_SERVICE_PORT")) ## configure configuration = client.Configuration() configuration.host = KUBERNETES_HOST if not os.path.isfile(SERVICE_TOKEN_FILENAME): raise ApiException("Service token file does not exists.") with open(SERVICE_TOKEN_FILENAME) as f: token = f.read() if not token: raise ApiException("Token file exists but empty.") configuration.api_key['authorization'] = "bearer " + token.strip('\n') if not os.path.isfile(SERVICE_CERT_FILENAME): raise ApiException("Service certification file does not exists.") with open(SERVICE_CERT_FILENAME) as f: if not f.read(): raise ApiException("Cert file exists but empty.") configuration.ssl_ca_cert = SERVICE_CERT_FILENAME client.Configuration.set_default(configuration) try: ret = client.CoreV1Api().list_namespaced_config_map("rest-project") print ret except ApiException as e: print( "Exception when calling CoreV1Api->list_namespaced_config_map: %s\n" % e)
def patch(self, name: str, namespace: str, body, **kwargs): if namespace not in self._namespaced_items: raise ApiException(404, "Not Found") if not body: raise ValueError body.metadata = dict(body.metadata, **{"namespace": namespace}) body.metadata = V1ObjectMeta(**body.metadata) for resource in self._namespaced_items[namespace]: if resource.metadata.name == name: self._namespaced_items[namespace].remove(resource) self._items.remove(resource) break else: raise ApiException(404, "Not Found") if hasattr(body, "metadata"): resource.metadata = body.metadata if hasattr(body, "spec"): resource.spec = body.spec if hasattr(body, "status"): resource.status = body.status self._namespaced_items[namespace].add(resource) self._items.add(resource) return resource
def add_operator_to_cluster(self, operator_name, source=None, target_namespaces=[]): """ Install an operator in a list of targeted namespaces :param operator_name: (required | str) The name of the operator to be installed :param source: (optional | str) The source of the operator to be installed. This parameter can be in the form of a path to a source YAML or JSON, or it can also be passed as a dictionary. If not specified, the package is assumed to be already be visible throught the operator hub and so the source can be discovered. :param target_namespaces: (optional | list) A list of namespace/Projects where want the operator to be enabled in. If left unspecified, the operartor will be installed/enabled throughout the entire cluster. """ if source: cs_name, cs_namespace = self._source_processor(source) if not self.ohp_obj.watch_package_manifest_present(operator_name): err_msg = "A package manifest for {} could not be found".format(operator_name) logger.exception(err_msg) raise ApiException(err_msg) else: pkg_obj = self.ohp_obj.get_package_manifest(operator_name) if pkg_obj: cs_name = pkg_obj.status.catalogSource cs_namespace = pkg_obj.metadata.namespace else: logger.exception(err_msg) raise ApiException(err_msg) install_mode = self._derive_install_mode_from_target_namespaces(operator_name, target_namespaces) og_name, og_namespace = self._create_og(operator_name, install_mode, target_namespaces) subscription = self.sub_obj.create_subscription(operator_name, install_mode, og_namespace) assert subscription.spec.source == cs_name assert subscription.spec.sourceNamespace == cs_namespace return True
def test_terminate_nodes(): mock_client = mock.MagicMock() mock_client.core.delete_node.side_effect = [None, ApiException(404), None] m1, m2, m3 = mock.Mock(), mock.Mock(), mock.Mock() success, errors = terminate_nodes(client=mock_client, nodes=[m1, m2, m3]) expected_calls = [ mock.call.core.delete_node( node, body=V1DeleteOptions(), propagation_policy="foreground" ) for node in [m1, m2, m3] ] assert mock_client.mock_calls == expected_calls assert success == [m1, m3] assert errors[0][0] == m2 assert isinstance(errors[0][1], ApiException) mock_client.reset_mock() mock_client.core.delete_node.side_effect = [None, ApiException(404), None] success, errors = terminate_nodes(client=mock_client, nodes=[m1, m2, m3]) expected_calls = [ mock.call.core.delete_node( node, body=V1DeleteOptions(), propagation_policy="foreground" ) for node in [m1, m2, m3] ] assert mock_client.mock_calls == expected_calls assert success == [m1, m3] assert errors[0][0] == m2 assert isinstance(errors[0][1], ApiException)
def read_namespaced_stateful_set(self, name, namespace): if self.name == 'fail': raise ApiException('Get statefulset fail') if self.name == '404': raise ApiException(reason='Not Found') my_response = namedtuple('my_response', 'metadata status') my_status = namedtuple('my_status', 'current_replicas current_revision ready_replicas replicas update_revision') if self.name == 'test1': return my_response(metadata={}, status=my_status(current_replicas=2, current_revision='revision-123', ready_replicas=1, replicas=3, update_revision='revision-321')) if self.name == 'test2': return my_response(metadata={}, status=my_status(current_replicas=3, current_revision='revision-123', ready_replicas=3, replicas=3, update_revision='revision-123')) return my_response(metadata={'key1': 'value1'}, status={'key1': 'value1'})
def test_edi_undeploy_all_models_by_version(self): with EDITestServer() as edi: with m_func('kubernetes.client.CoreV1Api.list_namespaced_service', 'demo_abc_models_1_0_and_1_1'), \ m_func('kubernetes.client.CoreV1Api.read_namespaced_service', [ApiException(404), ApiException(404)]), \ m_func('kubernetes.client.ExtensionsV1beta1Api.read_namespaced_deployment', ['demo_abc_model_1_0', ApiException(404), 'demo_abc_model_1_1', ApiException(404)]), \ m_func('kubernetes.client.apis.apps_v1beta1_api.AppsV1beta1Api.delete_namespaced_deployment', 'last_model_deleted'), \ m_func('kubernetes.client.CoreV1Api.delete_namespaced_service', 'undeploy_done'), \ mock.patch('legion.k8s.utils.is_code_run_in_cluster', return_value=None), \ mock.patch('legion.k8s.utils.build_client', return_value=None), \ mock.patch('legion.k8s.enclave.Enclave.graphite_service', return_value=None): deployments = edi.edi_client.undeploy(model='demo-abc-model', version='*') # Test count of returned deployments self.assertIsInstance(deployments, list) self.assertEqual(len(deployments), 2) # Validate deleted models # Test model #1 fields expected_result = { 'image': '127.0.0.1/legion/test-bare-model-api-model-1:0.9.0-20181106123540.560.3b9739a', 'model': 'demo-abc-model', 'version': '1.0', 'scale': 1, 'ready_replicas': 1, 'status': 'ok', 'namespace': 'debug-enclave' } actual_result = { 'image': deployments[0].image, 'model': deployments[0].model, 'version': deployments[0].version, 'scale': deployments[0].scale, 'ready_replicas': deployments[0].ready_replicas, 'status': deployments[0].status, 'namespace': deployments[0].namespace } self.assertDictEqual(expected_result, actual_result) # Test model #2 fields expected_result = { 'image': '127.0.0.1/legion/test-bare-model-api-model-2:0.9.0-20181106123540.560.3b9739a', 'model': 'demo-abc-model', 'version': '1.1', 'scale': 1, 'ready_replicas': 1, 'status': 'ok', 'namespace': 'debug-enclave' } actual_result = { 'image': deployments[1].image, 'model': deployments[1].model, 'version': deployments[1].version, 'scale': deployments[1].scale, 'ready_replicas': deployments[1].ready_replicas, 'status': deployments[1].status, 'namespace': deployments[1].namespace } self.assertDictEqual(expected_result, actual_result)
def read(self, name: str, namespace: str, **kwargs): # noqa if namespace not in self._namespaced_items: raise ApiException(404, "Not Found") for resource in self._namespaced_items[namespace]: if resource.metadata.name == name: return resource else: raise ApiException(404, "Not Found")
def test_pod_scheduled(init_openshift_deployer, pod_not_deployed): od = init_openshift_deployer flexmock(od).should_receive("get_pod").and_raise( ApiException(status=404, )).and_return( V1Pod(status=V1PodStatus(phase="Running"))).and_return( V1Pod(status=V1PodStatus(phase="Succeeded"))) flexmock(od.api).should_receive("create_namespaced_pod").and_raise( ApiException(status="403")).and_return(1) od.deploy_pod(["false"])
def delete(self, name: str, namespace: str, **kwargs): if namespace not in self._namespaced_items.keys(): raise ApiException(404, "Not Found") for service in self._items: if service.metadata.name == name: self._items.remove(service) self._namespaced_items[namespace].remove(service) break else: raise ApiException(404, "Not Found")
def test_start_pod_retries_three_times(self, mock_run_pod_async): mock_run_pod_async.side_effect = [ ApiException(status=409), ApiException(status=409), ApiException(status=409), ApiException(status=409), ] with pytest.raises(ApiException): self.pod_launcher.start_pod(mock.sentinel) assert mock_run_pod_async.call_count == 3
def delete_storage_class(self, name, body): if self.name == 'fail': raise ApiException('Delete storage class fail') if self.name == '404' or name == '404': raise ApiException(reason='Not Found') if self.name == 'test1' or name == 'test1': my_response = namedtuple('my_response', 'message') return my_response(message='Failed') if self.name == 'test2' or name == 'test2': my_response = namedtuple('my_response', 'message') return my_response(message=None) return {'key1': 'value1'}
def create(self, namespace: str, body, **kwargs): if not body: raise ValueError body.metadata = dict(body.metadata, **{"namespace": namespace}) body.metadata = V1ObjectMeta(**body.metadata) if namespace not in self._namespaced_items: raise ApiException(404, "Not Found") if body in self._namespaced_items[namespace]: raise ApiException(409, "AlreadyExists") self._items.add(body) self._namespaced_items[namespace].add(body) return body
def delete_namespaced_deployment(self, name, body, namespace): if self.name == 'fail': raise ApiException('Delete deployment fail') if self.name == '404' or name == '404': raise ApiException(reason='Not Found') if self.name == 'test1' or name == 'test1': my_response = namedtuple('my_response', 'message') return my_response(message='Failed') if self.name == 'test2' or name == 'test2': my_response = namedtuple('my_response', 'message') return my_response(message=None) return {'key1': 'value1'}
def test_is_pod_already_deployed(init_openshift_deployer): od = init_openshift_deployer flexmock(od).should_receive("get_pod").and_raise( ApiException(status=200, reason="POD already exists") ) with pytest.raises(SandcastleExecutionError): od.is_pod_already_deployed()
def websocket_call(configuration, *args, **kwargs): """An internal function to be called in api-client when a websocket connection is required. args and kwargs are the parameters of apiClient.request method.""" url = args[1] _request_timeout = kwargs.get("_request_timeout", 60) _preload_content = kwargs.get("_preload_content", True) capture_all = kwargs.get("capture_all", True) headers = kwargs.get("headers") # Expand command parameter list to indivitual command params query_params = [] for key, value in kwargs.get("query_params", {}): if key == 'command' and isinstance(value, list): for command in value: query_params.append((key, command)) else: query_params.append((key, value)) if query_params: url += '?' + urlencode(query_params) try: client = WSClient(configuration, get_websocket_url(url), headers, capture_all) if not _preload_content: return client client.run_forever(timeout=_request_timeout) return WSResponse('%s' % ''.join(client.read_all())) except (Exception, KeyboardInterrupt, SystemExit) as e: raise ApiException(status=0, reason=str(e))
def create_namespaced_ingress(namespace, body): name = '{}-{}'.format(namespace, body.metadata['name']) if name in MockExtensionsV1beta1Api.ingress_map.keys(): raise ApiException(status=409) else: MockExtensionsV1beta1Api.ingress_map[name] = body return body
def create_namespaced_service(body, namespace): name = '{}-{}'.format(body.metadata.name, namespace) if name in MockCoreV1Api.service_map.keys(): raise ApiException(status=409) # mock conflict else: MockCoreV1Api.service_map[name] = body return body
def read_namespaced_persistent_volume_claim_status(name, **_): if name == 'nonexistent-pvc': raise ApiException(status=404, reason="nonexistent-pvc") elif name == 'pending-pvc': return DotDict({'status': DotDict({'phase': 'Pending'})}) else: return DotDict({'status': DotDict({'phase': 'Bound'})})
def delete_deployment(source): ''' Deletes the kubernetes deployment defined by name and namespace ''' src_obj = __read_and_render_yaml_file(source) metadata = src_obj['metadata'] name = metadata['name'] namespace = metadata['namespace'] body = kubernetes.client.V1DeleteOptions(api_version="v1", grace_period_seconds=50, propagation_policy="Background") try: api_instance = kubernetes.client.AppsV1Api( kubernetes.client.ApiClient(configuration)) api_response = api_instance.delete_namespaced_deployment( pretty=pretty, name=name, namespace=namespace, body=body) mutable_api_response = api_response.to_dict() if mutable_api_response['code'] != 200: logging.warning( 'Reached polling time limit. Deployment is not yet ' 'deleted, but we are backing off. Sorry, but you\'ll ' 'have to check manually.') return mutable_api_response except (ApiException, HTTPError) as exc: if isinstance(exc, ApiException) and exc.status == 404: return None else: logging.exception( 'Exception when calling ' 'ExtensionsV1beta1Api->delete_namespaced_deployment: ' '{0}'.format(exc)) raise ApiException(exc)
def test_workflow_finish_and_kubernetes_not_available( in_memory_queue_connection, sample_serial_workflow_in_db, consume_queue, ): """Test workflow finish with a Kubernetes connection troubles.""" sample_serial_workflow_in_db.status = RunStatus.running next_status = RunStatus.failed job_status_consumer = JobStatusConsumer( connection=in_memory_queue_connection) workflow_status_publisher = WorkflowStatusPublisher( connection=in_memory_queue_connection, queue=job_status_consumer.queue) workflow_status_publisher.publish_workflow_status( str(sample_serial_workflow_in_db.id_), next_status.value, ) k8s_corev1_api_client_mock = Mock() k8s_corev1_api_client_mock.delete_namespaced_job = Mock( side_effect=ApiException(reason="Could not delete job.", status=404)) with patch( "reana_workflow_controller.consumer.current_k8s_corev1_api_client", k8s_corev1_api_client_mock, ): consume_queue(job_status_consumer, limit=1) assert sample_serial_workflow_in_db.status == next_status
def portforward_call(configuration, _method, url, **kwargs): """An internal function to be called in api-client when a websocket connection is required for port forwarding. args and kwargs are the parameters of apiClient.request method.""" query_params = kwargs.get("query_params") ports = [] for param, value in query_params: if param == 'ports': for port in value.split(','): try: port_number = int(port) except ValueError: raise ApiValueError("Invalid port number: %s" % port) if not (0 < port_number < 65536): raise ApiValueError("Port number must be between 0 and 65536: %s" % port) if port_number in ports: raise ApiValueError("Duplicate port numbers: %s" % port) ports.append(port_number) if not ports: raise ApiValueError("Missing required parameter `ports`") url = get_websocket_url(url, query_params) headers = kwargs.get("headers") try: websocket = create_websocket(configuration, url, headers) return PortForward(websocket, ports) except (Exception, KeyboardInterrupt, SystemExit) as e: raise ApiException(status=0, reason=str(e))