diff --git a/.gitignore b/.gitignore index a818682..34231f8 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,4 @@ GeoHealthCheck.conf # Data GeoHealthCheck/data.db +/.idea diff --git a/GeoHealthCheck/app.py b/GeoHealthCheck/app.py index ea1eec0..8c0d3bf 100644 --- a/GeoHealthCheck/app.py +++ b/GeoHealthCheck/app.py @@ -737,8 +737,7 @@ def test(resource_identifier): return redirect(request.referrer) from healthcheck import run_test_resource - result = run_test_resource( - resource) + result = run_test_resource(resource) if request.method == 'GET': if result.message == 'Skipped': diff --git a/GeoHealthCheck/config_main.py b/GeoHealthCheck/config_main.py index 0a04b15..df45db7 100644 --- a/GeoHealthCheck/config_main.py +++ b/GeoHealthCheck/config_main.py @@ -115,6 +115,9 @@ 'GeoHealthCheck.plugins.probe.ogcfeat', 'GeoHealthCheck.plugins.probe.ogc3dtiles', 'GeoHealthCheck.plugins.probe.esrifs', + 'GeoHealthCheck.plugins.probe.esrims', + 'GeoHealthCheck.plugins.probe.oracle', + 'GeoHealthCheck.plugins.probe.postgres', 'GeoHealthCheck.plugins.probe.ghcreport', 'GeoHealthCheck.plugins.probe.mapbox', @@ -166,9 +169,18 @@ 'OGC:3DTiles': { 'probe_class': 'GeoHealthCheck.plugins.probe.ogc3dtiles.OGC3DTiles' }, + 'ORACLE': { + 'probe_class': 'GeoHealthCheck.plugins.probe.oracle.OracleDrilldown' + }, + 'POSTGRES': { + 'probe_class': 'GeoHealthCheck.plugins.probe.postgres.PostgresDrilldown' + }, 'ESRI:FS': { 'probe_class': 'GeoHealthCheck.plugins.probe.esrifs.ESRIFSDrilldown' }, + 'ESRI:MS': { + 'probe_class': 'GeoHealthCheck.plugins.probe.esrims.ESRIMSDrilldown' + }, 'Mapbox:TileJSON': { 'probe_class': 'GeoHealthCheck.plugins.probe.mapbox.TileJSON' }, diff --git a/GeoHealthCheck/enums.py b/GeoHealthCheck/enums.py index b990f8d..d639e47 100644 --- a/GeoHealthCheck/enums.py +++ b/GeoHealthCheck/enums.py @@ -78,7 +78,10 @@ 'label': 'OGC 3D Tiles (OGC3D)' }, 'ESRI:FS': { - 'label': 'ESRI ArcGIS FeatureServer (FS)' + 'label': 'ESRI ArcGIS FeatureServer' + }, + 'ESRI:MS': { + 'label': 'ESRI ArcGIS MapServer' }, 'Mapbox:TileJSON': { 'label': 'Mapbox TileJSON Service (TileJSON)' @@ -92,6 +95,12 @@ 'FTP': { 'label': 'File Transfer Protocol (FTP)' }, + 'ORACLE': { + 'label': 'Oracle Database' + }, + 'POSTGRES': { + 'label': 'Postgres Database' + }, 'OSGeo:GeoNode': { 'label': 'GeoNode instance' }, diff --git a/GeoHealthCheck/healthcheck.py b/GeoHealthCheck/healthcheck.py index 1451699..28be2bd 100644 --- a/GeoHealthCheck/healthcheck.py +++ b/GeoHealthCheck/healthcheck.py @@ -158,6 +158,9 @@ def sniff_test_resource(config, resource_type, url): 'OGCFeat': [urlopen], 'OGC:3DTiles': [urlopen], 'ESRI:FS': [urlopen], + 'ESRI:MS': [urlopen], + 'ORACLE': [oracle_connect], + 'POSTGRES': [postgres_connect], 'OGC:STA': [urlopen], 'WWW:LINK': [urlopen], 'FTP': [urlopen], @@ -244,7 +247,9 @@ def sniff_test_resource(config, resource_type, url): elif resource_type == 'OGCFeat': title = 'OGC API Features (OAFeat)' elif resource_type == 'ESRI:FS': - title = 'ESRI ArcGIS FS' + title = 'ESRI ArcGIS FeatureService' + elif resource_type == 'ESRI:MS': + title = 'ESRI ArcGIS MapService' elif resource_type == 'OGC:3DTiles': title = 'OGC 3D Tiles' else: @@ -309,6 +314,25 @@ def geonode_make_tags(base_url): return [tag_name] +def oracle_connect(connect_string): + d = {} + for c in connect_string.split(";"): + key = c.split("=")[0] + value = c.split("=")[1] + d[key] = value + base_name = 'Oracle : {}'.format(d["service"]) + return True + +def postgres_connect(connect_string): + d = {} + for c in connect_string.split(";"): + key = c.split("=")[0] + value = c.split("=")[1] + d[key] = value + base_name = 'Postgres : {}'.format(d["database"]) + return True + + if __name__ == '__main__': print('START - Running health check tests on %s' % datetime.utcnow().isoformat()) diff --git a/GeoHealthCheck/models.py b/GeoHealthCheck/models.py index 2c09b51..5d452db 100644 --- a/GeoHealthCheck/models.py +++ b/GeoHealthCheck/models.py @@ -430,7 +430,7 @@ def run_count(self): def get_capabilities_url(self): if self.resource_type.startswith('OGC:') \ and self.resource_type not in \ - ['OGC:STA', 'OGCFeat', 'ESRI:FS', 'OGC:3DTiles']: + ['OGC:STA', 'OGCFeat', 'ESRI:FS', 'ESRI:MS', 'OGC:3DTiles']: url = '%s%s' % (bind_url(self.url), RESOURCE_TYPES[self.resource_type]['capabilities']) else: diff --git a/GeoHealthCheck/plugins/probe/esrims.py b/GeoHealthCheck/plugins/probe/esrims.py new file mode 100644 index 0000000..754ea47 --- /dev/null +++ b/GeoHealthCheck/plugins/probe/esrims.py @@ -0,0 +1,200 @@ +from GeoHealthCheck.probe import Probe +from GeoHealthCheck.result import Result, push_result + + +class ESRIMSDrilldown(Probe): + """ + Probe for ESRI MapServer endpoint "drilldown": starting + with top /MapServer endpoint: get Layers and get Features on these. + Test e.g. from https://sampleserver6.arcgisonline.com/arcgis/rest/services + (at least sampleserver6 is ArcGIS 10.6.1 supporting Paging). + """ + + NAME = 'ESRIMS Drilldown' + + DESCRIPTION = 'Traverses an ESRI MapServer ' \ + '(REST) API endpoint by drilling down' + + RESOURCE_TYPE = 'ESRI:MS' + + REQUEST_METHOD = 'GET' + + PARAM_DEFS = { + 'drilldown_level': { + 'type': 'string', + 'description': 'How heavy the drilldown should be.\ + basic: test presence of Capabilities, \ + full: go through Layers, get Features', + 'default': 'basic', + 'required': True, + 'range': ['basic', 'full'] + } + } + """Param defs""" + + def __init__(self): + Probe.__init__(self) + + def get_request_headers(self): + headers = Probe.get_request_headers(self) + + # Clear possibly dangling ESRI header + # https://github.com/geopython/GeoHealthCheck/issues/293 + if 'X-Esri-Authorization' in headers: + del headers['X-Esri-Authorization'] + + if 'Authorization' in headers: + # https://enterprise.arcgis.com/en/server/latest/ + # administer/linux/about-arcgis-tokens.htm + auth_val = headers['Authorization'] + if 'Bearer' in auth_val: + headers['X-Esri-Authorization'] = headers['Authorization'] + return headers + + def perform_esrims_get_request(self, url): + response = self.perform_get_request(url).json() + error_msg = 'code=%d message=%s' + # May have error like: + # { + # "error" : + # { + # "code" : 499, + # "message" : "Token Required", + # "messageCode" : "GWM_0003", + # "details" : [ + # "Token Required" + # ] + # } + # } + if 'error' in response: + err = response['error'] + raise Exception(error_msg % (err['code'], err['message'])) + + return response + + def perform_request(self): + """ + Perform the drilldown. + """ + + # Be sure to use bare root URL http://.../MapServer + ms_url = self._resource.url.split('?')[0] + + # Assemble request templates with root MS URL + req_tpl = { + 'ms_caps': ms_url + '?f=json', + + 'layer_caps': ms_url + '/%d?f=json', + + 'get_features': ms_url + + '/%d/query?where=1=1' + '&outFields=*&resultOffset=0&' + 'resultRecordCount=1&f=json', + + 'get_feature_by_id': ms_url + + '/%d/query?where=%s=%s&outFields=*&f=json' + } + + # 1. Test top Service endpoint existence + result = Result(True, 'Test Service Endpoint') + result.start() + layers = [] + try: + ms_caps = self.perform_esrims_get_request(req_tpl['ms_caps']) + for attr in ['currentVersion', 'layers']: + val = ms_caps.get(attr, None) + if val is None: + msg = 'Service: missing attr: %s' % attr + result = push_result( + self, result, False, msg, 'Test Layer:') + continue + + layers = ms_caps.get('layers', []) + + except Exception as err: + result.set(False, str(err)) + + result.stop() + self.result.add_result(result) + + if len(layers) == 0: + return + + # 2. Test each Layer Capabilities + result = Result(True, 'Test Layer Capabilities') + result.start() + layer_ids = [] + layer_caps = [] + try: + + for layer in layers: + if layer['subLayerIds'] is None: + layer_ids.append(layer['id']) + + for layer_id in layer_ids: + layer_caps.append(self.perform_esrims_get_request( + req_tpl['layer_caps'] % layer_id)) + + except Exception as err: + result.set(False, str(err)) + + result.stop() + self.result.add_result(result) + + if self._parameters['drilldown_level'] == 'basic': + return + + # ASSERTION: will do full drilldown from here + + # 3. Test getting Features from Layers + result = Result(True, 'Test 1 record for each layer in Layers') + result.start() + layer_id = -1 + try: + for layer_id in layer_ids: + try: + features = self.perform_esrims_get_request( + req_tpl['get_features'] % layer_id) + # Get the name of the OBJECTID FieldName + # In a FeatureService this is direct available from features['objectIdFieldName'] + # In a MapService this must be done by looping through the response fields and find the field with type 'esriFieldTypeOID' + obj_id_field_name = None + for f in features['fields']: + if f['type'] == 'esriFieldTypeOID': + obj_id_field_name = f['name'] + break + + features = features['features'] + if len(features) == 0: + continue + + # At least one Feature: use first and try to get by id + object_id = features[0]['attributes'][obj_id_field_name] + feature = self.perform_get_request( + req_tpl['get_feature_by_id'] % ( + layer_id, obj_id_field_name, + str(object_id))).json() + + feature = feature['features'] + if len(feature) == 0: + msg = 'layer: %d: missing Feature - id: %s' \ + % (layer_id, str(object_id)) + result = push_result( + self, result, False, msg, + 'Test Layer: %d' % layer_id) + + except Exception as e: + msg = 'GetLayer: id=%d: err=%s ' \ + % (layer_id, str(e)) + result = push_result( + self, result, False, msg, 'Test Get Features:') + continue + + except Exception as err: + result.set(False, 'Layer: id=%d : err=%s' + % (layer_id, str(err))) + + result.stop() + + # Add to overall Probe result + self.result.add_result(result) diff --git a/GeoHealthCheck/plugins/probe/http.py b/GeoHealthCheck/plugins/probe/http.py index 0687eb0..3f69019 100644 --- a/GeoHealthCheck/plugins/probe/http.py +++ b/GeoHealthCheck/plugins/probe/http.py @@ -58,7 +58,7 @@ class HttpPost(HttpGet): """ REQUEST_METHOD = 'POST' - REQUEST_HEADERS = {'content-type': '{post_content_type}'} + REQUEST_HEADERS = {'Content-Type': '{post_content_type}'} REQUEST_TEMPLATE = '{body}' PARAM_DEFS = { diff --git a/GeoHealthCheck/plugins/probe/oracle.py b/GeoHealthCheck/plugins/probe/oracle.py new file mode 100644 index 0000000..287b50a --- /dev/null +++ b/GeoHealthCheck/plugins/probe/oracle.py @@ -0,0 +1,119 @@ +import oracledb + +from GeoHealthCheck.probe import Probe +from GeoHealthCheck.result import Result, push_result + +class OracleDrilldown(Probe): + """ + Probe for Oracle Database endpoint "drilldown" + Possible tests are 'basic' for testing the connection to the database (is it up-and-running). + Or 'full' for testing both the connection and if Oracle Spatial functionality is available. + """ + + NAME = 'Oracle Drilldown' + + DESCRIPTION = 'Checks an Oracle database connection, use a string like "host=<hostname>;port=<portnumber>;service=<servicename>" to define the database connection' + + RESOURCE_TYPE = 'ORACLE' + + REQUEST_METHOD = 'DB' + + PARAM_DEFS = { + 'drilldown_level': { + 'type': 'string', + 'description': 'Which drilldown should be used.\ + basic: test connection, \ + full: test connection and check Oracle Spatial', + 'default': 'basic', + 'required': True, + 'range': ['basic', 'full'] + } + } + """Param defs""" + + def __init__(self): + Probe.__init__(self) + + def perform_ora_get_request(self, connectstring,sql): + response = None + try: + with oracledb.connect(connectstring) as con: + cursor = con.cursor() + result, = cursor.execute(sql) + response = repr(result) + except Exception as err: + raise Exception("Error: " + err) + + return response + + def perform_request(self): + """ + Perform the drilldown. + """ + + d = {} + for c in self._resource.url.split(";"): + key = c.split("=")[0] + value = c.split("=")[1] + d[key.lower()] = value + + # Check connection data + host = None + servicename = None + portnumber = "1521" + if "host" in self._resource.url.lower(): + host = d["host"] + if "service" in self._resource.url.lower(): + servicename = d["service"] + if "port" in self._resource.url.lower(): + portnumber = d["port"] + + if host is None or servicename is None: + raise Exception("No Database host or service in url") + + # Assemble request templates with root FS URL + if self._resource.auth['type'] == 'Basic': + usr = self._resource.auth['data']['username'] + pwd = self._resource.auth['data']['password'] + dsn = '{usr}/{pwd}@{host}:{portnumber}/{service}'.format(usr=usr,pwd=pwd,host=host,portnumber=portnumber,service=servicename) + else: + raise Exception("No username and password as Basic authentication saved") + + req_tpl = { + 'connectstring':dsn, + 'basic_check':'select to_char(current_date) from dual', + 'full_check':"select SDO_UTIL.FROM_WKTGEOMETRY('POINT(155000 463000)') from dual" + } + + # 1. Test top Service endpoint existence + result = Result(True, f"Test Oracle connection") + result.start() + try: + ora_result = self.perform_ora_get_request(req_tpl['connectstring'],req_tpl['basic_check']) + if ora_result is None: + result.set(False,"Error: The query '{}' was not executed".format(req_tpl['basic_check'])) + except Exception as err: + result.set(False, str(err)) + + result.stop() + self.result.add_result(result) + + if self._parameters['drilldown_level'] == 'basic': + return + + # ASSERTION: will do full drilldown from here + + # 2. Test Oracle Spatial + result = Result(True, 'Test Oracle Spatial') + result.start() + try: + ora_result = self.perform_ora_get_request(req_tpl['connectstring'],req_tpl['full_check']) + if ora_result is None: + result.set(False, "Error: The query '{}' was not executed".format(req_tpl['full_check'])) + except Exception as err: + result.set(False, str(err)) + + result.stop() + + # Add to overall Probe result + self.result.add_result(result) diff --git a/GeoHealthCheck/plugins/probe/postgres.py b/GeoHealthCheck/plugins/probe/postgres.py new file mode 100644 index 0000000..1ad74e2 --- /dev/null +++ b/GeoHealthCheck/plugins/probe/postgres.py @@ -0,0 +1,120 @@ +import psycopg2 + +from GeoHealthCheck.probe import Probe +from GeoHealthCheck.result import Result, push_result + +class PostgresDrilldown(Probe): + """ + Probe for Postgres Database endpoint "drilldown" + Possible tests are 'basic' for testing the connection to the database (is it up-and-running). + Or 'full' for testing both the connection and if ESRI's ST_GEOMETRY library is available. + """ + + NAME = 'Postgres Drilldown' + + DESCRIPTION = 'Checks a Postgres database connection, use a string like "host=<hostname>;port=<portnumber>;database=<databasename>" to define the database connection' + + RESOURCE_TYPE = 'POSTGRES' + + REQUEST_METHOD = 'DB' + + PARAM_DEFS = { + 'drilldown_level': { + 'type': 'string', + 'description': 'Which drilldown should be used.\ + basic: test connection, \ + full: test connection and check ESRI ST_GEOMETRY', + 'default': 'basic', + 'required': True, + 'range': ['basic', 'full'] + } + } + """Param defs""" + + def __init__(self): + Probe.__init__(self) + + def perform_pg_get_request(self, host,databasename,portnumber,usr,pwd,sql): + response = None + try: + con = psycopg2.connect(dbname=databasename, host=host, user=usr, password=pwd, port=portnumber) + cursor = con.cursor() + result = cursor.execute(sql) + response = cursor.rowcount + con.close() + except Exception as err: + raise Exception("Error: " + err) + + return response + + def perform_request(self): + """ + Perform the drilldown. + """ + + d = {} + for c in self._resource.url.split(";"): + key = c.split("=")[0] + value = c.split("=")[1] + d[key.lower()] = value + + # Check connection data + host = None + databasename = None + portnumber = "5432" + if "host" in self._resource.url.lower(): + host = d["host"] + if "database" in self._resource.url.lower(): + databasename = d["database"] + if "port" in self._resource.url.lower(): + portnumber = d["port"] + + if host is None or databasename is None: + raise Exception("No Database host or databasename in url") + + # Assemble request templates with root FS URL + try: + if self._resource.auth['type'] == 'Basic': + usr = self._resource.auth['data']['username'] + pwd = self._resource.auth['data']['password'] + except Exception: + raise Exception("No username and password as Basic authentication saved") + + req_tpl = { + 'basic_check':'SELECT CURRENT_DATE', + 'full_check':"select sde.st_x(sde.st_point (155000, 463000, 28992)) as X, sde.st_y(sde.st_point (155000, 463000, 28992)) as Y" + } + + # 1. Test top Service endpoint existence + result = Result(True, f"Test Postgres connection") + result.start() + try: + pg_result = self.perform_pg_get_request(host,databasename,portnumber,usr,pwd,req_tpl['basic_check']) + # pg_result = self.perform_pg_get_request(req_tpl['connectstring'],req_tpl['basic_check']) + if pg_result is None: + result.set(False,"Error: The query '{}' was not executed".format(req_tpl['basic_check'])) + except Exception as err: + result.set(False, str(err)) + + result.stop() + self.result.add_result(result) + + if self._parameters['drilldown_level'] == 'basic': + return + + # ASSERTION: will do full drilldown from here + + # 2. Test ESRI ST_GEOMETRY + result = Result(True, 'Test Postgres ESRI ST_GEOMETRY') + result.start() + try: + pg_result = self.perform_pg_get_request(host,databasename,portnumber,usr,pwd,req_tpl['full_check']) + if pg_result is None: + result.set(False, "Error: The query '{}' was not executed".format(req_tpl['full_check'])) + except Exception as err: + result.set(False, str(err)) + + result.stop() + + # Add to overall Probe result + self.result.add_result(result) diff --git a/GeoHealthCheck/probe.py b/GeoHealthCheck/probe.py index 44a3ae6..ea5dcf8 100644 --- a/GeoHealthCheck/probe.py +++ b/GeoHealthCheck/probe.py @@ -289,6 +289,7 @@ def perform_request(self): self.response = self.perform_get_request(url) elif self.REQUEST_METHOD == 'POST': + request_string = request_string.replace("?","") self.response = self.perform_post_request( url_base, request_string) except requests.exceptions.RequestException as e: diff --git a/GeoHealthCheck/util.py b/GeoHealthCheck/util.py index 6ca5b76..82913b1 100644 --- a/GeoHealthCheck/util.py +++ b/GeoHealthCheck/util.py @@ -110,6 +110,10 @@ def get_python_snippet(resource): if resource.resource_type.startswith('OGC:'): lines.append('# testing via OWSLib') lines.append('# test GetCapabilities') + elif resource.resource_type == 'ORACLE': + lines.append('# testing via OracleDB') + elif resource.resource_type == 'POSTGRES': + lines.append('# testing via PostgresDB') else: lines.append('# testing via urllib2 and urlparse') diff --git a/GeoHealthCheck/views.py b/GeoHealthCheck/views.py index 4d4c8b7..1ca4139 100644 --- a/GeoHealthCheck/views.py +++ b/GeoHealthCheck/views.py @@ -218,8 +218,11 @@ def get_probes_avail(resource_type=None, resource=None): # Assume no resource type filters = None if resource_type: - filters = [('RESOURCE_TYPE', resource_type), - ('RESOURCE_TYPE', '*:*')] + if resource_type == "ORACLE" or resource_type == "POSTGRES": + filters = [('RESOURCE_TYPE', resource_type)] + else: + filters = [('RESOURCE_TYPE', resource_type), + ('RESOURCE_TYPE', '*:*')] probe_classes = Plugin.get_plugins('GeoHealthCheck.probe.Probe', filters) diff --git a/docs/plugins.rst b/docs/plugins.rst index 021a674..7e49573 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -339,8 +339,8 @@ See an example for both below from `config_main.py` for **GHC_PLUGINS** and **GH 'OGCFeat': { 'probe_class': 'GeoHealthCheck.plugins.probe.ogcfeat.OGCFeatDrilldown' }, - 'ESRI:FS': { - 'probe_class': 'GeoHealthCheck.plugins.probe.esrifs.ESRIFSDrilldown' + 'ESRI': { + 'probe_class': 'GeoHealthCheck.plugins.probe.esri.ESRIDrilldown' }, 'urn:geoss:waf': { 'probe_class': 'GeoHealthCheck.plugins.probe.http.HttpGet' @@ -469,7 +469,7 @@ to override any of the `Probe` baseclass methods. :members: :show-inheritance: -.. automodule:: GeoHealthCheck.plugins.probe.esrifs +.. automodule:: GeoHealthCheck.plugins.probe.esri :members: :show-inheritance: diff --git a/tests/data/fixtures.json b/tests/data/fixtures.json index 1326a34..b621107 100644 --- a/tests/data/fixtures.json +++ b/tests/data/fixtures.json @@ -117,6 +117,16 @@ "tags": [ "esri" ] + }, + "ESRI MAPSERVER": { + "owner": "admin", + "resource_type": "ESRI:MS", + "active": true, + "title": "ESRI ArcGIS MapServer (MS)", + "url": "https://sampleserver6.arcgisonline.com/arcgis/rest/services/AGP/USA/MapServer", + "tags": [ + "esri" + ] } }, "probe_vars": { @@ -292,7 +302,14 @@ "parameters": { "drilldown_level": "full" } - } + }, + "ESRIMS - Drilldown": { + "resource": "ESRI MAPSERVER", + "probe_class": "GeoHealthCheck.plugins.probe.esrims.ESRIMSDrilldown", + "parameters": { + "drilldown_level": "full" + } + } }, "check_vars": { "PDOK BAG WMS - GetCaps - XML Parse": { diff --git a/tests/data/resources.json b/tests/data/resources.json index 2cdbbd5..ab05d09 100644 --- a/tests/data/resources.json +++ b/tests/data/resources.json @@ -105,6 +105,26 @@ "tags": [ "tiling" ] + }, + "ESRI FEATURESERVER": { + "owner": "admin", + "resource_type": "ESRI:FS", + "active": true, + "title": "ESRI ArcGIS FeatureServer (FS)", + "url": "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Wildfire/FeatureServer", + "tags": [ + "esri" + ] + }, + "ESRI MAPSERVER": { + "owner": "admin", + "resource_type": "ESRI:MS", + "active": true, + "title": "ESRI ArcGIS MapServer (MS)", + "url": "https://sampleserver6.arcgisonline.com/arcgis/rest/services/AGP/USA/MapServer", + "tags": [ + "esri" + ] } }, "probe_vars": { @@ -242,6 +262,20 @@ "y": "0", "extension" : "png" } + }, + "ESRIFS - Drilldown": { + "resource": "ESRI FEATURESERVER", + "probe_class": "GeoHealthCheck.plugins.probe.esrifs.ESRIFSDrilldown", + "parameters": { + "drilldown_level": "full" + } + }, + "ESRIMS - Drilldown": { + "resource": "ESRI MAPSERVER", + "probe_class": "GeoHealthCheck.plugins.probe.esrims.ESRIMSDrilldown", + "parameters": { + "drilldown_level": "full" + } } }, "check_vars": {