mirror of https://github.com/apache/superset.git
feat: error messages when connecting to MSSQL (#14093)
* feat: error messages when connecting to MSSQL * Address comments
This commit is contained in:
parent
ef1f048d81
commit
21f973f0bd
|
@ -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.
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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},
|
||||
)
|
||||
|
|
|
@ -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')"
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
]
|
||||
|
|
|
@ -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."
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue