def version_check(): """ Used to verify that h2o-python module and the H2O server are compatible with each other. """ if os.environ.get("H2O_DISABLE_STRICT_VERSION_CHECK"): return ci = h2oconn.info() if not ci: raise H2OConnectionError( "Connection not initialized. Did you run h2o.connect()?") ver_h2o = ci.version from .__init__ import __version__ as ver_pkg if ver_pkg == "SUBST_PROJECT_VERSION": ver_pkg = "UNKNOWN" if str(ver_h2o) != str(ver_pkg): branch_name_h2o = ci.branch_name build_number_h2o = ci.build_number if build_number_h2o is None or build_number_h2o == "unknown": raise H2OConnectionError( "Version mismatch. H2O is version {0}, but the h2o-python package is version {1}. " "Upgrade H2O and h2o-Python to latest stable version - " "http://h2o-release.s3.amazonaws.com/h2o/latest_stable.html" "".format(ver_h2o, ver_pkg)) elif build_number_h2o == "99999": raise H2OConnectionError( "Version mismatch. H2O is version {0}, but the h2o-python package is version {1}. " "This is a developer build, please contact your developer." "".format(ver_h2o, ver_pkg)) else: raise H2OConnectionError( "Version mismatch. H2O is version {0}, but the h2o-python package is version {1}. " "Install the matching h2o-Python version from - " "http://h2o-release.s3.amazonaws.com/h2o/{2}/{3}/index.html." "".format(ver_h2o, ver_pkg, branch_name_h2o, build_number_h2o))
def _test_connection(self, max_retries=5, messages=None): """ Test that the H2O cluster can be reached, and retrieve basic cluster status info. :param max_retries: Number of times to try to connect to the cluster (with 0.2s intervals). :returns: Cluster information (an H2OCluster object) :raises H2OConnectionError, H2OServerError: """ if messages is None: messages = ("Connecting to H2O server at {url} ..", "successful.", "failed.") self._print(messages[0].format(url=self._base_url), end="") cld = None errors = [] for _ in range(max_retries): self._print(".", end="", flush=True) if self._local_server and not self._local_server.is_running(): raise H2OServerError("Local server was unable to start") try: define_classes_from_schema(_classes_defined_from_schema_, self) cld = self.request("GET /3/Cloud") if self.name and cld.cloud_name != self.name: raise H2OConnectionError( "Connected to cloud %s but requested %s." % (cld.cloud_name, self.name)) if cld.consensus and cld.cloud_healthy: self._print(" " + messages[1]) return cld else: if cld.consensus and not cld.cloud_healthy: msg = "in consensus but not healthy" elif not cld.consensus and cld.cloud_healthy: msg = "not in consensus but healthy" else: msg = "not in consensus and not healthy" errors.append( "Cloud is in a bad shape: %s (size = %d, bad nodes = %d)" % (msg, cld.cloud_size, cld.bad_nodes)) except (H2OConnectionError, H2OServerError) as e: message = str(e) if "\n" in message: message = message[:message.index("\n")] errors.append("[%s.%02d] %s: %s" % (time.strftime("%M:%S"), int(time.time() * 100) % 100, e.__class__.__name__, message)) # Cloud too small, or voting in progress, or server is not up yet; sleep then try again time.sleep(0.2) self._print(" " + messages[2]) if cld and not cld.cloud_healthy: raise H2OServerError("Cluster reports unhealthy status") if cld and not cld.consensus: raise H2OServerError("Cluster cannot reach consensus") else: raise H2OConnectionError( "Could not establish link to the H2O cloud %s after %d retries\n%s" % (self._base_url, max_retries, "\n".join(errors)))
def _connect_h2o_server(urls: List[str]) -> H2OConnection: """Connect to a list of H2O server urls, first successful connection is returned. Args: urls: List of urls to try sequentially for connection. Returns: H2OConnection object. """ connection = None connection_error = None for url in urls: try: logger.debug(f"Attempting connection to {url}") connection = connect_h2o(url=url, verbose=False) except H2OConnectionError as e: logger.warning(f"Failed connection attempt to {url}") connection_error = e continue else: logger.debug(f"Successfully connected to {url}") connection_error = None break if connection_error: raise connection_error if not connection: raise H2OConnectionError("Failed to connect to h2o server for unknown reason") return connection
def flaky_request(method, url, **kwargs): if method == "GET" and url.find("/3/Jobs/") != -1: global job_request_counter job_request_counter += 1 if job_request_counter == 2: raise H2OConnectionError("Simulated connection failure") return requests.request_orig(method, url, **kwargs)
def __getattr__(self, item): if h2o.cluster(): raise AttributeError(( "Unknown attribute `{prop}` on object of type `{cls}`, " "this property is not available for this H2O backend [version={version}]." ).format(prop=item, cls=self.__class__.__name__, version=h2o.cluster().version)) else: raise H2OConnectionError( "Not connected to a cluster. Did you run `h2o.init()` or `h2o.connect()`?" )
def _init_h2o_server(port: int) -> H2OConnection: """Connect to a local H2O server running on a given port, if not successful start a new server and connect to it. Args: port: Local port to connect and start a new H2O server process. Returns: H2OConnection object. """ logger.info(f"Attempting connection and starting new local server on port {port}") init_h2o(port=port, max_mem_size=master_config.fallback_h2o_max_mem_size_GB) logger.info(f"Successfully connected to local server on port {port}") connection = connection_h2o() if not connection: raise H2OConnectionError("Failed to connect to h2o server for unknown reason") return connection
def check_version(self, strict=False): """ Verifies that h2o-python module and the H2O server are compatible with each other. :param strict: if True, an error is raised on version mismatch, otherwise, a warning is simply printed. """ ver_h2o = self.version ver_pkg = "UNKNOWN" if h2o.__version__ == "SUBST_PROJECT_VERSION" else h2o.__version__ if str(ver_h2o) != str(ver_pkg): branch_name_h2o = self.branch_name build_number_h2o = self.build_number if build_number_h2o is None or build_number_h2o == "unknown": message = ( "Version mismatch. H2O is version {0}, but the h2o-python package is version {1}. " "Upgrade H2O and h2o-Python to latest stable version - " "http://h2o-release.s3.amazonaws.com/h2o/latest_stable.html" ).format(ver_h2o, ver_pkg) elif build_number_h2o == "99999": message = ( "Version mismatch. H2O is version {0}, but the h2o-python package is version {1}. " "This is a developer build, please contact your developer." ).format(ver_h2o, ver_pkg) else: message = ( "Version mismatch. H2O is version {0}, but the h2o-python package is version {1}. " "Install the matching h2o-Python version from - " "http://h2o-release.s3.amazonaws.com/h2o/{2}/{3}/index.html." ).format(ver_h2o, ver_pkg, branch_name_h2o, build_number_h2o) if strict: raise H2OConnectionError(message) else: print("Warning:", message) # Check age of the install if self.build_too_old: print(( "Warning: Your H2O cluster version is too old ({})!" "Please download and install the latest version from http://h2o.ai/download/" ).format(self.build_age))
def request(self, endpoint, data=None, json=None, filename=None, save_to=None): """ Perform a REST API request to the backend H2O server. :param endpoint: (str) The endpoint's URL, for example "GET /4/schemas/KeyV4" :param data: data payload for POST (and sometimes GET) requests. This should be a dictionary of simple key/value pairs (values can also be arrays), which will be sent over in x-www-form-encoded format. :param json: also data payload, but it will be sent as a JSON body. Cannot be used together with `data`. :param filename: file to upload to the server. Cannot be used with `data` or `json`. :param save_to: if provided, will write the response to that file (additionally, the response will be streamed, so large files can be downloaded seamlessly). This parameter can be either a file name, or a folder name. If the folder doesn't exist, it will be created automatically. :returns: an H2OResponse object representing the server's response (unless ``save_to`` parameter is provided, in which case the output file's name will be returned). :raises H2OConnectionError: if the H2O server cannot be reached (or connection is not initialized). :raises H2OServerError: if there was a server error (http 500), or server returned malformed JSON. :raises H2OResponseError: if the server returned an H2OErrorV3 response (e.g. if the parameters were invalid). """ if self._stage == 0: raise H2OConnectionError( "Connection not initialized; run .connect() first.") if self._stage == -1: raise H2OConnectionError( "Connection was closed, and can no longer be used.") # Prepare URL assert_is_type(endpoint, str) match = assert_matches( str(endpoint), r"^(GET|POST|PUT|DELETE|PATCH|HEAD|TRACE) (/.*)$") method = match.group(1) urltail = match.group(2) url = self._base_url + urltail # Prepare data if filename is not None: assert_is_type(filename, str) assert_is_type( json, None, "Argument `json` should be None when `filename` is used.") assert_is_type( data, None, "Argument `data` should be None when `filename` is used.") assert_satisfies( method, method == "POST", "File uploads can only be done via POST method, got %s" % method) elif data is not None: assert_is_type(data, dict) assert_is_type( json, None, "Argument `json` should be None when `data` is used.") elif json is not None: assert_is_type(json, dict) request_data = self._prepare_data_payload( data) if filename is None else self._prepare_file_payload(filename) params = None if (method == "GET" or method == "DELETE") and data: params = request_data request_data = None stream = False if save_to is not None: assert_is_type(save_to, str, types.FunctionType) stream = True if self._cookies is not None and isinstance(self._cookies, list): self._cookies = ";".join(self._cookies) # Make the request start_time = time.time() try: self._log_start_transaction(endpoint, request_data, json, filename, params) args = self._request_args() resp = requests.request(method=method, url=url, data=request_data, json=json, params=params, stream=stream, **args) if isinstance(save_to, types.FunctionType): save_to = save_to(resp) self._log_end_transaction(start_time, resp) return self._process_response(resp, save_to) except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError) as e: if self._local_server and not self._local_server.is_running(): self._log_end_exception("Local server has died.") raise H2OConnectionError( "Local server has died unexpectedly. RIP.") else: self._log_end_exception(e) raise H2OConnectionError("Unexpected HTTP error: %s" % e) except requests.exceptions.Timeout as e: self._log_end_exception(e) elapsed_time = time.time() - start_time raise H2OConnectionError("Timeout after %.3fs" % elapsed_time) except H2OResponseError as e: err = e.args[0] if isinstance(err, H2OErrorV3): err.endpoint = endpoint err.payload = (request_data, json, filename, params) raise
def open(server=None, url=None, ip=None, port=None, name=None, https=None, auth=None, verify_ssl_certificates=True, cacert=None, proxy=None, cookies=None, verbose=True, msgs=None, strict_version_check=True): r""" Establish connection to an existing H2O server. The connection is not kept alive, so what this method actually does is attempt to connect to the specified server, and check that the server is healthy and responds to REST API requests. If the H2O server cannot be reached, an :class:`H2OConnectionError` will be raised. On a success, this method returns a new :class:`H2OConnection` object, and it is the only "official" way to create instances of this class. There are 3 ways to specify the target to connect to (these settings are mutually exclusive): * pass a ``server`` option, * pass the full ``url`` for the connection, * provide a triple of parameters ``ip``, ``port``, ``https``. :param H2OLocalServer server: connect to the specified local server instance. There is a slight difference between connecting to a local server by specifying its ip and address, and connecting through an H2OLocalServer instance: if the server becomes unresponsive, then having access to its process handle will allow us to query the server status through OS, and potentially provide snapshot of the server's error log in the exception information. :param url: full url of the server to connect to. :param ip: target server's IP address or hostname (default "localhost"). :param port: H2O server's port (default 54321). :param name: H2O cluster name. :param https: if True then connect using https instead of http (default False). :param verify_ssl_certificates: if False then SSL certificate checking will be disabled (default True). This setting should rarely be disabled, as it makes your connection vulnerable to man-in-the-middle attacks. When used, it will generate a warning from the requests library. Has no effect when ``https`` is False. :param cacert: Path to a CA bundle file or a directory with certificates of trusted CAs (optional). :param auth: authentication token for connecting to the remote server. This can be either a (username, password) tuple, or an authenticator (AuthBase) object. Please refer to the documentation in the ``requests.auth`` module. :param proxy: url address of a proxy server. If you do not specify the proxy, then the requests module will attempt to use a proxy specified in the environment (in HTTP_PROXY / HTTPS_PROXY variables). We check for the presence of these variables and issue a warning if they are found. In order to suppress that warning and use proxy from the environment, pass ``proxy="(default)"``. :param cookies: Cookie (or list of) to add to requests. :param verbose: if True, then connection progress info will be printed to the stdout. :param strict_version_check: If True, an error will be raised if the client and server versions don't match. :param msgs: custom messages to display during connection. This is a tuple (initial message, success message, failure message). :returns: A new :class:`H2OConnection` instance. :raises H2OConnectionError: if the server cannot be reached. :raises H2OServerError: if the server is in an unhealthy state (although this might be a recoverable error, the client itself should decide whether it wants to retry or not). """ if server is not None: assert_is_type(server, H2OLocalServer) assert_is_type( ip, None, "`ip` should be None when `server` parameter is supplied") assert_is_type( url, None, "`url` should be None when `server` parameter is supplied") assert_is_type( name, None, "`name` should be None when `server` parameter is supplied") if not server.is_running(): raise H2OConnectionError( "Unable to connect to server because it is not running") ip = server.ip port = server.port scheme = server.scheme context_path = '' elif url is not None: assert_is_type(url, str) assert_is_type( ip, None, "`ip` should be None when `url` parameter is supplied") assert_is_type(name, str, None) # We don't allow any Unicode characters in the URL. Maybe some day we will... match = assert_matches(url, H2OConnection.url_pattern) scheme = match.group(1) ip = match.group(2) port = int(match.group(3)) context_path = '' if match.group(4) is None else "%s" % ( match.group(4)) else: if ip is None: ip = str("localhost") if port is None: port = 54321 if https is None: https = False if is_type(port, str) and port.isdigit(): port = int(port) assert_is_type(ip, str) assert_is_type(port, int) assert_is_type(name, str, None) assert_is_type(https, bool) assert_matches(ip, r"(?:[\w-]+\.)*[\w-]+") assert_satisfies(port, 1 <= port <= 65535) scheme = "https" if https else "http" context_path = '' if verify_ssl_certificates is None: verify_ssl_certificates = True assert_is_type(verify_ssl_certificates, bool) assert_is_type(cacert, str, None) assert_is_type(proxy, str, None) assert_is_type(auth, AuthBase, (str, str), None) assert_is_type(cookies, str, [str], None) assert_is_type(msgs, None, (str, str, str)) conn = H2OConnection() conn._verbose = bool(verbose) conn._local_server = server conn._base_url = "%s://%s:%d%s" % (scheme, ip, port, context_path) conn._name = server.name if server else name conn._verify_ssl_cert = bool(verify_ssl_certificates) conn._cacert = cacert conn._auth = auth conn._cookies = cookies conn._proxies = None if proxy and proxy != "(default)": conn._proxies = {scheme: proxy} elif not proxy: # Give user a warning if there are any "*_proxy" variables in the environment. [PUBDEV-2504] # To suppress the warning pass proxy = "(default)". for name in os.environ: if name.lower() == scheme + "_proxy": warn("Proxy is defined in the environment: %s. " "This may interfere with your H2O Connection." % name) if "localhost" in conn.ip() or "127.0.0.1" in conn.ip(): # Empty list will cause requests library to respect the default behavior. # Thus a non-existing proxy is inserted. conn._proxies = { "http": None, "https": None, } try: retries = 20 if server else 5 conn._stage = 1 conn._timeout = 3.0 conn._cluster = conn._test_connection(retries, messages=msgs) # If a server is unable to respond within 1s, it should be considered a bug. However we disable this # setting for now, for no good reason other than to ignore all those bugs :( conn._timeout = None # create a weakref to prevent the atexit callback from keeping hard ref # to the connection even after manual close. conn_ref = ref(conn) def exit_close(): con = conn_ref() if con and con.connected: print("Closing connection %s at exit" % con.session_id) con.close() atexit.register(exit_close) except Exception: # Reset _session_id so that we know the connection was not initialized properly. conn._stage = 0 raise conn._cluster.check_version(strict=strict_version_check) return conn
def _deprecated_check_conn(): if not __H2OCONN__: raise H2OConnectionError( "No active connection to an H2O cluster. Try calling `h2o.connect()`" ) return __H2OCONN__
def open(server=None, url=None, ip=None, port=None, https=None, auth=None, verify_ssl_certificates=True, proxy=None, cluster_name=None, verbose=True): r""" Establish connection to an existing H2O server. The connection is not kept alive, so what this method actually does is it attempts to connect to the specified server, and checks that the server is healthy and responds to REST API requests. If the H2O server cannot be reached, an :class:`H2OConnectionError` will be raised. On success this method returns a new :class:`H2OConnection` object, and it is the only "official" way to create instances of this class. There are 3 ways to specify the target to connect to (these settings are mutually exclusive): * pass a ``server`` option, * pass the full ``url`` for the connection, * provide a triple of parameters ``ip``, ``port``, ``https``. :param H2OLocalServer server: connect to the specified local server instance. There is a slight difference between connecting to a local server by specifying its ip and address, and connecting through an H2OLocalServer instance: if the server becomes unresponsive, then having access to its process handle will allow us to query the server status through OS, and potentially provide snapshot of the server's error log in the exception information. :param url: full url of the server to connect to. :param ip: target server's IP address or hostname (default "localhost"). :param port: H2O server's port (default 54321). :param https: if True then connect using https instead of http (default False). :param verify_ssl_certificates: if False then SSL certificate checking will be disabled (default True). This setting should rarely be disabled, as it makes your connection vulnerable to man-in-the-middle attacks. When used, it will generate a warning from the requests library. Has no effect when ``https`` is False. :param auth: authentication token for connecting to the remote server. This can be either a (username, password) tuple, or an authenticator (AuthBase) object. Please refer to the documentation in the ``requests.auth`` module. :param proxy: url address of a proxy server. If you do not specify the proxy, then the requests module will attempt to use a proxy specified in the environment (in HTTP_PROXY / HTTPS_PROXY variables). We check for the presence of these variables and issue a warning if they are found. In order to suppress that warning and use proxy from the environment, pass ``proxy="(default)"``. :param cluster_name: name of the H2O cluster to connect to. This option is used from Steam only. :param verbose: if True, then connection progress info will be printed to the stdout. :returns: A new :class:`H2OConnection` instance. :raises H2OConnectionError: if the server cannot be reached. :raises H2OServerError: if the server is in an unhealthy state (although this might be a recoverable error, the client itself should decide whether it wants to retry or not). """ if server is not None: assert_is_type(server, H2OLocalServer) assert_is_type( ip, None, "`ip` should be None when `server` parameter is supplied") assert_is_type( url, None, "`ip` should be None when `server` parameter is supplied") if not server.is_running(): raise H2OConnectionError( "Unable to connect to server because it is not running") ip = server.ip port = server.port scheme = server.scheme elif url is not None: assert_is_type(url, str) assert_is_type( ip, None, "`ip` should be None when `url` parameter is supplied") # We don't allow any Unicode characters in the URL. Maybe some day we will... match = assert_matches( url, r"^(https?)://((?:[\w-]+\.)*[\w-]+):(\d+)/?$") scheme = match.group(1) ip = match.group(2) port = int(match.group(3)) else: if ip is None: ip = str("localhost") if port is None: port = 54321 if https is None: https = False if is_str(port) and port.isdigit(): port = int(port) assert_is_type(ip, str) assert_is_type(port, int) assert_is_type(https, bool) assert_matches(ip, r"(?:[\w-]+\.)*[\w-]+") assert_satisfies(port, 1 <= port <= 65535) scheme = "https" if https else "http" if verify_ssl_certificates is None: verify_ssl_certificates = True assert_is_type(verify_ssl_certificates, bool) assert_is_type(proxy, str, None) assert_is_type(auth, AuthBase, (str, str), None) assert_is_type(cluster_name, str, None) conn = H2OConnection() conn._verbose = bool(verbose) conn._local_server = server conn._base_url = "%s://%s:%d" % (scheme, ip, port) conn._verify_ssl_cert = bool(verify_ssl_certificates) conn._auth = auth conn._cluster_name = cluster_name conn._proxies = None if proxy and proxy != "(default)": conn._proxies = {scheme: proxy} elif not proxy: # Give user a warning if there are any "*_proxy" variables in the environment. [PUBDEV-2504] # To suppress the warning pass proxy = "(default)". for name in os.environ: if name.lower() == scheme + "_proxy": warn("Proxy is defined in the environment: %s. " "This may interfere with your H2O Connection." % os.environ[name]) try: # Make a fake _session_id, otherwise .request() will complain that the connection is not initialized retries = 20 if server else 5 conn._stage = 1 conn._timeout = 3.0 conn._cluster_info = conn._test_connection(retries) # If a server is unable to respond within 1s, it should be considered a bug. However we disable this # setting for now, for no good reason other than to ignore all those bugs :( conn._timeout = None atexit.register(lambda: conn.close()) except: # Reset _session_id so that we know the connection was not initialized properly. conn._stage = 0 raise return conn
def request(self, endpoint, data=None, json=None, filename=None): """ Perform a REST API request to the backend H2O server. :param endpoint: (str) The endpoint's URL, for example "GET /4/schemas/KeyV4" :param data: data payload for POST (and sometimes GET) requests. This should be a dictionary of simple key/value pairs (values can also be arrays), which will be sent over in x-www-form-encoded format. :param json: also data payload, but it will be sent as a JSON body. Cannot be used together with `data`. :param filename: file to upload to the server. Cannot be used with `data` or `json`. :returns: an H2OResponse object representing the server's response :raises H2OConnectionError: if the H2O server cannot be reached (or connection is not initialized) :raises H2OServerError: if there was a server error (http 500), or server returned malformed JSON :raises H2OResponseError: if the server returned an H2OErrorV3 response (e.g. if the parameters were invalid) """ if self._stage == 0: raise H2OConnectionError( "Connection not initialized; run .connect() first.") if self._stage == -1: raise H2OConnectionError( "Connection was closed, and can no longer be used.") # Prepare URL assert_is_type(endpoint, str) match = assert_matches(str(endpoint), r"^(GET|POST|PUT|DELETE|PATCH|HEAD) (/.*)$") method = match.group(1) urltail = match.group(2) url = self._base_url + urltail # Prepare data if filename is not None: assert_is_type(filename, str) assert_is_type( json, None, "Argument `json` should be None when `filename` is used.") assert_is_type( data, None, "Argument `data` should be None when `filename` is used.") assert_satisfies( method, method == "POST", "File uploads can only be done via POST method, got %s" % method) elif data is not None: assert_is_type(data, dict) assert_is_type( json, None, "Argument `json` should be None when `data` is used.") elif json is not None: assert_is_type(json, dict) data = self._prepare_data_payload(data) files = self._prepare_file_payload(filename) params = None if method == "GET" and data: params = data data = None # Make the request start_time = time.time() try: self._log_start_transaction(endpoint, data, json, files, params) headers = { "User-Agent": "H2O Python client/" + sys.version.replace("\n", ""), "X-Cluster": self._cluster_name } resp = requests.request(method=method, url=url, data=data, json=json, files=files, params=params, headers=headers, timeout=self._timeout, auth=self._auth, verify=self._verify_ssl_cert, proxies=self._proxies) self._log_end_transaction(start_time, resp) return self._process_response(resp) except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError) as e: if self._local_server and not self._local_server.is_running(): self._log_end_exception("Local server has died.") raise H2OConnectionError( "Local server has died unexpectedly. RIP.") else: self._log_end_exception(e) raise H2OConnectionError("Unexpected HTTP error: %s" % e) except requests.exceptions.Timeout as e: self._log_end_exception(e) elapsed_time = time.time() - start_time raise H2OConnectionError("Timeout after %.3fs" % elapsed_time) except H2OResponseError as e: err = e.args[0] err.endpoint = endpoint err.payload = (data, json, files, params) raise