From 0d782de77cd8c1565adf8d0efbb2d44c8b4cb132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luuk=20Schamin=C3=A9e?= Date: Tue, 18 Jul 2023 06:33:24 +0200 Subject: [PATCH 1/5] ArcGIS Server Mapservice probe added Now it is possible to probe ArcGIS Server MapServices --- GeoHealthCheck/enums.py | 3 + GeoHealthCheck/plugins/probe/esrims.py | 193 +++++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 GeoHealthCheck/plugins/probe/esrims.py diff --git a/GeoHealthCheck/enums.py b/GeoHealthCheck/enums.py index b990f8d..e05ec5a 100644 --- a/GeoHealthCheck/enums.py +++ b/GeoHealthCheck/enums.py @@ -80,6 +80,9 @@ 'ESRI:FS': { 'label': 'ESRI ArcGIS FeatureServer (FS)' }, + 'ESRI:MS': { + 'label': 'ESRI ArcGIS MapServer (MS)' + }, 'Mapbox:TileJSON': { 'label': 'Mapbox TileJSON Service (TileJSON)' }, diff --git a/GeoHealthCheck/plugins/probe/esrims.py b/GeoHealthCheck/plugins/probe/esrims.py new file mode 100644 index 0000000..d2f74b7 --- /dev/null +++ b/GeoHealthCheck/plugins/probe/esrims.py @@ -0,0 +1,193 @@ +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 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) + obj_id_field_name = "OBJECTID" #features['objectIdFieldName'] + 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) From 5bc206ebdff309c708fc50e17dfa0916f149b57a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luuk=20Schamin=C3=A9e?= Date: Thu, 17 Aug 2023 10:32:36 +0200 Subject: [PATCH 2/5] Support for ESRI Feature- en MapServices in 1-and-the-same probe called esri The complete URL of the Feature- of MapService is the input for the probe. Based on the last part of the url (FeatureServer or MapServer) the code uses another json field to get the identityfield. A drilldown test is niet possible in case of a query test because the url contains the number of the layer to test. Due to this the drilldown test gives an error. --- GeoHealthCheck/config_main.py | 6 +++--- GeoHealthCheck/enums.py | 7 ++----- GeoHealthCheck/healthcheck.py | 6 +++--- GeoHealthCheck/models.py | 2 +- GeoHealthCheck/plugins/probe/http.py | 2 +- GeoHealthCheck/probe.py | 1 + docs/plugins.rst | 6 +++--- tests/data/fixtures.json | 23 ++++++++++++++++++++--- 8 files changed, 34 insertions(+), 19 deletions(-) diff --git a/GeoHealthCheck/config_main.py b/GeoHealthCheck/config_main.py index 0a04b15..7bdd5a9 100644 --- a/GeoHealthCheck/config_main.py +++ b/GeoHealthCheck/config_main.py @@ -114,7 +114,7 @@ 'GeoHealthCheck.plugins.probe.wmsdrilldown', 'GeoHealthCheck.plugins.probe.ogcfeat', 'GeoHealthCheck.plugins.probe.ogc3dtiles', - 'GeoHealthCheck.plugins.probe.esrifs', + 'GeoHealthCheck.plugins.probe.esri', 'GeoHealthCheck.plugins.probe.ghcreport', 'GeoHealthCheck.plugins.probe.mapbox', @@ -166,8 +166,8 @@ 'OGC:3DTiles': { 'probe_class': 'GeoHealthCheck.plugins.probe.ogc3dtiles.OGC3DTiles' }, - 'ESRI:FS': { - 'probe_class': 'GeoHealthCheck.plugins.probe.esrifs.ESRIFSDrilldown' + 'ESRI': { + 'probe_class': 'GeoHealthCheck.plugins.probe.esri.ESRIDrilldown' }, 'Mapbox:TileJSON': { 'probe_class': 'GeoHealthCheck.plugins.probe.mapbox.TileJSON' diff --git a/GeoHealthCheck/enums.py b/GeoHealthCheck/enums.py index e05ec5a..1539bce 100644 --- a/GeoHealthCheck/enums.py +++ b/GeoHealthCheck/enums.py @@ -77,11 +77,8 @@ 'OGC:3DTiles': { 'label': 'OGC 3D Tiles (OGC3D)' }, - 'ESRI:FS': { - 'label': 'ESRI ArcGIS FeatureServer (FS)' - }, - 'ESRI:MS': { - 'label': 'ESRI ArcGIS MapServer (MS)' + 'ESRI': { + 'label': 'ESRI ArcGIS Server (Feature- or MapService)' }, 'Mapbox:TileJSON': { 'label': 'Mapbox TileJSON Service (TileJSON)' diff --git a/GeoHealthCheck/healthcheck.py b/GeoHealthCheck/healthcheck.py index 1451699..61b4d78 100644 --- a/GeoHealthCheck/healthcheck.py +++ b/GeoHealthCheck/healthcheck.py @@ -157,7 +157,7 @@ def sniff_test_resource(config, resource_type, url): 'OGC:SOS': [SensorObservationService], 'OGCFeat': [urlopen], 'OGC:3DTiles': [urlopen], - 'ESRI:FS': [urlopen], + 'ESRI': [urlopen], 'OGC:STA': [urlopen], 'WWW:LINK': [urlopen], 'FTP': [urlopen], @@ -243,8 +243,8 @@ def sniff_test_resource(config, resource_type, url): title = 'OGC STA' elif resource_type == 'OGCFeat': title = 'OGC API Features (OAFeat)' - elif resource_type == 'ESRI:FS': - title = 'ESRI ArcGIS FS' + elif resource_type == 'ESRI': + title = 'ESRI ArcGIS Service' elif resource_type == 'OGC:3DTiles': title = 'OGC 3D Tiles' else: diff --git a/GeoHealthCheck/models.py b/GeoHealthCheck/models.py index 2c09b51..102bd91 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', 'OGC:3DTiles']: url = '%s%s' % (bind_url(self.url), RESOURCE_TYPES[self.resource_type]['capabilities']) else: 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/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/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..b28b56f 100644 --- a/tests/data/fixtures.json +++ b/tests/data/fixtures.json @@ -110,13 +110,23 @@ }, "ESRI FEATURESERVER": { "owner": "admin", - "resource_type": "ESRI:FS", + "resource_type": "ESRI", "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", + "active": true, + "title": "ESRI ArcGIS MapServer (MS)", + "url": "https://sampleserver6.arcgisonline.com/arcgis/rest/services/AGP/USA/MapServer", + "tags": [ + "esri" + ] } }, "probe_vars": { @@ -288,11 +298,18 @@ }, "ESRIFS - Drilldown": { "resource": "ESRI FEATURESERVER", - "probe_class": "GeoHealthCheck.plugins.probe.esrifs.ESRIFSDrilldown", + "probe_class": "GeoHealthCheck.plugins.probe.esri.ESRIDrilldown", "parameters": { "drilldown_level": "full" } - } + }, + "ESRIMS - Drilldown": { + "resource": "ESRI MAPSERVER", + "probe_class": "GeoHealthCheck.plugins.probe.esri.ESRIDrilldown", + "parameters": { + "drilldown_level": "full" + } + } }, "check_vars": { "PDOK BAG WMS - GetCaps - XML Parse": { From 7e7b081eb70619f0219a6f5aabef1ade85ed1295 Mon Sep 17 00:00:00 2001 From: Luuk Date: Fri, 26 Jul 2024 13:15:07 +0200 Subject: [PATCH 3/5] Postgres, Oracle en ESRI MapService toegevoegd --- .gitignore | 1 + GeoHealthCheck/app.py | 3 +- GeoHealthCheck/config_main.py | 8 + GeoHealthCheck/enums.py | 6 + GeoHealthCheck/healthcheck.py | 21 +++ GeoHealthCheck/plugins/probe/esri.py | 201 +++++++++++++++++++++++ GeoHealthCheck/plugins/probe/oracle.py | 119 ++++++++++++++ GeoHealthCheck/plugins/probe/postgres.py | 120 ++++++++++++++ GeoHealthCheck/util.py | 4 + GeoHealthCheck/views.py | 7 +- 10 files changed, 486 insertions(+), 4 deletions(-) create mode 100644 GeoHealthCheck/plugins/probe/esri.py create mode 100644 GeoHealthCheck/plugins/probe/oracle.py create mode 100644 GeoHealthCheck/plugins/probe/postgres.py 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 7bdd5a9..c04b5b0 100644 --- a/GeoHealthCheck/config_main.py +++ b/GeoHealthCheck/config_main.py @@ -115,6 +115,8 @@ 'GeoHealthCheck.plugins.probe.ogcfeat', 'GeoHealthCheck.plugins.probe.ogc3dtiles', 'GeoHealthCheck.plugins.probe.esri', + 'GeoHealthCheck.plugins.probe.oracle', + 'GeoHealthCheck.plugins.probe.postgres', 'GeoHealthCheck.plugins.probe.ghcreport', 'GeoHealthCheck.plugins.probe.mapbox', @@ -166,6 +168,12 @@ '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': { 'probe_class': 'GeoHealthCheck.plugins.probe.esri.ESRIDrilldown' }, diff --git a/GeoHealthCheck/enums.py b/GeoHealthCheck/enums.py index 1539bce..6c83a14 100644 --- a/GeoHealthCheck/enums.py +++ b/GeoHealthCheck/enums.py @@ -92,6 +92,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 61b4d78..1f400b6 100644 --- a/GeoHealthCheck/healthcheck.py +++ b/GeoHealthCheck/healthcheck.py @@ -158,6 +158,8 @@ def sniff_test_resource(config, resource_type, url): 'OGCFeat': [urlopen], 'OGC:3DTiles': [urlopen], 'ESRI': [urlopen], + 'ORACLE': [oracle_connect], + 'POSTGRES': [postgres_connect], 'OGC:STA': [urlopen], 'WWW:LINK': [urlopen], 'FTP': [urlopen], @@ -309,6 +311,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/plugins/probe/esri.py b/GeoHealthCheck/plugins/probe/esri.py new file mode 100644 index 0000000..2272f9c --- /dev/null +++ b/GeoHealthCheck/plugins/probe/esri.py @@ -0,0 +1,201 @@ +from GeoHealthCheck.probe import Probe +from GeoHealthCheck.result import Result, push_result + +class ESRIDrilldown(Probe): + """ + Probe for ESRI Feature- or MapServer endpoint "drilldown": starting + with top /FeatureServer or /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 = 'ESRI Drilldown' + + DESCRIPTION = 'Traverses an ESRI Feature- or MapServer ' \ + '(REST) API endpoint by drilling down' + + RESOURCE_TYPE = 'ESRI' + + 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_esri_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://.../FeatureServer or http://.../MapServer + es_url = self._resource.url.split('?')[0] + + # Add service type to the url + if "FEATURESERVER" in es_url.upper(): + service_type = "FeatureServer" + elif "MAPSERVER" in es_url.upper(): + service_type = "MapServer" + else: + raise Exception("No FeatureServer of MapServer in url") + + # Assemble request templates with root FS URL + req_tpl = { + 'es_caps': es_url + '?f=json', + + 'layer_caps': es_url + '/%d?f=json', + + 'get_features': es_url + + '/%d/query?where=1=1' + '&outFields=*&resultOffset=0&' + 'resultRecordCount=1&f=json', + + 'get_feature_by_id': es_url + + "/%d/query?where=%s='%s'&resultRecordCount=1&outFields=*&f=json" + } + + # 1. Test top Service endpoint existence + result = Result(True, f"Test Service Endpoint (type={service_type})") + result.start() + layers = [] + try: + es_caps = self.perform_esri_get_request(req_tpl['es_caps']) + for attr in ['currentVersion', 'layers']: + val = es_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 = es_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_esri_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 Layers') + result.start() + layer_id = 0 + try: + for layer_id in layer_ids: + try: + features = self.perform_esri_get_request( + req_tpl['get_features'] % layer_id) + if service_type == 'FeatureServer': + obj_id_field_name = features['objectIdFieldName'] + elif service_type == 'MapServer': + obj_id_field_name = features['displayFieldName'] + 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/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=;port=;service=" 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=;port=;database=" 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/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) From 439da5646ad8f65e4de379ee7d28f4bf85b63736 Mon Sep 17 00:00:00 2001 From: Luuk Date: Fri, 9 Aug 2024 16:01:33 +0200 Subject: [PATCH 4/5] Next to the FeatureServer probe there is now also a MapServer probe. The esri probe was deleted. --- GeoHealthCheck/config_main.py | 10 +- GeoHealthCheck/enums.py | 7 +- GeoHealthCheck/healthcheck.py | 9 +- GeoHealthCheck/models.py | 2 +- GeoHealthCheck/plugins/probe/esri.py | 201 ------------------------- GeoHealthCheck/plugins/probe/esrims.py | 13 +- 6 files changed, 29 insertions(+), 213 deletions(-) delete mode 100644 GeoHealthCheck/plugins/probe/esri.py diff --git a/GeoHealthCheck/config_main.py b/GeoHealthCheck/config_main.py index c04b5b0..df45db7 100644 --- a/GeoHealthCheck/config_main.py +++ b/GeoHealthCheck/config_main.py @@ -114,7 +114,8 @@ 'GeoHealthCheck.plugins.probe.wmsdrilldown', 'GeoHealthCheck.plugins.probe.ogcfeat', 'GeoHealthCheck.plugins.probe.ogc3dtiles', - 'GeoHealthCheck.plugins.probe.esri', + 'GeoHealthCheck.plugins.probe.esrifs', + 'GeoHealthCheck.plugins.probe.esrims', 'GeoHealthCheck.plugins.probe.oracle', 'GeoHealthCheck.plugins.probe.postgres', 'GeoHealthCheck.plugins.probe.ghcreport', @@ -174,8 +175,11 @@ 'POSTGRES': { 'probe_class': 'GeoHealthCheck.plugins.probe.postgres.PostgresDrilldown' }, - 'ESRI': { - 'probe_class': 'GeoHealthCheck.plugins.probe.esri.ESRIDrilldown' + '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 6c83a14..d639e47 100644 --- a/GeoHealthCheck/enums.py +++ b/GeoHealthCheck/enums.py @@ -77,8 +77,11 @@ 'OGC:3DTiles': { 'label': 'OGC 3D Tiles (OGC3D)' }, - 'ESRI': { - 'label': 'ESRI ArcGIS Server (Feature- or MapService)' + 'ESRI:FS': { + 'label': 'ESRI ArcGIS FeatureServer' + }, + 'ESRI:MS': { + 'label': 'ESRI ArcGIS MapServer' }, 'Mapbox:TileJSON': { 'label': 'Mapbox TileJSON Service (TileJSON)' diff --git a/GeoHealthCheck/healthcheck.py b/GeoHealthCheck/healthcheck.py index 1f400b6..28be2bd 100644 --- a/GeoHealthCheck/healthcheck.py +++ b/GeoHealthCheck/healthcheck.py @@ -157,7 +157,8 @@ def sniff_test_resource(config, resource_type, url): 'OGC:SOS': [SensorObservationService], 'OGCFeat': [urlopen], 'OGC:3DTiles': [urlopen], - 'ESRI': [urlopen], + 'ESRI:FS': [urlopen], + 'ESRI:MS': [urlopen], 'ORACLE': [oracle_connect], 'POSTGRES': [postgres_connect], 'OGC:STA': [urlopen], @@ -245,8 +246,10 @@ def sniff_test_resource(config, resource_type, url): title = 'OGC STA' elif resource_type == 'OGCFeat': title = 'OGC API Features (OAFeat)' - elif resource_type == 'ESRI': - title = 'ESRI ArcGIS Service' + elif resource_type == 'ESRI:FS': + title = 'ESRI ArcGIS FeatureService' + elif resource_type == 'ESRI:MS': + title = 'ESRI ArcGIS MapService' elif resource_type == 'OGC:3DTiles': title = 'OGC 3D Tiles' else: diff --git a/GeoHealthCheck/models.py b/GeoHealthCheck/models.py index 102bd91..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', '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/esri.py b/GeoHealthCheck/plugins/probe/esri.py deleted file mode 100644 index 2272f9c..0000000 --- a/GeoHealthCheck/plugins/probe/esri.py +++ /dev/null @@ -1,201 +0,0 @@ -from GeoHealthCheck.probe import Probe -from GeoHealthCheck.result import Result, push_result - -class ESRIDrilldown(Probe): - """ - Probe for ESRI Feature- or MapServer endpoint "drilldown": starting - with top /FeatureServer or /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 = 'ESRI Drilldown' - - DESCRIPTION = 'Traverses an ESRI Feature- or MapServer ' \ - '(REST) API endpoint by drilling down' - - RESOURCE_TYPE = 'ESRI' - - 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_esri_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://.../FeatureServer or http://.../MapServer - es_url = self._resource.url.split('?')[0] - - # Add service type to the url - if "FEATURESERVER" in es_url.upper(): - service_type = "FeatureServer" - elif "MAPSERVER" in es_url.upper(): - service_type = "MapServer" - else: - raise Exception("No FeatureServer of MapServer in url") - - # Assemble request templates with root FS URL - req_tpl = { - 'es_caps': es_url + '?f=json', - - 'layer_caps': es_url + '/%d?f=json', - - 'get_features': es_url + - '/%d/query?where=1=1' - '&outFields=*&resultOffset=0&' - 'resultRecordCount=1&f=json', - - 'get_feature_by_id': es_url + - "/%d/query?where=%s='%s'&resultRecordCount=1&outFields=*&f=json" - } - - # 1. Test top Service endpoint existence - result = Result(True, f"Test Service Endpoint (type={service_type})") - result.start() - layers = [] - try: - es_caps = self.perform_esri_get_request(req_tpl['es_caps']) - for attr in ['currentVersion', 'layers']: - val = es_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 = es_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_esri_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 Layers') - result.start() - layer_id = 0 - try: - for layer_id in layer_ids: - try: - features = self.perform_esri_get_request( - req_tpl['get_features'] % layer_id) - if service_type == 'FeatureServer': - obj_id_field_name = features['objectIdFieldName'] - elif service_type == 'MapServer': - obj_id_field_name = features['displayFieldName'] - 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/esrims.py b/GeoHealthCheck/plugins/probe/esrims.py index d2f74b7..754ea47 100644 --- a/GeoHealthCheck/plugins/probe/esrims.py +++ b/GeoHealthCheck/plugins/probe/esrims.py @@ -147,16 +147,23 @@ def perform_request(self): # ASSERTION: will do full drilldown from here # 3. Test getting Features from Layers - result = Result(True, 'Test 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) - obj_id_field_name = "OBJECTID" #features['objectIdFieldName'] + # 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 From 9aa5194913aa8a3cf08b8208a982a82d7aa8abbf Mon Sep 17 00:00:00 2001 From: Luuk Date: Fri, 9 Aug 2024 16:39:50 +0200 Subject: [PATCH 5/5] Added/Changed tests for ArcGIS Server for both the FeatureServer (ESRI:FS) and the Mapserver (ESRI:MS) --- tests/data/fixtures.json | 8 ++++---- tests/data/resources.json | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/tests/data/fixtures.json b/tests/data/fixtures.json index b28b56f..b621107 100644 --- a/tests/data/fixtures.json +++ b/tests/data/fixtures.json @@ -110,7 +110,7 @@ }, "ESRI FEATURESERVER": { "owner": "admin", - "resource_type": "ESRI", + "resource_type": "ESRI:FS", "active": true, "title": "ESRI ArcGIS FeatureServer (FS)", "url": "https://sampleserver6.arcgisonline.com/arcgis/rest/services/Wildfire/FeatureServer", @@ -120,7 +120,7 @@ }, "ESRI MAPSERVER": { "owner": "admin", - "resource_type": "ESRI", + "resource_type": "ESRI:MS", "active": true, "title": "ESRI ArcGIS MapServer (MS)", "url": "https://sampleserver6.arcgisonline.com/arcgis/rest/services/AGP/USA/MapServer", @@ -298,14 +298,14 @@ }, "ESRIFS - Drilldown": { "resource": "ESRI FEATURESERVER", - "probe_class": "GeoHealthCheck.plugins.probe.esri.ESRIDrilldown", + "probe_class": "GeoHealthCheck.plugins.probe.esrifs.ESRIFSDrilldown", "parameters": { "drilldown_level": "full" } }, "ESRIMS - Drilldown": { "resource": "ESRI MAPSERVER", - "probe_class": "GeoHealthCheck.plugins.probe.esri.ESRIDrilldown", + "probe_class": "GeoHealthCheck.plugins.probe.esrims.ESRIMSDrilldown", "parameters": { "drilldown_level": "full" } 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": {