diff --git a/docs/src/pages/docs/Miscellaneous/issue_codes.mdx b/docs/src/pages/docs/Miscellaneous/issue_codes.mdx index 37536ff5450..baf7422b9ff 100644 --- a/docs/src/pages/docs/Miscellaneous/issue_codes.mdx +++ b/docs/src/pages/docs/Miscellaneous/issue_codes.mdx @@ -147,7 +147,8 @@ that the username is typed correctly and exists in the database. The password provided when connecting to a database is not valid. ``` -The user provided a password that is incorrect. Please check that the password is typed correctly. +The user provided a password that is incorrect. Please check that the +password is typed correctly. ## Issue 1014 @@ -155,5 +156,5 @@ The user provided a password that is incorrect. Please check that the password i Either the username or the password used are incorrect. ``` -Either the username provided does not exist or the password was written incorrectly. Please -check that the username and password were typed correctly. +Either the username provided does not exist or the password was written +incorrectly. Please check that the username and password were typed correctly. diff --git a/superset/databases/commands/test_connection.py b/superset/databases/commands/test_connection.py index 7c2ce6fcf23..4f4b915f067 100644 --- a/superset/databases/commands/test_connection.py +++ b/superset/databases/commands/test_connection.py @@ -20,6 +20,7 @@ from typing import Any, Dict, Optional from flask_appbuilder.security.sqla.models import User from flask_babel import gettext as _ +from sqlalchemy.engine.url import make_url from sqlalchemy.exc import DBAPIError, NoSuchModuleError from superset.commands.base import BaseCommand @@ -86,7 +87,14 @@ class TestConnectionDatabaseCommand(BaseCommand): engine=database.db_engine_spec.__name__, ) # check for custom errors (wrong username, wrong password, etc) - errors = database.db_engine_spec.extract_errors(ex) + url = make_url(uri) + context = { + "hostname": url.host, + "password": url.password, + "port": url.port, + "username": url.username, + } + errors = database.db_engine_spec.extract_errors(ex, context) raise DatabaseTestConnectionFailedError(errors) except SupersetSecurityException as ex: event_logger.log_with_context( diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py index d92a0eb604d..5cdb2a03070 100644 --- a/superset/db_engine_specs/base.py +++ b/superset/db_engine_specs/base.py @@ -746,16 +746,20 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods return utils.error_msg_from_exception(ex) @classmethod - def extract_errors(cls, ex: Exception) -> List[SupersetError]: + def extract_errors( + cls, ex: Exception, context: Optional[Dict[str, Any]] = None + ) -> List[SupersetError]: raw_message = cls._extract_error_message(ex) + context = context or {} for regex, (message, error_type) in cls.custom_errors.items(): match = regex.search(raw_message) if match: + params = {**context, **match.groupdict()} return [ SupersetError( error_type=error_type, - message=message % match.groupdict(), + message=message % params, level=ErrorLevel.ERROR, extra={"engine_name": cls.engine_name}, ) diff --git a/superset/db_engine_specs/mssql.py b/superset/db_engine_specs/mssql.py index 67b9ec1b62d..51873ced349 100644 --- a/superset/db_engine_specs/mssql.py +++ b/superset/db_engine_specs/mssql.py @@ -15,15 +15,33 @@ # specific language governing permissions and limitations # under the License. import logging +import re from datetime import datetime from typing import Any, List, Optional, Tuple +from flask_babel import gettext as __ + from superset.db_engine_specs.base import BaseEngineSpec, LimitMethod +from superset.errors import SupersetErrorType from superset.utils import core as utils logger = logging.getLogger(__name__) +# Regular expressions to catch custom errors +TEST_CONNECTION_ACCESS_DENIED_REGEX = re.compile("Adaptive Server connection failed") +TEST_CONNECTION_INVALID_HOSTNAME_REGEX = re.compile( + r"Adaptive Server is unavailable or does not exist \((?P.*?)\)" + "(?!.*Net-Lib error).*$" +) +TEST_CONNECTION_PORT_CLOSED_REGEX = re.compile( + r"Net-Lib error during Connection refused \(61\)" +) +TEST_CONNECTION_HOST_DOWN_REGEX = re.compile( + r"Net-Lib error during Operation timed out \(60\)" +) + + class MssqlEngineSpec(BaseEngineSpec): engine = "mssql" engine_name = "Microsoft SQL" @@ -46,6 +64,28 @@ class MssqlEngineSpec(BaseEngineSpec): "P1Y": "DATEADD(year, DATEDIFF(year, 0, {col}), 0)", } + custom_errors = { + TEST_CONNECTION_ACCESS_DENIED_REGEX: ( + __('Either the username "%(username)s" or the password is incorrect.'), + SupersetErrorType.TEST_CONNECTION_ACCESS_DENIED_ERROR, + ), + TEST_CONNECTION_INVALID_HOSTNAME_REGEX: ( + __('The hostname "%(hostname)s" cannot be resolved.'), + SupersetErrorType.TEST_CONNECTION_INVALID_HOSTNAME_ERROR, + ), + TEST_CONNECTION_PORT_CLOSED_REGEX: ( + __('Port %(port)s on hostname "%(hostname)s" refused the connection.'), + SupersetErrorType.TEST_CONNECTION_PORT_CLOSED_ERROR, + ), + TEST_CONNECTION_HOST_DOWN_REGEX: ( + __( + 'The host "%(hostname)s" might be down, and can\'t be ' + "reached on port %(port)s." + ), + SupersetErrorType.TEST_CONNECTION_HOST_DOWN_ERROR, + ), + } + @classmethod def epoch_to_dttm(cls) -> str: return "dateadd(S, {col}, '1970-01-01')" diff --git a/superset/db_engine_specs/postgres.py b/superset/db_engine_specs/postgres.py index 15615b1133e..59effff0cd7 100644 --- a/superset/db_engine_specs/postgres.py +++ b/superset/db_engine_specs/postgres.py @@ -56,20 +56,22 @@ class FixedOffsetTimezone(_FixedOffset): # Regular expressions to catch custom errors -INVALID_USERNAME_REGEX = re.compile('role "(?P.*?)" does not exist') -INVALID_PASSWORD_REGEX = re.compile( +TEST_CONNECTION_INVALID_USERNAME_REGEX = re.compile( + 'role "(?P.*?)" does not exist' +) +TEST_CONNECTION_INVALID_PASSWORD_REGEX = re.compile( 'password authentication failed for user "(?P.*?)"' ) -INVALID_HOSTNAME_REGEX = re.compile( +TEST_CONNECTION_INVALID_HOSTNAME_REGEX = re.compile( 'could not translate host name "(?P.*?)" to address: ' "nodename nor servname provided, or not known" ) -CONNECTION_PORT_CLOSED_REGEX = re.compile( +TEST_CONNECTION_PORT_CLOSED_REGEX = re.compile( r"could not connect to server: Connection refused\s+Is the server " r'running on host "(?P.*?)" (\(.*?\) )?and accepting\s+TCP/IP ' r"connections on port (?P.*?)\?" ) -CONNECTION_HOST_DOWN_REGEX = re.compile( +TEST_CONNECTION_HOST_DOWN_REGEX = re.compile( r"could not connect to server: (?P.*?)\s+Is the server running on " r'host "(?P.*?)" (\(.*?\) )?and accepting\s+TCP/IP ' r"connections on port (?P.*?)\?" @@ -95,26 +97,26 @@ class PostgresBaseEngineSpec(BaseEngineSpec): } custom_errors = { - INVALID_USERNAME_REGEX: ( + TEST_CONNECTION_INVALID_USERNAME_REGEX: ( __('The username "%(username)s" does not exist.'), SupersetErrorType.TEST_CONNECTION_INVALID_USERNAME_ERROR, ), - INVALID_PASSWORD_REGEX: ( + TEST_CONNECTION_INVALID_PASSWORD_REGEX: ( __('The password provided for username "%(username)s" is incorrect.'), SupersetErrorType.TEST_CONNECTION_INVALID_PASSWORD_ERROR, ), - INVALID_HOSTNAME_REGEX: ( + TEST_CONNECTION_INVALID_HOSTNAME_REGEX: ( __('The hostname "%(hostname)s" cannot be resolved.'), SupersetErrorType.TEST_CONNECTION_INVALID_HOSTNAME_ERROR, ), - CONNECTION_PORT_CLOSED_REGEX: ( - __("Port %(port)s on hostname %(hostname)s refused the connection."), + TEST_CONNECTION_PORT_CLOSED_REGEX: ( + __('Port %(port)s on hostname "%(hostname)s" refused the connection.'), SupersetErrorType.TEST_CONNECTION_PORT_CLOSED_ERROR, ), - CONNECTION_HOST_DOWN_REGEX: ( + TEST_CONNECTION_HOST_DOWN_REGEX: ( __( - "The host %(hostname)s might be down, and can't be " - "reached on port %(port)s" + 'The host "%(hostname)s" might be down, and can\'t be ' + "reached on port %(port)s." ), SupersetErrorType.TEST_CONNECTION_HOST_DOWN_ERROR, ), diff --git a/superset/db_engine_specs/presto.py b/superset/db_engine_specs/presto.py index bb4eefc6e46..a62dd0ecfa7 100644 --- a/superset/db_engine_specs/presto.py +++ b/superset/db_engine_specs/presto.py @@ -1132,7 +1132,9 @@ class PrestoEngineSpec(BaseEngineSpec): # pylint: disable=too-many-public-metho return database.get_df("SHOW FUNCTIONS")["Function"].tolist() @classmethod - def extract_errors(cls, ex: Exception) -> List[SupersetError]: + def extract_errors( + cls, ex: Exception, context: Optional[Dict[str, Any]] = None + ) -> List[SupersetError]: raw_message = cls._extract_error_message(ex) column_match = re.search(COLUMN_NOT_RESOLVED_ERROR_REGEX, raw_message) @@ -1166,7 +1168,7 @@ class PrestoEngineSpec(BaseEngineSpec): # pylint: disable=too-many-public-metho ) ] - return super().extract_errors(ex) + return super().extract_errors(ex, context) @classmethod def is_readonly_query(cls, parsed_query: ParsedQuery) -> bool: diff --git a/tests/db_engine_specs/mssql_tests.py b/tests/db_engine_specs/mssql_tests.py index 74c3715f28a..cfc75cf09e8 100644 --- a/tests/db_engine_specs/mssql_tests.py +++ b/tests/db_engine_specs/mssql_tests.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. import unittest.mock as mock +from textwrap import dedent from sqlalchemy import column, table from sqlalchemy.dialects import mssql @@ -24,6 +25,7 @@ from sqlalchemy.types import String, UnicodeText from superset.db_engine_specs.base import BaseEngineSpec from superset.db_engine_specs.mssql import MssqlEngineSpec +from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.utils.core import GenericDataType from tests.db_engine_specs.base_tests import TestDbEngineSpec @@ -149,3 +151,154 @@ class TestMssqlEngineSpec(TestDbEngineSpec): original, mssql.dialect() ) self.assertEqual(actual, expected) + + def test_extract_errors(self): + """ + Test that custom error messages are extracted correctly. + """ + msg = dedent( + """ +DB-Lib error message 20009, severity 9: +Unable to connect: Adaptive Server is unavailable or does not exist (locahost) + """ + ) + result = MssqlEngineSpec.extract_errors(Exception(msg)) + assert result == [ + SupersetError( + error_type=SupersetErrorType.TEST_CONNECTION_INVALID_HOSTNAME_ERROR, + message='The hostname "locahost" cannot be resolved.', + level=ErrorLevel.ERROR, + extra={ + "engine_name": "Microsoft SQL", + "issue_codes": [ + { + "code": 1007, + "message": "Issue 1007 - The hostname provided can't be resolved.", + } + ], + }, + ) + ] + + msg = dedent( + """ +DB-Lib error message 20009, severity 9: +Unable to connect: Adaptive Server is unavailable or does not exist (localhost) +Net-Lib error during Connection refused (61) +DB-Lib error message 20009, severity 9: +Unable to connect: Adaptive Server is unavailable or does not exist (localhost) +Net-Lib error during Connection refused (61) + """ + ) + result = MssqlEngineSpec.extract_errors( + Exception(msg), context={"port": 12345, "hostname": "localhost"} + ) + assert result == [ + SupersetError( + error_type=SupersetErrorType.TEST_CONNECTION_PORT_CLOSED_ERROR, + message='Port 12345 on hostname "localhost" refused the connection.', + level=ErrorLevel.ERROR, + extra={ + "engine_name": "Microsoft SQL", + "issue_codes": [ + {"code": 1008, "message": "Issue 1008 - The port is closed."} + ], + }, + ) + ] + + msg = dedent( + """ +DB-Lib error message 20009, severity 9: +Unable to connect: Adaptive Server is unavailable or does not exist (example.com) +Net-Lib error during Operation timed out (60) +DB-Lib error message 20009, severity 9: +Unable to connect: Adaptive Server is unavailable or does not exist (example.com) +Net-Lib error during Operation timed out (60) + """ + ) + result = MssqlEngineSpec.extract_errors( + Exception(msg), context={"port": 12345, "hostname": "example.com"} + ) + assert result == [ + SupersetError( + error_type=SupersetErrorType.TEST_CONNECTION_HOST_DOWN_ERROR, + message=( + 'The host "example.com" might be down, ' + "and can't be reached on port 12345." + ), + level=ErrorLevel.ERROR, + extra={ + "engine_name": "Microsoft SQL", + "issue_codes": [ + { + "code": 1009, + "message": "Issue 1009 - The host might be down, and can't be reached on the provided port.", + } + ], + }, + ) + ] + + msg = dedent( + """ +DB-Lib error message 20009, severity 9: +Unable to connect: Adaptive Server is unavailable or does not exist (93.184.216.34) +Net-Lib error during Operation timed out (60) +DB-Lib error message 20009, severity 9: +Unable to connect: Adaptive Server is unavailable or does not exist (93.184.216.34) +Net-Lib error during Operation timed out (60) + """ + ) + result = MssqlEngineSpec.extract_errors( + Exception(msg), context={"port": 12345, "hostname": "93.184.216.34"} + ) + assert result == [ + SupersetError( + error_type=SupersetErrorType.TEST_CONNECTION_HOST_DOWN_ERROR, + message=( + 'The host "93.184.216.34" might be down, ' + "and can't be reached on port 12345." + ), + level=ErrorLevel.ERROR, + extra={ + "engine_name": "Microsoft SQL", + "issue_codes": [ + { + "code": 1009, + "message": "Issue 1009 - The host might be down, and can't be reached on the provided port.", + } + ], + }, + ) + ] + + msg = dedent( + """ +DB-Lib error message 20018, severity 14: +General SQL Server error: Check messages from the SQL Server +DB-Lib error message 20002, severity 9: +Adaptive Server connection failed (mssqldb.cxiotftzsypc.us-west-2.rds.amazonaws.com) +DB-Lib error message 20002, severity 9: +Adaptive Server connection failed (mssqldb.cxiotftzsypc.us-west-2.rds.amazonaws.com) + """ + ) + result = MssqlEngineSpec.extract_errors( + Exception(msg), context={"username": "testuser"} + ) + assert result == [ + SupersetError( + message='Either the username "testuser" or the password is incorrect.', + error_type=SupersetErrorType.TEST_CONNECTION_ACCESS_DENIED_ERROR, + level=ErrorLevel.ERROR, + extra={ + "engine_name": "Microsoft SQL", + "issue_codes": [ + { + "code": 1014, + "message": "Issue 1014 - Either the username or the password is wrong.", + } + ], + }, + ) + ] diff --git a/tests/db_engine_specs/postgres_tests.py b/tests/db_engine_specs/postgres_tests.py index aae14e8d66e..867faa1265f 100644 --- a/tests/db_engine_specs/postgres_tests.py +++ b/tests/db_engine_specs/postgres_tests.py @@ -223,7 +223,18 @@ class TestPostgresDbEngineSpec(TestDbEngineSpec): error_type=SupersetErrorType.TEST_CONNECTION_INVALID_USERNAME_ERROR, message='The username "testuser" does not exist.', level=ErrorLevel.ERROR, - extra={"engine_name": "PostgreSQL"}, + extra={ + "engine_name": "PostgreSQL", + "issue_codes": [ + { + "code": 1012, + "message": ( + "Issue 1012 - The username provided when " + "connecting to a database is not valid." + ), + }, + ], + }, ) ] @@ -234,7 +245,15 @@ class TestPostgresDbEngineSpec(TestDbEngineSpec): error_type=SupersetErrorType.TEST_CONNECTION_INVALID_HOSTNAME_ERROR, message='The hostname "locahost" cannot be resolved.', level=ErrorLevel.ERROR, - extra={"engine_name": "PostgreSQL"}, + extra={ + "engine_name": "PostgreSQL", + "issue_codes": [ + { + "code": 1007, + "message": "Issue 1007 - The hostname provided can't be resolved.", + } + ], + }, ) ] @@ -252,9 +271,14 @@ could not connect to server: Connection refused assert result == [ SupersetError( error_type=SupersetErrorType.TEST_CONNECTION_PORT_CLOSED_ERROR, - message="Port 12345 on hostname localhost refused the connection.", + message='Port 12345 on hostname "localhost" refused the connection.', level=ErrorLevel.ERROR, - extra={"engine_name": "PostgreSQL"}, + extra={ + "engine_name": "PostgreSQL", + "issue_codes": [ + {"code": 1008, "message": "Issue 1008 - The port is closed."} + ], + }, ) ] @@ -270,11 +294,19 @@ psql: error: could not connect to server: Operation timed out SupersetError( error_type=SupersetErrorType.TEST_CONNECTION_HOST_DOWN_ERROR, message=( - "The host example.com might be down, " - "and can't be reached on port 12345" + 'The host "example.com" might be down, ' + "and can't be reached on port 12345." ), level=ErrorLevel.ERROR, - extra={"engine_name": "PostgreSQL"}, + extra={ + "engine_name": "PostgreSQL", + "issue_codes": [ + { + "code": 1009, + "message": "Issue 1009 - The host might be down, and can't be reached on the provided port.", + } + ], + }, ) ] @@ -291,11 +323,19 @@ psql: error: could not connect to server: Operation timed out SupersetError( error_type=SupersetErrorType.TEST_CONNECTION_HOST_DOWN_ERROR, message=( - "The host 93.184.216.34 might be down, " - "and can't be reached on port 12345" + 'The host "93.184.216.34" might be down, ' + "and can't be reached on port 12345." ), level=ErrorLevel.ERROR, - extra={"engine_name": "PostgreSQL"}, + extra={ + "engine_name": "PostgreSQL", + "issue_codes": [ + { + "code": 1009, + "message": "Issue 1009 - The host might be down, and can't be reached on the provided port.", + } + ], + }, ) ] @@ -306,6 +346,17 @@ psql: error: could not connect to server: Operation timed out error_type=SupersetErrorType.TEST_CONNECTION_INVALID_PASSWORD_ERROR, message=('The password provided for username "postgres" is incorrect.'), level=ErrorLevel.ERROR, - extra={"engine_name": "PostgreSQL"}, + extra={ + "engine_name": "PostgreSQL", + "issue_codes": [ + { + "code": 1013, + "message": ( + "Issue 1013 - The password provided when " + "connecting to a database is not valid." + ), + }, + ], + }, ) ]