Skip to content

Commit

Permalink
[sqlserver] Fix odbc.ini config handling for Linux (SDBM-1171) (#18586)
Browse files Browse the repository at this point in the history
* func for get embed dir

* [sqlserver] Fix ODBC config handling for Linux

* changelog

* linter

* test only for Linux

* linter

* import error

* non-atomic getsize

* refactoring file test

* linter

* fix os

* fix tests

* fix test

* test case for odbc.inst creation

* linter

* linter

* fix test_get_unixodbc_sysconfig

* test_linux_connection

* linter

* unused imports

* declare test as non windows test

* is_non_empty_file always return boolean
  • Loading branch information
nenadnoveljic authored Sep 16, 2024
1 parent ec07846 commit 93a779a
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 20 deletions.
1 change: 1 addition & 0 deletions sqlserver/changelog.d/18586.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[sqlserver] Fix ODBC config handling for Linux
35 changes: 28 additions & 7 deletions sqlserver/datadog_checks/sqlserver/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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)
Expand Down
51 changes: 38 additions & 13 deletions sqlserver/tests/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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])
Expand Down Expand Up @@ -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"

0 comments on commit 93a779a

Please sign in to comment.