feat: error messages when connecting to MSSQL (#14093)

* feat: error messages when connecting to MSSQL

* Address comments
This commit is contained in:
Beto Dealmeida 2021-04-14 10:57:58 -07:00 committed by GitHub
parent ef1f048d81
commit 21f973f0bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 293 additions and 32 deletions

View File

@ -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.

View File

@ -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(

View File

@ -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},
)

View File

@ -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<hostname>.*?)\)"
"(?!.*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')"

View File

@ -56,20 +56,22 @@ class FixedOffsetTimezone(_FixedOffset):
# Regular expressions to catch custom errors
INVALID_USERNAME_REGEX = re.compile('role "(?P<username>.*?)" does not exist')
INVALID_PASSWORD_REGEX = re.compile(
TEST_CONNECTION_INVALID_USERNAME_REGEX = re.compile(
'role "(?P<username>.*?)" does not exist'
)
TEST_CONNECTION_INVALID_PASSWORD_REGEX = re.compile(
'password authentication failed for user "(?P<username>.*?)"'
)
INVALID_HOSTNAME_REGEX = re.compile(
TEST_CONNECTION_INVALID_HOSTNAME_REGEX = re.compile(
'could not translate host name "(?P<hostname>.*?)" 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<hostname>.*?)" (\(.*?\) )?and accepting\s+TCP/IP '
r"connections on port (?P<port>.*?)\?"
)
CONNECTION_HOST_DOWN_REGEX = re.compile(
TEST_CONNECTION_HOST_DOWN_REGEX = re.compile(
r"could not connect to server: (?P<reason>.*?)\s+Is the server running on "
r'host "(?P<hostname>.*?)" (\(.*?\) )?and accepting\s+TCP/IP '
r"connections on port (?P<port>.*?)\?"
@ -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,
),

View File

@ -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:

View File

@ -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.",
}
],
},
)
]

View File

@ -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."
),
},
],
},
)
]