diff --git a/sqlserver/changelog.d/18586.fixed b/sqlserver/changelog.d/18586.fixed new file mode 100644 index 0000000000000..e6177056b7c48 --- /dev/null +++ b/sqlserver/changelog.d/18586.fixed @@ -0,0 +1 @@ +[sqlserver] Fix ODBC config handling for Linux diff --git a/sqlserver/datadog_checks/sqlserver/utils.py b/sqlserver/datadog_checks/sqlserver/utils.py index c9b207320de2a..98825e804c316 100644 --- a/sqlserver/datadog_checks/sqlserver/utils.py +++ b/sqlserver/datadog_checks/sqlserver/utils.py @@ -3,6 +3,7 @@ # Licensed under a 3-clause BSD style license (see LICENSE) import os import re +import shutil import sys from typing import Dict @@ -11,6 +12,7 @@ CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) DRIVER_CONFIG_DIR = os.path.join(CURRENT_DIR, 'data', 'driver_config') +ODBC_INST_INI = 'odbcinst.ini' # Database is used to store both the name and physical_database_name @@ -32,6 +34,22 @@ def __str__(self): return "name:{}, physical_db_name:{}".format(self.name, self.physical_db_name) +def get_unixodbc_sysconfig(python_executable): + return os.path.join(os.path.dirname(os.path.dirname(python_executable)), "etc") + + +def is_non_empty_file(path): + if not os.path.exists(path): + return False + try: + if os.path.getsize(path) > 0: + return True + # exists and getsize aren't atomic + except FileNotFoundError: + return False + return False + + def set_default_driver_conf(): if Platform.is_containerized(): # Use default `./driver_config/odbcinst.ini` when Agent is running in docker. @@ -54,13 +72,16 @@ def set_default_driver_conf(): # linux_unixodbc_sysconfig is set to the agent embedded /etc directory # this is a hacky way to get the path to the etc directory # by getting the path to the python executable and get the directory above /bin/python - linux_unixodbc_sysconfig = os.path.dirname(os.path.dirname(sys.executable)) - if os.path.exists(os.path.join(linux_unixodbc_sysconfig, 'odbcinst.ini')) or os.path.exists( - os.path.join(linux_unixodbc_sysconfig, 'odbc.ini') - ): - # If there are already drivers or dataSources installed, don't override the ODBCSYSINI - # This means user has copied odbcinst.ini and odbc.ini to the unixODBC sysconfig location - return + linux_unixodbc_sysconfig = get_unixodbc_sysconfig(sys.executable) + odbc_ini = os.path.join(linux_unixodbc_sysconfig, 'odbc.ini') + if is_non_empty_file(odbc_ini): + os.environ.setdefault('ODBCSYSINI', linux_unixodbc_sysconfig) + odbc_inst_ini_sysconfig = os.path.join(linux_unixodbc_sysconfig, ODBC_INST_INI) + if not is_non_empty_file(odbc_inst_ini_sysconfig): + shutil.copy(os.path.join(DRIVER_CONFIG_DIR, ODBC_INST_INI), odbc_inst_ini_sysconfig) + # If there are already drivers or dataSources installed, don't override the ODBCSYSINI + # This means user has copied odbcinst.ini and odbc.ini to the unixODBC sysconfig location + return # Use default `./driver_config/odbcinst.ini` to let the integration use agent embedded odbc driver. os.environ.setdefault('ODBCSYSINI', DRIVER_CONFIG_DIR) diff --git a/sqlserver/tests/test_unit.py b/sqlserver/tests/test_unit.py index 9d46734f1d95f..4aadfa462af2f 100644 --- a/sqlserver/tests/test_unit.py +++ b/sqlserver/tests/test_unit.py @@ -20,12 +20,14 @@ from datadog_checks.sqlserver.utils import ( Database, extract_sql_comments_and_procedure_name, + get_unixodbc_sysconfig, + is_non_empty_file, parse_sqlserver_major_version, set_default_driver_conf, ) from .common import CHECK_NAME, DOCKER_SERVER, assert_metrics -from .utils import deep_compare, windows_ci +from .utils import deep_compare, not_windows_ci, windows_ci try: import pyodbc @@ -435,6 +437,12 @@ def test_set_default_driver_conf(): set_default_driver_conf() assert os.environ['ODBCSYSINI'].endswith(os.path.join('data', 'driver_config')) + with mock.patch("datadog_checks.base.utils.platform.Platform.is_linux", return_value=True): + with EnvVars({}, ignore=['ODBCSYSINI']): + set_default_driver_conf() + assert 'ODBCSYSINI' in os.environ, "ODBCSYSINI should be set" + assert os.environ['ODBCSYSINI'].endswith(os.path.join('data', 'driver_config')) + # `set_default_driver_conf` have no effect on the cases below with EnvVars({'ODBCSYSINI': 'ABC', 'DOCKER_DD_AGENT': 'true'}): set_default_driver_conf() @@ -446,23 +454,27 @@ def test_set_default_driver_conf(): assert 'ODBCSYSINI' in os.environ assert os.environ['ODBCSYSINI'].endswith(os.path.join('tests', 'odbc')) - with EnvVars({}, ignore=['ODBCSYSINI']): - with mock.patch("os.path.exists", return_value=True): - # odbcinst.ini or odbc.ini exists in agent embedded directory - set_default_driver_conf() - assert 'ODBCSYSINI' not in os.environ - - with EnvVars({}, ignore=['ODBCSYSINI']): - set_default_driver_conf() - assert 'ODBCSYSINI' in os.environ # ODBCSYSINI is set by the integration - if pyodbc is not None: - assert pyodbc.drivers() is not None - with EnvVars({'ODBCSYSINI': 'ABC'}): set_default_driver_conf() assert os.environ['ODBCSYSINI'] == 'ABC' +@not_windows_ci +def test_set_default_driver_conf_linux(): + odbc_config_dir = os.path.expanduser('~') + with mock.patch("datadog_checks.sqlserver.utils.get_unixodbc_sysconfig", return_value=odbc_config_dir): + with EnvVars({}, ignore=['ODBCSYSINI']): + odbc_inst = os.path.join(odbc_config_dir, "odbcinst.ini") + odbc_ini = os.path.join(odbc_config_dir, "odbc.ini") + for file in [odbc_inst, odbc_ini]: + if os.path.exists(file): + os.remove(file) + with open(odbc_ini, "x") as file: + file.write("dummy-content") + set_default_driver_conf() + assert is_non_empty_file(odbc_inst), "odbc_inst should have been created when a non empty odbc.ini exists" + + @windows_ci def test_check_local(aggregator, dd_run_check, init_config, instance_docker): sqlserver_check = SQLServer(CHECK_NAME, init_config, [instance_docker]) @@ -866,3 +878,16 @@ def test_exception_handling_by_do_for_dbs(instance_docker): 'datadog_checks.sqlserver.utils.is_azure_sql_database', return_value={} ): schemas._fetch_for_databases() + + +def test_get_unixodbc_sysconfig(): + etc_dir = os.path.sep + for dir in ["opt", "datadog-agent", "embedded", "bin", "python"]: + etc_dir = os.path.join(etc_dir, dir) + assert get_unixodbc_sysconfig(etc_dir).split(os.path.sep) == [ + "", + "opt", + "datadog-agent", + "embedded", + "etc", + ], "incorrect unix odbc config dir"