Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: error messages when connecting to MSSQL #14093

Merged
merged 2 commits into from
Apr 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/src/pages/docs/Miscellaneous/issue_codes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,14 @@ 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

```
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.
10 changes: 9 additions & 1 deletion superset/databases/commands/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

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
Expand Down Expand Up @@ -86,7 +87,14 @@ def run(self) -> None:
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(
Expand Down
8 changes: 6 additions & 2 deletions superset/db_engine_specs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,16 +746,20 @@ def _extract_error_message(cls, ex: Exception) -> str:
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},
)
Expand Down
40 changes: 40 additions & 0 deletions superset/db_engine_specs/mssql.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we only going to use these errors for test connection, or will we be using them for database creation as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use them for both. Anytime we have a DBAPI exception we can call extract_errors to get these errors.

r"Net-Lib error during Operation timed out \(60\)"
)


class MssqlEngineSpec(BaseEngineSpec):
engine = "mssql"
engine_name = "Microsoft SQL"
Expand All @@ -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')"
Expand Down
28 changes: 15 additions & 13 deletions superset/db_engine_specs/postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -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>.*?)\?"
Expand All @@ -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,
),
Expand Down
6 changes: 4 additions & 2 deletions superset/db_engine_specs/presto.py
Original file line number Diff line number Diff line change
Expand Up @@ -1132,7 +1132,9 @@ def get_function_names(cls, database: "Database") -> List[str]:
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)
Expand Down Expand Up @@ -1166,7 +1168,7 @@ def extract_errors(cls, ex: Exception) -> List[SupersetError]:
)
]

return super().extract_errors(ex)
return super().extract_errors(ex, context)

@classmethod
def is_readonly_query(cls, parsed_query: ParsedQuery) -> bool:
Expand Down
153 changes: 153 additions & 0 deletions tests/db_engine_specs/mssql_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,6 +25,7 @@

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

Expand Down Expand Up @@ -149,3 +151,154 @@ def test_column_datatype_to_string(self):
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.",
}
],
},
)
]
Loading